记录一次无聊的(经历了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过程…   各位客官看看即可   有深入了解的大神也给小弟多分享下,非常感谢~ 不然只能以后自己功力深了再来解释了

 

posted @ 2017-09-24 21:49  野兽'  阅读(1129)  评论(7编辑  收藏  举报