我如何通过编程方式快速前进单个Git提交?

17

我定期从 Git 收到这样的消息:

Your branch is behind the tracked remote branch 'local-master/master' 
by 3 commits, and can be fast-forwarded.

我希望能够编写shell脚本命令,实现以下功能:

  1. 如何判断当前分支是否可以从所跟踪的远程分支快进?

  2. 如何知道我的分支落后于远程分支多少个提交?

  3. 如何只快进一个提交,例如,将我的本地分支从“落后于3个提交”变为“落后于2个提交”?

(对于那些感兴趣的人,我正在尝试组建一个优质的 git/darcs 镜像。)

3个回答

11

替代方案

你提到正在为Git和Darcs制作某种类型的镜像。你可以考虑使用git fast-importgit fast-export命令,以查看它们是否提供了更好的方法来管理需要提取/提供的数据,而无需在历史记录中拖着一个工作树。

如何确定分支能否向其上游分支进行快进合并

这个问题有两个部分。首先,您必须知道或确定哪个分支是当前分支的“上游”分支。然后,一旦您知道如何引用上游分支,您就可以检查是否可以进行快进合并。

查找分支的上游分支

Git 1.7.0有一种方便的方法来查询分支跟踪的分支(即其“上游”分支)。@{upstream}对象规范语法可用作分支指定器。作为一个裸名,它引用当前已检出的分支的上游分支。作为后缀,可以用于查找当前没有检出的分支的上游分支。

对于早于1.7.0版本的Git,您将不得不自己解析分支配置选项(branch.name.remotebranch.name.merge)。或者,如果您有一个标准的命名约定,您可以使用它来确定上游分支的名称。

在本答案中,我将使用upstream来引用当前分支上游的提交。

检查能否进行快进合并

如果提交A是提交B的祖先,则可以将提交A快进到提交B。

gyim展示了一种检查此条件的方法(列出可从提交B到达的所有提交并检查列表中是否包含提交A)。也许一种更简单的检查此条件的方法是检查A是否是A和B的合并基础。

can_ff() {
    a="$(git rev-parse "$1")" &&
    test "$(git merge-base "$a" "$2")" = "$a"
}
if can_ff HEAD local-master/master; then
    echo can ff to local-master/master
else
    echo CAN NOT ff to local-master/master
fi

找到“落后提交”的数量

git rev-list ^HEAD upstream | wc -l

这并不要求HEAD可以快速前进到upstream(它只计算HEAD落后于upstream的距离,而不是upstream落后于HEAD的距离)。

向前移动一个提交

一般来说,可快速前进的历史可能不是线性的。在下面的历史DAG中,master可以快速前进到upstream,但A和B都比master“向upstream前进”了一个提交。

---o---o                      master
       |\
       | A--o--o--o--o--o--o  upstream
        \                 /
         B---o---o---o---o

你可以像线性历史一样跟随其中一条分支,但只能一直追溯到合并提交的直接祖先。

修订历史命令有一个--first-parent选项,可以轻松地跟踪仅导致合并提交的第一个父提交。将其与git reset结合使用,您可以有效地“逐个提交地”将一个分支“向前拉”。

git reset --hard "$(git rev-list --first-parent --topo-order --reverse ^HEAD upstream | head -1)"

在另一个答案的评论中,您表达了对于git reset的恐惧。如果您担心破坏某个分支,那么您可以使用临时分支或使用分离HEAD作为未命名分支。只要您的工作树干净并且不介意移动分支(或分离的HEAD),git reset --hard不会破坏任何东西。如果您仍然担心,您应该认真考虑使用git fast-export,您不必接触工作树。

跟随不同的父级将更加困难。您可能需要编写自己的历史遍历器,以便您可以为每个合并提供“向哪个方向”的建议。

当您向前移动到接近合并点时,DAG将如下图所示(拓扑结构与之前相同,只有master标签已移动):

---o---o--A--o--o--o--o--o    master
       |                  \
       |                   o  upstream
        \                 /
         B---o---o---o---o

如果你“向前移动一次提交”,那么你将转到合并。这也将使从B开始的所有提交“带入”(从master可达)。如果您认为“向前移动一次提交”只会向历史DAG中添加一个提交,则此步骤将违反该假设。

在这种情况下,您可能需要仔细考虑您真正想要做什么。把额外的提交拖进来是可以的,或者应该有一些机制让您“返回”到B的父提交,并在处理合并提交之前在该分支上向前移动吗?


这里有很多好的信息,特别是关于git-merge-base的,谢谢。大部分都是我想避免的“陷阱和问题”。我认为git-fast-export在根本上与darcs的业务方式不一致,事实上,获取准确信息的唯一方法是逐步浏览历史记录。我希望我使用仓库的方式能确保我避免这些问题。+1 - Norman Ramsey

10

如果当前提交是远程分支头的祖先,则可以将远程分支快进到本地分支。换句话说,如果远程分支的“单一历史记录”包含当前提交(因为如果是这样,可以确定新的提交是“基于”当前提交进行的),那么可以将远程分支安全地快进。

因此,确定远程分支是否可以快进的一种安全方法是:

# Convert reference names to commit IDs
current_commit=$(git rev-parse HEAD)
remote_commit=$(git rev-parse remote_name/remote_branch_name)

# Call git log so that it prints only commit IDs
log=$(git log --topo-order --format='%H' $remote_commit | grep $current_commit)

# Check the existence of the current commit in the log
if [ ! -z "$log" ]
  then echo 'Remote branch can be fast-forwarded!'
fi

注意,git log没有使用--all参数(该参数将列出所有分支),因此当前提交不可能在“侧分支”上并仍然打印在输出中。

当前提交之前的行数等于$log$在$current_commit$之前的行数。

如果你想快进只有一次提交,你需要获取当前提交之前的一行(例如使用grep -B 1命令),然后将本地分支重置到这个提交。

更新: 您可以使用git log commit1..commit2来确定快进提交的数量:

if [ ! -z "$log" ]
then
  # print the number of commits ahead of the current commit
  ff_commits=$(git log --topo-order --format='%H' \
    $current_commit..$remote_commit | wc -l)
  echo "Number of fast-forwarding commits: $ff_commits"

  # fast-forward only one commit
  if [ $ff_commits -gt 1 ]
  then
    next_commit=$(git log --topo-order --format='%H' \
      $current_commit..$remote_commit | tail -1)
    git reset --hard $next_commit
  fi
fi

当然,如果你将第一次调用的结果保存到一个文件中,你可以只使用一次git log命令来实现这个目标。


1
谢谢,这种方法看起来相当有原则性。git-rev-parse的手册是我见过的最糟糕的手册之一... +1 - Norman Ramsey
程序的简要描述(“挑选并处理参数”)其实并不是太具有信息性 :D 它用于将引用名称(HEAD、origin/master)转换为提交 ID。我添加了一些注释到代码中,所以现在可能更容易理解了。 - gyim
谢谢解释。如果我们使用git merge $next_commit代替git reset --hard $next_commit会怎样呢?或者我漏掉了什么吗? - Venkat D.
“git reset --hard” 正是我想要的,可以快进几个提交而不是整个分支。谢谢。 - Felipe Alvarez

9

这可能不是最优雅的方法,但它有效:

$ git fetch
$ git status | sed -n 2p
# 您的分支比'origin/master'落后23个提交,可以进行快进。
$ git reset origin/master~22 > /dev/null
$ git status | sed -n 2p
# 您的分支比'origin/master'落后22个提交,可以进行快进。

谢谢 +1。我有点害怕 git reset,所以最好非常确定这个东西可以快进。很遗憾 git status 的这种行为没有记录在案。 - Norman Ramsey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接