git 在不破坏任何现有分支的情况下创建远程分支

git create remote branch without clobbering any existing branch

我想在本地和远程创建一个开发分支,而不会意外干扰任何其他人的开发分支。创建本地分支很容易,并且不会受到任何竞争条件的影响,但是安全地创建远程分支很棘手。

假设我想创建一个名为 cleanup 的分支,但其他人可能有相同的想法并在我之前创建了自己的名为 cleanup 的分支。如果我只是 git push --set-upstream origin cleanup,也许它会创建一个新的远程分支,或者它可能会快进一个已经存在的分支。

如果远程分支已经存在,我希望git push ...失败,这样我就可以选择不同的分支名称。

我已经知道了几个不完美的解决方案,比如 git fetch 然后 quickly push;这仍然取决于比赛条件。或者做一个git push,注意命令输出中是否存在远程分支,然后尝试从不良情况中恢复;这太混乱了,并且在恢复过程中会遇到更糟糕的比赛条件。也可以以各种方式使用 git push --force-with-lease 来做类似的事情,但我最接近解决问题的方法是拒绝创建分支而不是快速转发,这与我的目标相反。

UPDATE:理想情况下,解决方案的算法复杂度不依赖于树中文件的数量或 git 历史记录的长度。需要重新克隆存储库或删除并重新检出工作树中所有文件的解决方案是不可接受的。

使用新的根提交创建分支。由于 root 提交没有任何 parents,它不可能是 fast-forward.

然后使用 --force-with-lease 将临时根提交替换为您想要的真实提交。

编辑:self-contained 脚本以证明其有效:

#!/bin/bash
set -e

# make this script idempotent
rm -rf /tmp/git-remote-demo/
mkdir -p /tmp/git-remote-demo/
cd /tmp/git-remote-demo/

# create a new "remote" repo and a couple of clones.
# (i.e what most real-world repos already look like)
git init --bare ./remote.git/
echo
git clone ./remote.git user1/
(cd user1/;
echo 'this is a readme' > readme.txt
git add readme.txt
git commit -m 'initial commit'
echo 'it has been modified' >> readme.txt
git add readme.txt
git commit -m 'modify readme'
git push origin master
)
echo
git clone ./remote.git user2/
echo

# then each user does work on their part
for user in user1 user2
do
(
    cd "$user"
    echo "I am $user"

    # work on a feature
    echo "work by $user" > readme.txt
    git add readme.txt
    git commit -m "work by $user"

    # interesting part starts here
    empty_tree="$(true | git mktree)"

    temp_commit="$(git commit-tree "$empty_tree" -m "temp commit for $user")"
    # demonstrate that this works even for race conditions
    git push origin "$temp_commit":refs/heads/race || echo "race push failed for $user"
    # demonstrate that this works normally
    { git push origin "$temp_commit":refs/heads/full && git push origin master:full --force-with-lease; } || echo "full push failed for $user"
    echo
)
done

echo "Remote branches:"
(cd remote.git && git branch -av)

所以,您有一个海报板回购,每个人都在其中推送可能值得分享或其他任何内容的 WIP 提交。还有其他工作流程不会导致像您这样的问题,运行 关闭拉取请求等,但所有工作流程都有其缺点。缩放可能是海报板存储库的一个缺点,但名称空间冲突并不难避免。

分支名称是 repo-local。回购协议之间的任何通信都是为了方便和协作;使用相同的名称通常很方便,但分支机构使用本地名称或根本不共享名称通常也很方便。

继续你所说的,所以它就在这里。

git push -u origin featureb:thejoshwolfe/featureb

第一次推送你的 featureb 分支时,我会推送 :jthill/featureb 等。你的回购是你的,回购中的分支跟踪你说的任何上游分支。

编辑:Git 默认情况下,将拒绝不包含分支的所有当前引用历史记录的推送。如果两个人试图将相互冲突的历史推送到同一个分支名称,git 将拒绝排在第二位的那个。您可以使用 --force-with-lease 在大容量 grand-central-station 回购中进行安全强制推送,但还有其他方法可以避免该问题。

要保留或使新分支名称失败,请执行

git push origin $(git commit-tree -m - `:|git mktree` <&-):refs/heads/newbranch

如果这个名字是你的,你现在可以安全地用 git push -u origin +newbranch.

强行推过它
function git-new-branch() {
    # usage: git-new-branch branchname
    git push origin $(git commit-tree -m "" $(git mktree <&-) <&-):refs/heads/ || return 1
    git checkout -b 
    git push --set-upstream --force-with-lease origin 
}

说明

git mktreegit commit-tree 读取标准输入,因此使用 <%- 关闭这些进程的标准输入。

git mktree 是幂等的,打印空树的 sha1。

git commit-tree 使用空树进行空提交,重要的是提交没有 parents。 git commit-tree 也打印 sha1,但它不是幂等的。提交有您的 user.name 和时间戳。此外,此提交作为孤立提交进入 .git 目录。孤立的提交最终将由 git 自动进行垃圾回收;参见 git help gc-m "" 表示提交消息将为空。但是none这件事太重要了,因为这个提交将被使用一次并立即被放弃。

第一个 git push 将空提交推送到您指定的分支名称。如果分支已经存在,它肯定会被拒绝,因为空提交没有parents,因此推送它不可能导致快进。 (并且由于提交是在每次调用时重新生成的,因此提交不可能已经在远程仓库中的任何地方。)

如果命令失败,|| return 1 将中止该功能。

git checkout -b 命令没有错误检查。此函数假定您还没有具有给定名称的本地分支。

第二个git push使用--force-with-lease,这意味着远程分支只有在它仍然是我们认为的那样时才会更新,也就是我们刚刚推送的空提交。

学分

感谢 o11c 和 jthill 的回答为这个解决方案提供了灵感。