如何以编程方式快进单个 git 提交?

发布于 2024-09-02 18:29:47 字数 417 浏览 2 评论 0原文

我定期从 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 镜像。)

I periodically get message from git that look like this:

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

I would like to be able to write commands in a shell script that can do the following:

  1. How can I tell if my current branch can be fast-forwarded from the remote branch it is tracking?

  2. How can I tell how many commits "behind" my branch is?

  3. How can I fast-forward by just one commit, so that for example, my local branch would go from "behind by 3 commits" to "behind by 2 commits"?

(For those who are interested, I am trying to put together a quality git/darcs mirror.)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

阿楠 2024-09-09 18:29:47

替代方法

您提到您正在为 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 可以快进到上游 (它只计算 HEAD 落后上游多远,而不计算上游落后 HEAD 多远)。

向前推进一次提交

一般来说,可快进的历史记录可能不是线性的。在下面的历史 DAG 中,master 可以快进到上游,但 A 和 B 都是从 master 上“一次提交”向上游方向前进。

---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重置的恐惧。如果您担心损坏某些分支,那么您可以使用临时分支或使用分离的 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

当您向前移动到距离合并不远的点 如果你“向前推进一个提交”,你将进入合并。这也将“引入”(使 master 可以访问)从 B 到合并提交的所有提交。如果您假设“向前推进一个提交”只会向历史 DAG 添加一个提交,那么此步骤将违反该假设。

您可能需要仔细考虑在这种情况下您真正想要做什么。像这样拖入额外的提交是可以的,或者应该有某种机制可以在处理合并提交之前“返回”B 的父级并在该分支上前进?

Alternate Approaches

You mention that you are working on some sort of mirror for Git and Darcs. Instead of dragging a working tree through history, you might instead look at the git fast-import and git fast-export commands to see if they offer a better way to manage the data you need to extract/provide.

How to Tell Whether a Branch Can Fast-Forward to Its Upstream Branch

There are two parts to this. First, you have to either know or determine which branch is the current branch’s “upstream”. Then, once you know how to refer to the upstream, you check for the ability to fast-forward.

Finding the Upstream for a Branch

Git 1.7.0 has a convenient way to query which branch a branch tracks (its “upstream” branch). The @{upstream} object specification syntax can be used as a branch specifier. As a bare name, it refers to the upstream branch for the branch that is currently checked out. As a suffix, it can be used to find the upstream branch for branches that are not currently checked out.

For Gits earlier than 1.7.0, you will have to parse the branch configuration options yourself (branch.name.remote and branch.name.merge). Alternatively, if you have a standard naming convention, you can just use that to determine a name for the upstream branch.

In this answer I will write upstream to refer to the commit at the tip of the branch that is upstream of the current branch.

Checking for Ability to Fast-Forward

A branch at commit A can be fast-forwarded to commit B if and only if A is an ancestor of B.

gyim shows one way to check for this condition (list all the commits reachable from B and check for A in the list). Perhaps a simpler way to check for this condition is to check that A is the merge base of A and 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

Finding the Number of “Commits Behind”

git rev-list ^HEAD upstream | wc -l

This does not require that HEAD can fast-forward to upstream (it only counts how far HEAD is behind upstream, not how far upstream is behind HEAD).

Move Forward by One Commit

In general, a fast-forward-able history may not be linear. In the history DAG below, master could fast-forward to upstream, but both A and B are “one commit forward” from master on the way towards upstream.

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

You can follow one side as if it was a linear history, but only up to the immediate ancestor of the merge commit.

The revision walking commands have a --first-parent option that makes it easy to follow only the commits that lead to the first parent of merge commits. Combine this with git reset and you can effectively drag a branch “forward, one commit at a time”.

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

In a comment on another answer, you express from fear of git reset. If you are worried about corrupting some branch, then you can either use a temporary branch or use a detached HEAD as an unnamed branch. As long as your working tree is clean and you do not mind moving a branch (or the detached HEAD), git reset --hard will not trash anything. If you are still worried, you should seriously look into using git fast-export where you do not have to touch the working tree at all.

Following a different parent would be more difficult. You would probably have to write your own history walker so that you could give it advice as to “which direction” you wanted to go for each merge.

When you have moved forward to a point just short of the merge, the DAG will look like this (the topology is the same as before, it is only the master label that has moved):

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

At this point if you “move forward one commit”, you will move to the merge. This will also “bring in” (make reachable from master) all the commits from B up to the merge commit. If you assume that “moving forward one commit” will only add one commit to the history DAG, then this step will violate that assumption.

You will probably want to carefully consider what you really want to do in this case. It is OK to just drag in extra commits like this, or should there be some mechanism for “going back” to the parent of B and moving forward on that branch before you process the merge commit?

唔猫 2024-09-09 18:29:47

如果当前提交是远程分支头的祖先,则远程分支可以快进到本地分支。换句话说,如果远程分支的“单分支历史记录”包含当前提交(因为如果包含,则可以肯定新提交已提交“到”当前提交),

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

# 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 调用来完成此操作第一次调用文件的结果。

The remote branch can be fast-forwarded to the local branch if the current commit is the ancestor of the remote branch head. In other words, if the "one-branch history" of the remote branch contains the current commit (because if it does, it is sure that the new commits were committed "onto" the current commit)

So a safe way to determine whether the remote branch can be fast-forwarded:

# 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

Note that git log was called without the --all parameter (which would list all branches), so it is not possible that the current commit is on a "side branch" and is still printed on the output.

The number of commits ahead of the current commit equals the number of rows in $log before $current_commit.

If you want to fast-forward only one commit, you take the row previous to the current commit (with grep -B 1, for example), and reset the local branch to this commit.

UPDATE: you can use git log commit1..commit2 to determine the number of fast-forwarding commits:

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

Of course, you can do this with one git log call if you save the result of the first call into a file.

送你一个梦 2024-09-09 18:29:47

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

$ git fetch
$ git status | sed -n 2p
# Your branch is behind 'origin/master' by 23 commits, and can be fast-forwarded.
$ git reset origin/master~22 > /dev/null
$ git status | sed -n 2p
# Your branch is behind 'origin/master' by 22 commits, and can be fast-forwarded.

This is probably not the most elegant, but it works:

$ git fetch
$ git status | sed -n 2p
# Your branch is behind 'origin/master' by 23 commits, and can be fast-forwarded.
$ git reset origin/master~22 > /dev/null
$ git status | sed -n 2p
# Your branch is behind 'origin/master' by 22 commits, and can be fast-forwarded.
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文