我想在本地和远程创建一个开发分支,而不会意外干扰其他人的开发分支。创建本地分支很容易,并且不受任何竞争条件的影响,但安全地创建远程分支很棘手。
假设我想创建一个名为cleanup
的分支,但其他人可能有同样的想法,并在我之前创建了自己的名为cleanup
的分支。如果我只是git push --set-upstream origin cleanup
,也许它会创建一个新的远程分支,或者它会快进一个已经存在的分支。
如果远程分支已存在,我希望git push ...
失败,以便我可以选择其他分支名称。
我已经知道几个不完美的解决方案,比如git fetch
然后快速推送;这仍然受竞争条件的影响。或者做一个git push
,注意命令的输出中是否存在远程分支,然后尝试从不希望的情况中恢复;这是非常混乱的,并且在恢复期间受到更糟糕的竞争条件的影响。也可以以各种方式使用git push --force-with-lease
来做类似的事情,但我最接近解决问题的方法是拒绝创建分支而不是快速前进,这与我的目标相反。
更新:理想情况下,解决方案不会对树中的文件数量或 git 历史记录的长度产生算法复杂性依赖。需要创建存储库的新克隆或删除并重新签出工作树中的所有文件的解决方案是不可接受的。
function git-new-branch() {
# usage: git-new-branch branchname
git push origin $(git commit-tree -m "" $(git mktree <&-) <&-):refs/heads/$1 || return 1
git checkout -b $1
git push --set-upstream --force-with-lease origin $1
}
解释
git mktree
和git commit-tree
读取标准,因此请使用<%-
关闭这些进程的 stdin。
git mktree
是幂等的,并打印空树的 sha1。
git commit-tree
使用空树进行空提交,重要的是提交没有父级。git commit-tree
还打印 sha1,但它不是幂等的。提交具有您的user.name
和时间戳。此外,此提交作为孤立提交进入.git
目录。孤立的提交最终将由git
自动进行垃圾回收;请参阅git help gc
。-m ""
表示提交消息将为空。但这些都无关紧要,因为此提交将使用一次并立即放弃。
第一个git push
将空提交推送到您指定的分支名称。如果分支已经存在,则保证会被拒绝,因为空提交没有父级,因此推送它不可能导致快进。(由于提交是在每次调用时重新生成的,因此提交不可能已经在远程存储库中的任何位置。
如果命令失败,|| return 1
将中止函数。
git checkout -b
命令没有错误检查。此函数假定您还没有具有给定名称的本地分支。
第二个git push
使用--force-with-lease
,这意味着远程分支只有在它仍然是我们认为的那样时才会被更新,这是我们刚刚推送的空提交。
学分
感谢o11c和jthill的回答为这个解决方案提供了灵感。
所以,你有一个海报板存储库,每个人都在推送可能值得分享的 WIP 提交或其他什么。 还有其他工作流不会导致像您这样的问题,运行拉取请求等,但所有工作流都有其缺点。 缩放可能是海报板存储库的缺点,但命名空间冲突并不难避免。
分支名称是存储库本地的。 回购之间的任何通信都是严格意义上的方便和协作问题;使用相同的名称通常很方便,但为分支使用本地名称或根本不共享它通常也很方便。
继续你在这里说的,所以它在这里。
git push -u origin featureb:thejoshwolfe/featureb
第一次推你的featureb
分支时,我会推:jthill/featureb
等。 你的存储库是你的,存储库中的分支跟踪你所说的任何上游分支。
编辑:默认情况下,Git 将拒绝不包含分支的所有当前引用历史记录的推送。 如果两个人试图将冲突的历史推送到一个分支名称中,git 将拒绝第二个分支名称。 您可以使用--force-with-lease
在大容量中央车站存储库中进行安全的强制推送,但还有其他方法可以避免此问题。
要保留或失败新的分支名称,请执行
git push origin $(git commit-tree -m - `:|git mktree` <&-):refs/heads/newbranch
如果这个名字是你的,你现在可以安全地用git push -u origin +newbranch
强制推倒它。
使用新的根提交创建分支。由于根提交没有任何父级,因此它不能快进。
然后使用--force-with-lease
将临时根提交替换为所需的实际提交。
编辑:自包含脚本以证明其工作:
#!/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)