記錄一次無聊的(經歷了Nodejs -> Shell -> C)的探索問題過程


提出問題

在運行項目的服務器的git是1.8.3.1版本的時候,pm2 deploy 項目,服務器fetch不到最新的一次commit。

對於這個問題,在pm2的github也有issues討論。然后開issues的人表示 pm2-deploy is garbage  並且覺得  I find it funny that it is easier for the authors to blame the problem on git or anything else rather than change one single line of code to make it work... excelent pm2 deploy couldn't be any more wonderfull.  em…   呵呵,表示不服,其實這個問題真的有一半原因得歸咎於git …

解決方案(針對服務器上git版本 < 1.9.0)

1. 升級服務器git版本 >= 1.9.0

2. 回退pm2版本至1.x,如果該項目用不到pm2 后期版本的一些功能的話...

由於文章中的測試驗證過程略顯無聊(一步一步的挖掘問題所在),且以上已給出解決方案,有遇到類似問題的小伙伴可參考方案解決問題,有興趣交流下debugger過程的同學可繼續往下看。

git都出了2.14.1了,為什么還在用1.8.3.1的老版本呢?

我們用的是阿里雲的服務器,使用yum安裝的git包,然而yum源里的git包是1.8.3.1的,對於像我這種的新手來說,要不是遇到問題,我的態度是 "還有這操作? (。◕ˇ∀ˇ◕)"

進入探索流程

問題由pm2出發,首先我們找到pm2是怎么處理deploy命令的:

打開 pm2 地址:https://github.com/Unitech/pm2  ,進入 lib/API.js ,我們在文件中找到:

require('./API/Deploy.js')(API);

於是打開該Deploy.js:

var Deploy = require('pm2-deploy');

    /*.其他代碼.*/

module.exports = function (CLI) {
    CLI.prototype.deploy = function (file, commands, cb) {
        Deploy.deployForEnv(json_conf.deploy, env, args, function (err, data) {

            /*.其他代碼.*/

            if (err) {
                Common.printError('Deploy failed');
                return cb ? cb(err) : that.exitCli(cst.ERROR_EXIT);
            }
            Common.printOut('--> Success');
            return cb ? cb(null, data) : that.exitCli(cst.SUCCESS_EXIT);
        });
    }
}

由此,我們又將查找對象指向了pm2-deploy模塊的Deploy對象

打開 pm2-deploy 地址:https://github.com/Unitech/pm2-deploy ,找到deploy.js 並且打開:

// 在里面找到 deployForEnv 方法 並發現在進行一堆參數處理后走到了spawn方法,縱觀spawn方法,執行了個shell腳本

function spawn(hostJSON, args, cb) {
    var shellSyntaxCommand = "echo '" + hostJSON + "' | \"" + __dirname.replace(/\\/g, '/') + "/deploy\" " + args.join(' ');
    var proc = childProcess.spawn('sh', ['-c', shellSyntaxCommand], {
        stdio: 'inherit'
    });

    proc.on('error', function (e) {
        return cb(e.stack || e);
    });

    proc.on('close', function (code) {
        if (code == 0) return cb(null, args);
        else return cb(code);
    });
}

好了,現在到了shell了...

打開deploy的sh文件 => 找到deploy方法(所幸作者打了閃閃發光的注釋,很簡單的我們就找到哪里是重點):

deploy() {
    # pre-deploy hook

    # fetch source
    log fetching updates
    run "cd $path/source && git fetch --depth=5 --all --tags"
    test $? -eq 0 || abort fetch failed

    # latest tags & reset HEAD & link current & deploy log 等操作
}

在這里看到,最新版本的pm2-deploy執行了 git fetch --depth=5 --all --tags  來fetch最新一次的提交,那么這個fetch是不是存在問題呢?因為git-1.8.3.1對應使用pm2-1.x版本可行,那么我們看看pm2-deploy早期的deploy里的fetch是怎么寫的,打開早期版本: https://github.com/Unitech/pm2-deploy/blob/0.2.0/deploy

deploy() {
    # pre-deploy hook

    # fetch source
    log fetching updates
    run "cd $path/source && git fetch --all"
    test $? -eq 0 || abort fetch failed
      
    # latest tags & reset HEAD & link current & deploy log 等操作
}

好了,看出區別了, git fetch --all  和  git fetch --depth=5 --all --tags 

於是上git官網查這2個參數,然后失望而歸,官網文檔最老版本僅能找到1.9.0的,那么怎么辦?

我們上這個網站:https://www.kernel.org 在其/pub/software/scm/git/文件夾下可以看到各個版本的git源碼壓縮包,下載git-1.8.3.1.tar.gz => 解壓 => vscode中打開文件夾 ,git每個版本包自帶文檔,打開看看,Documentation/fetch-options.txt 1.9.0 對比 1.8.3.1:

/*
1.8.3.1
-t::
--tags::
    This is a short-hand for giving "refs/tags/*:refs/tags/*"
    refspec from the command line, to ask all tags to be fetched
    and stored locally.  Because this acts as an explicit
    refspec, the default refspecs (configured with the
    remote.$name.fetch variable) are overridden and not used.

*/

/*
1.9.0
-t
--tags
  Fetch all tags from the remote (i.e., fetch remote tags refs/tags/*
  into local tags with the same name), in addition to whatever else
  would otherwise be fetched. Using this option alone does not subject
  tags to pruning, even if --prune is used (though tags may be pruned
  anyway if they are also the destination of an explicit refspec; see 
  --prune).
*/

大家來找茬 + 推測 --tags 參數的描述可能是導致問題的關鍵 :refspec(Reference Specification/參考規范,這里個人覺得理解為本地和遠程的對應關系更適合),1.8.3.1 的說這個操作是基於明確的映射關系滴,所以默認的映射關系將被覆蓋並且不被使用。

那么到底是不是refspecs存在問題呢? 還是明明是 remotes tags 存在問題導致不能fetch到?先測試看看結果,不同版本、不同參數的 git fetch 測試:

初始化

創建文件 test.txt 內容為 test > 3

push 到 test 分支

服務器 pull 代碼,確保test.txt文件存在且內容值為 test > 3

如圖:

1.8.3.1版本測試:

修改test.txt 為 test > 4

服務器上執行 git fetch --all --tags --depth=5

效果如圖:

輸出 test > 3

接下來去掉--tags 參數試試

服務器上執行 git fetch --all --depth=5

效果如圖:

輸出 test > 4

結果正確(但由於沒有--tags參數,其實並沒有拉取到所有tags)

人為指定映射關系,驗證是不是因為 --tags 影響refspec而導致問題

到這里,對於問題而言我們確定了是 --tags 導致fetch不到最新代碼,但不能確定是refspec的問題,那么接着下一步的驗證:

修改test.txt 為 test > 5

push 代碼

服務器上執行 git fetch origin test:refs/remotes/origin/test --depth=5 --all --tags

效果如圖:

英文版報錯:fetch --all does not take a repository argument

那么暫時去掉 --all ,接下去驗證猜想

服務器上執行 git fetch origin test:refs/remotes/origin/test --depth=5 --tags

效果如圖:

輸出 test > 5

結果正確

2.7.4版本測試 

修改test.txt 為 test > 6

服務器上執行 git fetch --depth=5 --all --tags

效果如圖:

輸出 test > 6

結果正確

結論:1.8.3.1 版本 git fetch --depth=5 --all --tags  的時候由於加了--tags 導致refspec出現問題

1.8.3.1版本里的 fetch 做了什么導致refspec不正確呢?

打開 git項目 里的 builtin/fetch.c => 找到 get_ref_map

static struct ref *get_ref_map(struct transport *transport,
                   struct refspec *refs, int ref_count, int tags,
                   int *autotags)
{
    int i;
    struct ref *rm;
    struct ref *ref_map = NULL;
    struct ref **tail = &ref_map;

    const struct ref *remote_refs = transport_get_remote_refs(transport);
    int *num_i = &ref_count;    /* ++ */
    int *num_tags = &tags;    /* ++ */
    printf("ref_count ->  %d\n",*num_i);    /* ++  打印ref_count*/ 
    printf("tags -> %d\n",*num_tags);    /* ++  打印num_tags*/ 
    if (ref_count || tags == TAGS_SET) {
        for (i = 0; i < ref_count; i++) {
            get_fetch_map(remote_refs, &refs[i], &tail, 0);
            if (refs[i].dst && refs[i].dst[0])
                *autotags = 1;
        printf("autotags -> %d\n",*autotags);    /* ++  打印autotags*/ 
        }
        /* Merge everything on the command line, but not --tags */
        for (rm = ref_map; rm; rm = rm->next)
            rm->merge = 1;
        if (tags == TAGS_SET)
            get_fetch_map(remote_refs, tag_refspec, &tail, 0);
    } else {
    …
    }
    if (tags == TAGS_DEFAULT && *autotags)
        find_non_local_tags(transport, &ref_map, &tail);
    ref_remove_duplicates(ref_map);

    return ref_map;
}

上面是加了打印測試的代碼,並未修改其邏輯,然后編譯 => 配置 => 運行試試

測試ref_count和tags的打印結果

修改test.txt 為 test > 7

執行命令 git fetch --all --tags --depth=5

效果如圖:

得出結果 ref_count = 0,並且都 Already up-to-date 了,拉沒拉到最新提交,心里也有點B數了...

然后執行指定映射關系的命令 git fetch origin test:refs/remotes/origin/test --tags --depth=5

效果如圖:

由於指定了映射關系,git知道該fetch哪些代碼,於是獲取到了最新的提交。

然后再看1.9.0的 fetch 代碼

static struct ref *get_ref_map(struct transport *transport,
                   struct refspec *refspecs, int refspec_count,
                   int tags, int *autotags)
{
    int i;
    struct ref *rm;
    struct ref *ref_map = NULL;
    struct ref **tail = &ref_map;

    /* opportunistically-updated references: */
    struct ref *orefs = NULL, **oref_tail = &orefs;

    const struct ref *remote_refs = transport_get_remote_refs(transport);

    if (refspec_count) {
        for (i = 0; i < refspec_count; i++) {
            get_fetch_map(remote_refs, &refspecs[i], &tail, 0);
            if (refspecs[i].dst && refspecs[i].dst[0])
                *autotags = 1;
        }
        /* Merge everything on the command line (but not --tags) */
        for (rm = ref_map; rm; rm = rm->next)
            rm->fetch_head_status = FETCH_HEAD_MERGE;

        /*
         * For any refs that we happen to be fetching via
         * command-line arguments, the destination ref might
         * have been missing or have been different than the
         * remote-tracking ref that would be derived from the
         * configured refspec.  In these cases, we want to
         * take the opportunity to update their configured
         * remote-tracking reference.  However, we do not want
         * to mention these entries in FETCH_HEAD at all, as
         * they would simply be duplicates of existing
         * entries, so we set them FETCH_HEAD_IGNORE below.
         *
         * We compute these entries now, based only on the
         * refspecs specified on the command line.  But we add
         * them to the list following the refspecs resulting
         * from the tags option so that one of the latter,
         * which has FETCH_HEAD_NOT_FOR_MERGE, is not removed
         * by ref_remove_duplicates() in favor of one of these
         * opportunistic entries with FETCH_HEAD_IGNORE.
         */
        for (i = 0; i < transport->remote->fetch_refspec_nr; i++)
            get_fetch_map(ref_map, &transport->remote->fetch[i],
                      &oref_tail, 1);

        if (tags == TAGS_SET)
            get_fetch_map(remote_refs, tag_refspec, &tail, 0);
    } else {
       …
    }

    if (tags == TAGS_SET)
        /* also fetch all tags */
        get_fetch_map(remote_refs, tag_refspec, &tail, 0);
    else if (tags == TAGS_DEFAULT && *autotags)
        find_non_local_tags(transport, &ref_map, &tail);

    /* Now append any refs to be updated opportunistically: */
    *tail = orefs;
    for (rm = orefs; rm; rm = rm->next) {
        rm->fetch_head_status = FETCH_HEAD_IGNORE;
        tail = &rm->next;
    }

    return ref_remove_duplicates(ref_map);
}

tags == TAGS_SET 和 refspec_count 單獨判斷,在 refspec_count = 0  的時候使用默認的refspec,這樣get到的ref_map便是正確的,git之后的版本里把在 refspec_count 判斷里的tags == TAGS_SET 判斷和get_fetch_map移除了。但其實1.8.3.1官方文檔說的覆蓋和不使用默認的refspec,在上面代碼里我還是沒能看出是在哪里操作的(實在汗顏),猜測是在  Merge everything on the command line  這步,同時也求大神解釋...  之前沒接觸過 C …

其實在這過程中,也產生了個問題,就是 refspec 關系的操作是怎么處理的,這個也值得探究探究額,決定再刷刷書熟悉下git核心那塊,然后再根據源碼探一探 

git fetch 的參考文檔

官網文檔 : https://git-scm.com/docs/git-fetch

stackoverflow 大佬的回答:https://stackoverflow.com/questions/1204190/does-git-fetch-tags-include-git-fetch

以及各個版本源碼:https://github.com/git/git/ 

 

說完git fetch的鍋,然后回到之前說的 “有一半原因得歸咎於git” ,另一半鍋還是得pm2-deploy背,pm2-deploy在fetch的時候理應做個兼容,哪怕這個兼容並不是個很好的實踐(因為pm2新版本有對git倉庫的管理做了更嚴謹的把控)
比如在檢測機器上git版本< 1.9.0,則走原先的 git fetch --all

如下代碼:

version=`git --version | awk '{print $3}' | tr "." " "`
f=`echo $version | awk '{print $1}'`
s=`echo $version | awk '{print $2}'`

if [[ $f -le 1 ]] && [[ $s -le 8 ]]
then
    echo "version < 1.9.0"
else
    echo "version >= 1.9.0"
fi

 

<(▰˘◡˘▰)>  完!  就這么一段無聊的debugger過程…   各位客官看看即可   有深入了解的大神也給小弟多分享下,非常感謝~ 不然只能以后自己功力深了再來解釋了

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM