假设我有以下本地存储库,其提交树如下:
master --> a
\
\
develop c --> d
\
\
feature f --> g --> h
master
是我的这是最新的稳定版本代码,develop
是我的这是“下一个”版本代码,feature
是 a正在为 develop
准备新功能。
使用钩子,我希望能够拒绝将 feature
推送到我的远程存储库,除非提交 f
是 develop
HEAD 的直接后代。即,提交树看起来像这样,因为功能已在 d
上设置为 git rebase
。
master --> a
\
\
develop c --> d
\
\
feature f --> g --> h
那么是否有可能:
识别特征的父分支?
确定父分支中 f 是其后代的提交?
从那里我会检查父分支的 HEAD 是什么,并查看 f
前任是否与父分支 HEAD 匹配,以确定是否需要重新设置该功能。
git log --first-parent
,它将显示当前分支的所有提交,然后显示父分支及其提交
假设远程存储库有一个develop分支的副本(您最初的描述是在本地存储库中描述的,但听起来它也存在于远程),您应该能够实现我认为您想要的,但是方法和你想象的有点不同。
Git 的历史记录基于 DAG 次提交。分支(和一般的“引用”)只是指向不断增长的提交 DAG 中特定提交的临时标签。因此,分支之间的关系可能会随着时间而变化,但提交之间的关系不会。
---o---1 foo
\
2---3---o bar
\
4
\
5---6 baz
看起来 baz
是基于(旧版本的)bar
?但是如果我们删除 bar
呢?
---o---1 foo
\
2---3
\
4
\
5---6 baz
现在看起来 baz
是基于 foo
。但baz
的血统并没有改变。我们刚刚删除了一个标签(以及由此产生的悬空提交)。如果我们在 4
添加一个新标签会怎样?
---o---1 foo
\
2---3
\
4 quux
\
5---6 baz
现在看起来 baz
是基于 quux
。尽管如此,血统并没有改变,只是标签发生了变化。
但是,如果我们问的是“提交 6
是提交 3
的后代吗?” (假设 3
和 6
是完整的 SHA-1 提交名称),那么无论 bar
和 quux
标签是否存在,答案都是“是”。
因此,您可以问诸如“推送的提交是否是开发分支的当前提示的后代?”,但您不能可靠地问“推送的提交的父分支是什么?”。
一个似乎接近您想要的最可靠的问题是:
对于所有推送的提交的祖先(不包括当前的开发提示及其祖先),将当前的开发提示作为父项:是否至少存在一个这样的提交?所有这些提交都是单亲提交吗?
这可以实现为:
pushedrev=...
basename=develop
if ! baserev="$(git rev-parse --verify refs/heads/"$basename" 2>/dev/null)"; then
echo "'$basename' is missing, call for help!"
exit 1
fi
parents_of_children_of_base="$(
git rev-list --pretty=tformat:%P "$pushedrev" --not "$baserev" |
grep -F "$baserev"
)"
case ",$parents_of_children_of_base" in
,) echo "must descend from tip of '$basename'"
exit 1 ;;
,*\ *) echo "must not merge tip of '$basename' (rebase instead)"
exit 1 ;;
,*) exit 0 ;;
esac
这将涵盖您想要限制的一些内容,但可能不是所有内容。
作为参考,这里是一个扩展的示例历史:
A master
\
\ o-----J
\ / \
\ | o---K---L
\ |/
C--------------D develop
\ |\
F---G---H | F'--G'--H'
| |\
| | o---o---o---N
\ \ \ \
\ \ o---o---P
\ \
R---S
上述代码可用于拒绝 H
和 S
,同时接受 H'
、J
、K
或 N
,但它也可以接受 L
和 P
(它们涉及合并,但它们不合并 develop 的提示)。
要同时拒绝 L
和 P
,您可以更改问题并提出
对于所有被推送的提交的祖先(不包括当前的开发技巧及其祖先):是否有两个父提交?如果没有,是否至少有一个这样的提交具有开发其(唯一)父级的当前提示?
pushedrev=...
basename=develop
if ! baserev="$(git rev-parse --verify refs/heads/"$basename" 2>/dev/null)"; then
echo "'$basename' is missing, call for help!"
exit 1
fi
parents_of_commits_beyond_base="$(
git rev-list --pretty=tformat:%P "$pushedrev" --not "$baserev" |
grep -v '^commit '
)"
case "$parents_of_commits_beyond_base" in
*\ *) echo "must not push merge commits (rebase instead)"
exit 1 ;;
*"$baserev"*) exit 0 ;;
*) echo "must descend from tip of '$basename'"
exit 1 ;;
esac
改写
表达问题的另一种方式是“驻留在当前分支以外的分支上的最近提交是什么,那是哪个分支?”
一个解法
你可以用一点命令行魔法找到它
git show-branch \
| sed "s/].*//" \
| grep "\*" \
| grep -v "$(git rev-parse --abbrev-ref HEAD)" \
| head -n1 \
| sed "s/^.*\[//"
使用 AWK:
git show-branch -a \
| grep '\*' \
| grep -v `git rev-parse --abbrev-ref HEAD` \
| head -n1 \
| sed 's/[^\[]*//' \
| awk 'match($0, /\[[a-zA-Z0-9\/.-]+\]/) { print substr( $0, RSTART+1, RLENGTH-2 )}'
以下是它的工作原理:
显示所有提交的文本历史记录,包括远程分支。当前提交的祖先用星号表示。过滤掉其他所有内容。忽略当前分支中的所有提交。第一个结果将是最近的祖先分支。忽略其他结果。分支名称显示在 [括号中]。忽略括号和括号之外的所有内容。有时分支名称会包含一个 ~# 或 ^# 来指示在引用的提交和分支提示之间有多少次提交。我们不在乎。别理他们。
结果
运行上面的代码
A---B---D <-master
\
\
C---E---I <-develop
\
\
F---G---H <-topic
如果你从 H 运行它会给你 develop
,如果你从 I 运行它会给你 master
。
The code is available as a gist。
cannot handle more than 25 refs
ack
在 Mac 上不可用(有人建议用 grep
替换 ack
)
git show-branch | grep '*' | grep -v "$(git rev-parse --abbrev-ref HEAD)" | head -n1 | sed 's/.*\[\(.*\)\].*/\1/' | sed 's/[\^~].*//'
git show-branch | sed "s/].*//" | grep "\*" | grep -v "$(git rev-parse --abbrev-ref HEAD)" | head -n1 | sed "s/^.*\[//"
parent = "!git show-branch | grep '*' | grep -v \"$(git rev-parse --abbrev-ref HEAD)\" | head -n1 | sed 's/.*\\[\\(.*\\)\\].*/\\1/' | sed 's/[\\^~].*//' #"
对我有用
Git 父级
你可以只运行命令
git parent
如果您将 Joe Chrysler's answer 添加为 Git 别名,则可以找到分支的父级。它将简化使用。
使用任何文本编辑器(适用于 Linux)打开位于 "~/.gitconfig"
的 gitconfig 文件。对于 Windows,“.gitconfig”路径通常位于 C:\users\your-user\.gitconfig
。
vim ~/.gitconfig
在文件中添加以下别名命令:
[alias]
parent = "!git show-branch | grep '*' | grep -v \"$(git rev-parse --abbrev-ref HEAD)\" | head -n1 | sed 's/.*\\[\\(.*\\)\\].*/\\1/' | sed 's/[\\^~].*//' #"
保存并退出编辑器。
运行命令 git parent
。
而已!
cannot handle more than 25 refs
异常。
git show-branch 2>&1 | ...
。以下 sed/grep 将以这种方式过滤掉警告。或者,您也可以通过 2>/dev/null
将错误传送到 /dev/null。
你也可以试试:
git log --graph --decorate
git log --graph --decorate --simplify-by-decoration
其中 --graph
是可选的。
git log --graph --decorate --simplify-by-decoration --oneline
我对您的整体问题有一个解决方案(确定 feature
是否来自 develop
的尖端),但使用您概述的方法不起作用。
您可以使用 git branch --contains
列出从 develop
的尖端下降的所有分支,然后使用 grep
确保 feature
在其中。
git branch --contains develop | grep "^ *feature$"
如果它在其中,它将打印 " feature"
到标准输出并返回代码为 0。否则,它将不打印任何内容并返回代码为 1。
<branch>
,我在其中执行:git checkout -b <branch-2>
from... 这就是答案!真的不需要grep。 git branch --contains <branch>
这对我来说很好用:
git show-branch | grep '*' | grep -v "$(git rev-parse --abbrev-ref HEAD)" | head -n1 | sed 's/.*\[\(.*\)\].*/\1/' | sed 's/[\^~].*//'
礼貌的评论和回答 from droidbot 和 @Jistanidiot。
*
不是传递给 grep 的正确正则表达式。应该改用 grep -F '*'
或 grep '\*'
。否则很好的解决方案。
cannot handle more than 26 refs
。
一个解法
解决方案 based on git show-branch
对我来说不太有效(见下文),所以我将它与一个 based on git log
结合起来,最终得到了这个:
git log --decorate --simplify-by-decoration --oneline \ # selects only commits with a branch or tag
| grep -v "(HEAD" \ # removes current head (and branch)
| head -n1 \ # selects only the closest decoration
| sed 's/.* (\(.*\)) .*/\1/' \ # filters out everything but decorations
| sed 's/\(.*\), .*/\1/' \ # picks only the first decoration
| sed 's/origin\///' # strips "origin/" from the decoration
限制和注意事项
HEAD 可以分离(许多 CI 工具这样做是为了确保它们在给定分支中构建正确的提交),但原始分支和本地分支必须都在当前 HEAD 的标准或“之上”。
途中必须没有标签(我想;我没有在子分支和父分支之间使用标签测试提交的脚本)
该脚本依赖于“HEAD”始终被 log 命令列为第一个装饰的事实
在主服务器上运行脚本并在
结果
A---B---D---E---F <-origin/master, master
\ \
\ \
\ G---H---I <- origin/hotfix, hotfix
\
\
J---K---L <-origin/develop, develop
\
\
M---N---O <-origin/feature/a, feature/a
\ \
\ \
\ P---Q---R <-origin/feature/b, feature/b
\
\
S---T---U <-origin/feature/c, feature/c
尽管存在本地分支(例如,由于提交 O
直接由其 SHA 签出,因此仅存在 origin/topic
),脚本应打印如下:
对于提交 G、H、I(分支修补程序)→ 主
对于提交 M、N、O(分支功能/a)→ 开发
对于提交 S、T、U(分支功能/c)→ 开发
对于提交 P、Q、R(分支特征/b)→ 特征/a
对于提交 J、K、L(分支开发)→
对于提交 B、D、E、F(分支主控)→
- 如果 develop
的提交位于 master 的 HEAD 之上,则为 master
(~ master 可以快速开发)
为什么 show-branch 不为我工作
在以下情况下,解决方案 based on git show-branch
对我来说是不可靠的:
分离的 HEAD - 包括分离的头部情况意味着将 grep '\*' \ 替换为 `grep '!' \——这只是所有麻烦的开始
在 master 和 develop 上运行脚本分别在 develop 和 ``
master 分支(修补程序/分支)上的分支最终以 develop 作为父级,因为它们最近的 master 分支父级标记为!而不是 * 是有原因的。
"!git log --decorate --simplify-by-decoration --oneline | grep -v '(HEAD' | head -n1 | sed 's/.* (\\(.*\\)) .*/\\1/' | sed 's/\\(.*\\), .*/\\1/' | sed 's/origin\\///'"
git --no-pager log --simplify-by-decoration --format="format:%D%n" -n1 --decorate-refs-exclude=refs/tags HEAD~1
,它可能显示多个分支。添加 | sed 's/\(.*\), .+/\1/'
以获取第一个分支。
下面会发生什么
要点:
gitr:查找(可能多个)相关分支
gitp:通过类似 git-flow 的内部规则/正则表达式找到可能的父母
为什么会有人想看这篇长文?因为虽然以前的答案清楚地理解了原始问题的问题,但它们没有得到正确/有意义的结果;或准确地解决不同的问题。
随意查看第一部分;它解决了“找东西”的问题,并且应该突出问题的范围。对于某些人来说,这可能就足够了。
这将向您展示一种从 git 中提取正确且有意义的结果的方法(您可能不喜欢它们),并展示一种将您对约定的知识应用于这些结果以提取您真正需要的结果的方法。
以下部分涵盖:
一个公正的问题和解决方案:使用 git show-branch 的最近的 git 分支。预期结果应该是什么样子
使用 git show-branch 最近的 git 分支。
预期结果应该是什么样子
示例图表和结果
批处理分支:解决 git show-branch 的限制
有偏见的问题和解决方案:引入(命名)约定以改善结果
问题的问题
如前所述,git 不跟踪分支之间的关系;分支只是引用提交的名称。在官方 git 文档和其他来源中,我们经常会遇到一些误导性的图表,例如:
A---B---C---D <- master branch
\
E---F <- work branch
让我们更改图表的形式和分层提示名称以显示等效图表:
E---F <- jack
/
A---B
\
C---D <- jill
该图(因此是 git)绝对没有告诉我们首先创建哪个分支(因此,哪个分支从另一个分支)。
第一个图中 master
是 work
的父级,这是一个约定问题。
所以
简单的工具将产生忽略偏差的响应
更复杂的工具包含约定(偏差)。
一个公正的问题
首先,我必须首先承认乔克莱斯勒的回应,这里的其他回应,以及周围的许多评论/建议;他们启发并为我指明了方向!
请允许我重新措辞乔的措辞,考虑到与最近提交相关的多个分支(它发生了!):
“驻留在当前分支以外的分支上的最近提交是什么,那是哪个分支?”
或者,换句话说:
第一季度
给定一个分支 B:考虑与其他分支共享的最接近 B'HEAD 的提交 C(C 可能是 B'HEAD):除了 B 之外,哪些分支的提交历史中有 C?
一个公正的解决方案
预先道歉;似乎人们更喜欢单线。随意提出(可读/可维护的)改进建议!
#!/usr/local/bin/bash
# git show-branch supports 29 branches; reserve 1 for current branch
GIT_SHOW_BRANCH_MAX=28
CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
if (( $? != 0 )); then
echo "Failed to determine git branch; is this a git repo?" >&2
exit 1
fi
##
# Given Params:
# EXCEPT : $1
# VALUES : $2..N
#
# Return all values except EXCEPT, in order.
#
function valuesExcept() {
local except=$1 ; shift
for value in "$@"; do
if [[ "$value" != "$except" ]]; then
echo $value
fi
done
}
##
# Given Params:
# BASE_BRANCH : $1 : base branch; default is current branch
# BRANCHES : [ $2 .. $N ] : list of unique branch names (no duplicates);
# perhaps possible parents.
# Default is all branches except base branch.
#
# For the most recent commit in the commit history for BASE_BRANCH that is
# also in the commit history of at least one branch in BRANCHES: output all
# BRANCHES that share that commit in their commit history.
#
function nearestCommonBranches() {
local BASE_BRANCH
if [[ -z "${1+x}" || "$1" == '.' ]]; then
BASE_BRANCH="$CURRENT_BRANCH"
else
BASE_BRANCH="$1"
fi
shift
local -a CANDIDATES
if [[ -z "${1+x}" ]]; then
CANDIDATES=( $(git rev-parse --symbolic --branches) )
else
CANDIDATES=("$@")
fi
local BRANCHES=( $(valuesExcept "$BASE_BRANCH" "${CANDIDATES[@]}") )
local BRANCH_COUNT=${#BRANCHES[@]}
if (( $BRANCH_COUNT > $GIT_SHOW_BRANCH_MAX )); then
echo "Too many branches: limit $GIT_SHOW_BRANCH_MAX" >&2
exit 1
fi
local MAP=( $(git show-branch --topo-order "${BRANCHES[@]}" "$BASE_BRANCH" \
| tail -n +$(($BRANCH_COUNT+3)) \
| sed "s/ \[.*$//" \
| sed "s/ /_/g" \
| sed "s/*/+/g" \
| egrep '^_*[^_].*[^_]$' \
| head -n1 \
| sed 's/\(.\)/\1\n/g'
) )
for idx in "${!BRANCHES[@]}"; do
## to include "merge", symbolized by '-', use
## ALT: if [[ "${MAP[$idx]}" != "_" ]]
if [[ "${MAP[$idx]}" == "+" ]]; then
echo "${BRANCHES[$idx]}"
fi
done
}
# Usage: gitr [ baseBranch [branchToConsider]* ]
# baseBranch: '.' (no quotes needed) corresponds to default current branch
# branchToConsider* : list of unique branch names (no duplicates);
# perhaps possible (bias?) parents.
# Default is all branches except base branch.
nearestCommonBranches "${@}"
这个怎么运作
考虑输出: git show-branch
对于 git show-branch --topo-order feature/g hotfix master release/2 release/3 feature/d
,输出类似于:
! [feature/g] TEAM-12345: create X
* [hotfix] TEAM-12345: create G
! [master] TEAM-12345: create E
! [release/2] TEAM-12345: create C
! [release/3] TEAM-12345: create C
! [feature/d] TEAM-12345: create S
------
+ [feature/g] TEAM-12345: create X
+ [feature/g^] TEAM-12345: create W
+ [feature/d] TEAM-12345: create S
+ [feature/d^] TEAM-12345: create R
+ [feature/d~2] TEAM-12345: create Q
...
+ [master] TEAM-12345: create E
* [hotfix] TEAM-12345: create G
* [hotfix^] TEAM-12345: create F
*+ [master^] TEAM-12345: create D
+*+++ [release/2] TEAM-12345: create C
+*++++ [feature/d~8] TEAM-12345: create B
几点:
原始命令在命令行上列出了 N (6) 个分支名称
这些分支名称按顺序显示为输出的前 N 行
标题后面的行代表提交
提交行的前 N 列(作为一个整体)表示“分支/提交矩阵”,其中 X 列中的单个字符表示分支(标题行 X)与当前提交之间的关系(或缺失)。
主要步骤
给定一个 BASE_BRANCH 给定一个不包含 BASE_BRANCH 的有序集(唯一)BRANCHES 为简洁起见,设 N 为 BRANCH_COUNT,即 BRANCHES 的大小;它不包括 BASE_BRANCH git show-branch --topo-order $BRANCHES $BASE_BRANCH:由于 BRANCHES 仅包含唯一名称(假定有效),因此名称将映射 1-1 与输出的标题行,并对应于第一个 N分支/提交矩阵的列。由于 BASE_BRANCH 不在 BRANCHES 中,它将是标题行的最后一行,并且对应于最后一列分支/提交矩阵。 tail:从第 N+3 行开始;扔掉前 N+2 行:N 个分支 + 基本分支 + 分隔行 ---... sed:这些可以组合成一个......但为了清楚起见分开删除分支/提交矩阵后的所有内容 替换空格为下划线'_';我的主要原因是避免潜在的 IFS 解析麻烦和调试/可读性。将 * 替换为 +; base 分支总是在最后一列,这就足够了。此外,如果不理会它会通过 bash 路径名扩展,这总是很有趣 * egrep: grep 用于映射到至少一个分支 ([^_]) 和 BASE_BRANCH ([^_]$) 的提交。也许那个基本分支模式应该是\+$? head -n1:取第一个剩余的提交 sed:将分支/提交矩阵的每个字符分隔为单独的行。捕获数组 MAP 中的行,此时我们有两个数组: BRANCHES:长度 N MAP:长度 N+1:前 N 个元素 1-1 与 BRANCHES,最后一个元素对应于 BASE_BRANCH。遍历 BRANCHES(这就是我们想要的,而且它更短)并检查 MAP 中的相应元素:如果 MAP[$idx] 为 +,则输出 BRANCH[$idx]。
示例图表和结果
考虑以下有点人为的示例图:
将使用有偏见的名称,因为它们有助于(我)权衡和考虑结果。
假设合并存在并且被忽略。
该图通常试图突出显示分支(分叉),而不是在视觉上暗示偏好/层次结构;具有讽刺意味的是,在我完成这件事后,主人脱颖而出。
J <- feature/b
/
H
/ \
/ I <- feature/a
/
D---E <- master
/ \
/ F---G <- hotfix
/
A---B---C <- feature/f, release/2, release/3
\ \
\ W--X <- feature/g
\
\ M <- support/1
\ /
K---L <- release/4
\
\ T---U---V <- feature/e
\ /
N---O
\
P <- feature/c
\
Q---R---S <- feature/d
示例图的无偏结果
假设脚本在可执行文件 gitr
中,然后运行:
gitr <baseBranch>
对于不同的分支B
,我们得到以下结果:
GIVEN B Shared Commit C Branches !B 与 C 在他们的历史?特征/a H 特征/b 特征/b H 特征/a 特征/c P 特征/d 特征/d P 特征/c 特征/e O 特征/c, 特征/d 特征/f C 特征/a, 特征/b , feature/g, hotfix, master, release/2, release/3 feature/g C feature/a, feature/b, feature/f, hotfix, master, release/2, release/3 hotfix D feature/a, feature /b, master master D feature/a, feature/b, hotfix release/2 C feature/a, feature/b, feature/f, feature/g, hotfix, master, release/3 release/3 C feature/a,功能/b、功能/f、功能/g、修补程序、主、发布/2 发布/4 L 功能/c、功能/d、功能/e、支持/1 支持/1 L 功能/c、功能/d、功能/e,发布/4
批处理分支
[在这个阶段呈现,因为它最适合此时的最终脚本。此部分不是必需的,请随意向前跳过。]
git show-branch
将自身限制为 29 个分支。这对某些人来说可能是一个障碍(没有判断力,只是说!)。
在某些情况下,我们可以通过将分支分组来改进结果。
BASE_BRANCH 必须与每个分支一起提交。
如果回购中有大量分支,则其本身的价值可能有限。
如果您找到其他方法来限制分支(将被批处理),则可能会提供更多价值。
前一点适合我的用例,所以请提前充电!
这种机制并不完美,因为结果大小接近最大值(29),预计它会失败。详情如下
批量解决方案
#
# Remove/comment-out the function call at the end of script,
# and append this to the end.
##
##
# Given:
# BASE_BRANCH : $1 : first param on every batch
# BRANCHES : [ $2 .. $N ] : list of unique branch names (no duplicates);
# perhaps possible parents
# Default is all branches except base branch.
#
# Output all BRANCHES that share that commit in their commit history.
#
function repeatBatchingUntilStableResults() {
local BASE_BRANCH="$1"
shift
local -a CANDIDATES
if [[ -z "${1+x}" ]]; then
CANDIDATES=( $(git rev-parse --symbolic --branches) )
else
CANDIDATES=("$@")
fi
local BRANCHES=( $(valuesExcept "$BASE_BRANCH" "${CANDIDATES[@]}") )
local SIZE=$GIT_SHOW_BRANCH_MAX
local COUNT=${#BRANCHES[@]}
local LAST_COUNT=$(( $COUNT + 1 ))
local NOT_DONE=1
while (( $NOT_DONE && $COUNT < $LAST_COUNT )); do
NOT_DONE=$(( $SIZE < $COUNT ))
LAST_COUNT=$COUNT
local -a BRANCHES_TO_BATCH=( "${BRANCHES[@]}" )
local -a AGGREGATE=()
while (( ${#BRANCHES_TO_BATCH[@]} > 0 )); do
local -a BATCH=( "${BRANCHES_TO_BATCH[@]:0:$SIZE}" )
AGGREGATE+=( $(nearestCommonBranches "$BASE_BRANCH" "${BATCH[@]}") )
BRANCHES_TO_BATCH=( "${BRANCHES_TO_BATCH[@]:$SIZE}" )
done
BRANCHES=( "${AGGREGATE[@]}" )
COUNT=${#BRANCHES[@]}
done
if (( ${#BRANCHES[@]} > $SIZE )); then
echo "Unable to reduce candidate branches below MAX for git-show-branch" >&2
echo " Base Branch : $BASE_BRANCH" >&2
echo " MAX Branches: $SIZE" >&2
echo " Candidates : ${BRANCHES[@]}" >&2
exit 1
fi
echo "${BRANCHES[@]}"
}
repeatBatchingUntilStableResults "$@"
exit 0
这个怎么运作
重复直到结果稳定
将 BRANCHES 分解为 GIT_SHOW_BRANCH_MAX(又名 SIZE)元素的批次调用最近的CommonBranches BASE_BRANCH BATCH 将结果聚合到新的(更小?)的一组分支中
它怎么会失败
如果聚合分支的数量超过最大 SIZE
并且进一步的批处理/处理无法减少该数量,那么:
聚合的分支是解决方案,但无法通过 git show-branch 验证,或者
每批不减;可能一个批次的分支将有助于减少另一个(差异合并基础);当前的算法承认失败并失败。
考虑替代方案
将一个基础分支与每个其他感兴趣的分支单独配对,为每对确定一个提交节点(合并基础);按提交历史顺序对合并基集进行排序,取最近的节点,确定与该节点关联的所有分支。
我是从后见之明的立场提出的。这可能真的是正确的方法。我在前进;也许在当前主题之外还有价值。
一个有偏见的问题
您可能已经注意到,前面脚本中的核心函数 nearestCommonBranches
回答的问题比 Q1 提出的问题要多。事实上,该函数回答了一个更普遍的问题:
第二季度
给定一个分支 B 和一个分支的有序集(无重复) P(B 不在 P 中):考虑最接近 B'HEAD 的提交 C(C 可能是 B'HEAD),它由 P 中的分支共享:按顺序P 的顺序,P 中的哪些分支在其提交历史中具有 C?
选择 P
会提供偏见,或描述(有限的)约定。要匹配您的偏见/惯例的所有特征,可能需要额外的工具,这超出了本讨论的范围。
建模简单的偏差/约定
偏见因不同的组织和实践而异,以下可能不适合您的组织。如果不出意外,也许这里的一些想法可能会帮助您找到满足您需求的解决方案。
有偏见的解决方案;分支命名约定的偏差
也许偏差可以映射到使用的命名约定中并从中提取。
P 的偏差(那些其他分支名称)
下一步我们将需要它,所以让我们看看通过正则表达式过滤分支名称可以做什么。
合并的旧代码和下面的新代码可用作 gist: gitr
#
# Remove/comment-out the function call at the end of script,
# and append this to the end.
##
##
# Given Params:
# BASE_BRANCH : $1 : base branch
# REGEXs : $2 [ .. $N ] : regex(s)
#
# Output:
# - git branches matching at least one of the regex params
# - base branch is excluded from result
# - order: branches matching the Nth regex will appear before
# branches matching the (N+1)th regex.
# - no duplicates in output
#
function expandUniqGitBranches() {
local -A BSET[$1]=1
shift
local ALL_BRANCHES=$(git rev-parse --symbolic --branches)
for regex in "$@"; do
for branch in $ALL_BRANCHES; do
## RE: -z ${BSET[$branch]+x ... ; presumes ENV 'x' is not defined
if [[ $branch =~ $regex && -z "${BSET[$branch]+x}" ]]; then
echo "$branch"
BSET[$branch]=1
fi
done
done
}
##
# Params:
# BASE_BRANCH: $1 : "." equates to the current branch;
# REGEXS : $2..N : regex(es) corresponding to other to include
#
function findBranchesSharingFirstCommonCommit() {
if [[ -z "$1" ]]; then
echo "Usage: findBranchesSharingFirstCommonCommit ( . | baseBranch ) [ regex [ ... ] ]" >&2
exit 1
fi
local BASE_BRANCH
if [[ -z "${1+x}" || "$1" == '.' ]]; then
BASE_BRANCH="$CURRENT_BRANCH"
else
BASE_BRANCH="$1"
fi
shift
local REGEXS
if [[ -z "$1" ]]; then
REGEXS=(".*")
else
REGEXS=("$@")
fi
local BRANCHES=( $(expandUniqGitBranches "$BASE_BRANCH" "${REGEXS[@]}") )
## nearestCommonBranches can also be used here, if batching not used.
repeatBatchingUntilStableResults "$BASE_BRANCH" "${BRANCHES[@]}"
}
findBranchesSharingFirstCommonCommit "$@"
示例图的偏差结果
让我们考虑有序集
= { ^release/.*$ ^support/.*$ ^master$ }
假设脚本(所有部分)在可执行文件 gitr
中,然后运行:
gitr <baseBranch> '^release/.*$' '^support/.*$' '^master$'
对于不同的分支B
,我们得到以下结果:
GIVEN B Shared Commit C 分支 P 和 C 在他们的历史中(按顺序) feature/a D master feature/b D master feature/c L release/4, support/1 feature/d L release/4, support/1 feature/ e L release/4, support/1 feature/f C release/2, release/3, master feature/g C release/2, release/3, master hotfix D master master C release/2, release/3 release/2 C 版本/3,主版本/3 C 版本/2,主版本/4 L 支持/1 支持/1 L 版本/4
这越来越接近一个明确的答案。发布分支的响应并不理想。让我们更进一步。
BASE_NAME 和 P 的偏差
采取此方法的一个方向可能是对不同的基本名称使用不同的 P
。让我们为此设计一个设计。
约定
免责声明:我不是一个 git flow 纯粹主义者,请原谅我
支持分支应从主分支分支。不会有两个支持分支共享一个共同的提交。
不会有两个支持分支共享一个共同的提交。
修补程序分支应从支持分支或主分支分支出来。
发布分支应从支持分支或主分支分支出来。可能有多个发布分支共享一个共同的提交;即同时分支出master。
可能有多个发布分支共享一个共同的提交;即同时分支出master。
错误修复分支应从发布分支分支出来。
一个特性分支可以分支一个特性、发布、支持或主分支:出于“父”的目的,一个特性分支不能被建立为另一个特性分支的父级(参见最初的讨论)。因此:跳过功能分支并在发布、支持和/或主分支中寻找“父”。
出于“父”的目的,不能将一个功能分支建立为另一个功能分支的父级(请参阅初始讨论)。
因此:跳过功能分支并在发布、支持和/或主分支中寻找“父”。
任何其他被视为工作分支的分支名称,与功能分支具有相同的约定。
让我们看看我们git
有多远:
基础分支模式 父分支,有序注释 ^master$ n/a no parent ^support/.*$ ^master$ ^hotfix/.*$ ^support/.*$ ^master$ 优先支持支持分支master (ordering) ^release/.*$ ^support/.*$ ^master$ 优先支持支持分支而不是 master (ordering) ^bugfix/.*$ ^release/.*$ ^feature/.*$ ^release /.*$ ^support/.*$ ^master$ ^.*$ ^release/.*$ ^support/.*$ ^master$ 冗余,但将设计问题分开
脚本
合并的旧代码和下面的新代码可用作 gist: gitp
#
# Remove/comment-out the function call at the end of script,
# and append this to the end.
##
# bash associative arrays maintain key/entry order.
# So, use two maps, values correlated by index:
declare -a MAP_BASE_BRANCH_REGEX=( "^master$" \
"^support/.*$" \
"^hotfix/.*$" \
"^release/.*$" \
"^bugfix/.*$" \
"^feature/.*$" \
"^.*$" )
declare -a MAP_BRANCHES_REGEXS=("" \
"^master$" \
"^support/.*$ ^master$" \
"^support/.*$ ^master$" \
"^release/.*$" \
"^release/.*$ ^support/.*$ ^master$" \
"^release/.*$ ^support/.*$ ^master$" )
function findBranchesByBaseBranch() {
local BASE_BRANCH
if [[ -z "${1+x}" || "$1" == '.' ]]; then
BASE_BRANCH="$CURRENT_BRANCH"
else
BASE_BRANCH="$1"
fi
for idx in "${!MAP_BASE_BRANCH_REGEX[@]}"; do
local BASE_BRANCH_REGEX=${MAP_BASE_BRANCH_REGEX[$idx]}
if [[ "$BASE_BRANCH" =~ $BASE_BRANCH_REGEX ]]; then
local BRANCHES_REGEXS=( ${MAP_BRANCHES_REGEXS[$idx]} )
if (( ${#BRANCHES_REGEXS[@]} > 0 )); then
findBranchesSharingFirstCommonCommit $BASE_BRANCH "${BRANCHES_REGEXS[@]}"
fi
break
fi
done
}
findBranchesByBaseBranch "$1"
示例图的偏差结果
假设脚本(所有部分)在可执行文件 gitr
中,然后运行:
gitr <baseBranch>
对于不同的分支B
,我们得到以下结果:
GIVEN B Shared Commit C 分支 P 和 C 在他们的历史中(按顺序) feature/a D master feature/b D master feature/c L release/4, support/1 feature/d L release/4, support/1 feature/ e L release/4, support/1 feature/f C release/2, release/3, master feature/g C release/2, release/3, master hotfix D master master (空白,无值) release/2 C master发布/3 C 主版本/4 L 支持/1 支持/1 L 主
为胜利重构!
机会!
在最后一个示例中,发布分支与多个其他分支共享一个公共提交:发布、支持或主分支。
让我们“重构”或重新评估使用的约定,并稍微收紧它们。
请考虑以下 git
使用约定:
创建新的发布分支时:立即创建新的提交;也许更新一个版本,或者 README 文件。这确保了发布的功能/工作分支(从发布分支)将在底层支持或主分支的提交之前(而不是由发布分支共享)与发布分支共享提交。
例如:
G---H <- feature/z
/
E <- release/1
/
A---B---C---D <- master
\
F <- release/2
从 release/1 分支的功能不能有一个包含 release/1(它的父级)和 master 或 release/2 的公共提交。
这为每个分支提供了一个结果,即父级,具有这些约定。
完毕!通过工具和约定,我可以生活在一个 OCD 友好的结构化 git 世界中。
你的旅费可能会改变!
离别的思念
要点
gitr:查找(可能多个)相关分支
gitp:通过类似 git-flow 的内部规则/正则表达式找到可能的父母
最重要的是:我得出的结论是,除了这里介绍的内容之外,在某些时候可能需要接受可能有多个分支需要处理。也许可以对所有潜在的分支进行验证; “至少一个”或“全部”还是??可能会应用规则。像这样的几周,我真的认为是时候学习 Python 了。
由于之前的答案都不适用于我们的存储库,因此我想分享我自己的方式,使用 git log
中的最新合并:
#!/bin/bash
git log --oneline --merges "$@" | grep into | sed 's/.* into //g' | uniq --count | head -n 10
将其放入名为 git-last-merges
的脚本中,该脚本还接受分支名称作为参数(而不是当前分支)以及其他 git log
参数。
从输出中,我们可以根据自己的分支约定和每个分支的合并次数手动检测父分支。
如果您经常在子分支上使用 git rebase
(并且合并经常快进,因此没有太多合并提交),这个答案不会很好,所以我编写了一个脚本来计算提前提交(正常和合并),以及与当前分支相比,所有分支上的提交后面(父分支中不应有任何后面的合并)。
#!/bin/bash
HEAD="`git rev-parse --abbrev-ref HEAD`"
echo "Comparing to $HEAD"
printf "%12s %12s %10s %s\n" "Behind" "BehindMerge" "Ahead" "Branch"
git branch | grep -v '^*' | sed 's/^\* //g' | while read branch ; do
ahead_merge_count=`git log --oneline --merges $branch ^$HEAD | wc -l`
if [[ $ahead_merge_count != 0 ]] ; then
continue
fi
ahead_count=`git log --oneline --no-merges $branch ^$HEAD | wc -l`
behind_count=`git log --oneline --no-merges ^$branch $HEAD | wc -l`
behind_merge_count=`git log --oneline --merges ^$branch $HEAD | wc -l`
behind="-$behind_count"
behind_merge="-M$behind_merge_count"
ahead="+$ahead_count"
printf "%12s %12s %10s %s\n" "$behind" "$behind_merge" "$ahead" "$branch"
done | sort -n
rebase
(并且经常fast-forward
进行合并),这可能效果不佳。如果我找到更好的解决方案,我会编辑我的答案。
git log --oneline --merges "$@" | grep into | sed 's/.* into //g' | uniq --count | head -n 1 | cut -d ' ' -f 8
请记住,如 "Git: Finding what branch a commit came from" 中所述,即使 git branch --contains <commit>
是一个开始,您也无法轻松确定提交的分支(可以重命名、移动、删除分支...)。
您可以从提交返回到提交,直到 git branch --contains
将该提交 SHA1 与 /refs/heads/develop 进行比较
如果两个提交 ID 匹配,您就可以开始了(这意味着 feature
分支的起源位于 develop
的 HEAD)。
我并不是说这是解决此问题的好方法,但这似乎对我有用:
git branch --contains $(cat .git/ORIG_HEAD)
问题在于对文件进行分类是在窥探 Git 的内部工作,因此这不一定是向前兼容的(或向后兼容的)。
git log -2 --pretty=format:'%d' --abbrev-commit | tail -n 1 | sed 's/\s(//g; s/,/\n/g';
(来源/父名,父名)
git log -2 --pretty=format:'%d' --abbrev-commit | tail -n 1 | sed 's/\s(//g; s/,/\n/g';
原产地/父名
git log -2 --pretty=format:'%d' --abbrev-commit | tail -n 1 | sed 's/(.*,//g; s/)//';
父母名字
Joe Chrysler's command-line magic 可以简化。这是 Joe 的逻辑 - 为简洁起见,我在两个版本中都引入了一个名为 cur_branch
的参数来代替命令替换 `git rev-parse --abbrev-ref HEAD`
;可以像这样初始化:
cur_branch=$(git rev-parse --abbrev-ref HEAD)
然后,这是乔的管道:
git show-branch -a |
grep '\*' | # we want only lines that contain an asterisk
grep -v "$cur_branch" | # but also don't contain the current branch
head -n1 | # and only the first such line
sed 's/.*\[\(.*\)\].*/\1/' | # really, just the part of the line between []
sed 's/[\^~].*//' # and with any relative refs (^, ~n) removed
我们可以在一个相对简单的 awk
命令中完成与所有五个单独的命令过滤器相同的事情:
git show-branch -a |
awk -F'[]^~[]' '/\*/ && !/'"$cur_branch"'/ {print $2;exit}'
分解如下:
-F'[]^~[]'
将行拆分为 ]
、^
、~
和 [
个字符的字段。
/\*/
查找包含星号的行
&& !/'"$cur_branch"'/
...但不是当前分支名称
{ print $2;
当你找到这样的一行时,打印它的第二个字段(即我们的字段分隔符的第一次和第二次出现之间的部分)。对于简单的分支名称,这就是括号之间的内容;对于具有相对跳转的引用,它只是没有修饰符的名称。因此,我们的字段分隔符集处理两个 sed
命令的意图。
exit }
然后立即退出。这意味着它只处理第一个匹配行,因此我们不需要通过 head -n 1
管道输出。
cannot handle more than 26 refs
。
这是 Mark Reed's solution 的 PowerShell 实现:
git show-branch -a | where-object { $_.Contains('*') -eq $true} | Where-object {$_.Contains($branchName) -ne $true } | select -first 1 | % {$_ -replace('.*\[(.*)\].*','$1')} | % { $_ -replace('[\^~].*','') }
利用:
vbc=$(git rev-parse --abbrev-ref HEAD)
vbc_col=$(( $(git show-branch | grep '^[^\[]*\*' | head -1 | cut -d* -f1 | wc -c) - 1 ))
swimming_lane_start_row=$(( $(git show-branch | grep -n "^[\-]*$" | cut -d: -f1) + 1 ))
git show-branch | tail -n +$swimming_lane_start_row | grep -v "^[^\[]*\[$vbc" | grep "^.\{$vbc_col\}[^ ]" | head -n1 | sed 's/.*\[\(.*\)\].*/\1/' | sed 's/[\^~].*//'
它实现了与 Mark Reed's answer 相同的目的,但它使用了一种更安全的方法,在许多情况下都不会出现错误行为:
父分支的最后一次提交是合并,使列显示-,而不是 * 提交消息包含分支名称 提交消息包含 *
这是我的 PowerShell 版本:
function Get-GHAParentBranch {
[CmdletBinding()]
param(
$Name = (git branch --show-current)
)
git show-branch |
Select-String '^[^\[]*\*' |
Select-String -NotMatch -Pattern "\[$([Regex]::Escape($Name)).*?\]" |
Select-Object -First 1 |
Foreach-Object {$PSItem -replace '^.+?\[(.+)\].+$','$1'}
}
Mark Reed's solution 基本正确。但是,请注意,提交行不仅应包含星号,还应以星号开头!否则,包含星号的提交消息也包含在匹配的行中。所以应该是:
git show-branch -a | awk -F'[]^~[]' '/^\*/ && !/'"$current_branch"'/ {print $2;exit}'
或长版本:
git show-branch -a |
awk '^\*' | # we want only lines that contain an asterisk
awk -v "$current_branch" | # but also don't contain the current branch
head -n1 | # and only the first such line
sed 's/.*\[\(.*\)\].*/\1/' | # really, just the part of the line between []
sed 's/[\^~].*//' # and with any relative refs (^, ~n) removed`
使用 Ant 实现跨平台
<exec executable="git" outputproperty="currentBranch">
<arg value="rev-parse" />
<arg value="--abbrev-ref" />
<arg value="HEAD" />
</exec>
<exec executable="git" outputproperty="showBranchOutput">
<arg value="show-branch" />
<arg value="-a" />
</exec>
<loadresource property="baseBranch">
<propertyresource name="showBranchOutput"/>
<filterchain>
<linecontains>
<contains value="*"/>
</linecontains>
<linecontains negate="true">
<contains value="${currentBranch}"/>
</linecontains>
<headfilter lines="1"/>
<tokenfilter>
<replaceregex pattern=".*\[(.*)\].*" replace="\1"/>
<replaceregex pattern="[\^~].*" replace=""/>
</tokenfilter>
</filterchain>
</loadresource>
<echo message="${currentBranch} ${baseBranch}" />
我不喜欢解析一些复杂文本输出的解决方案中涉及的许多不安全假设,因此我想要一个更强大的解决方案,对半结构化数据的假设更少:
# Search backwards in history for the first commit that is in a branch other than $1
# and output that branch's name.
parent_branch() {
local result rev child_branch=$1
rev=$(git rev-parse --revs-only $child_branch)
while [[ -n $rev ]]; do
result=$(git branch --contains $rev | grep -v " $child_branch$")
if [[ -n $result ]]; then
echo $result
return 0
fi
rev=$(git rev-parse --revs-only $rev^)
done
return 1
}
警告:由于这在历史上迭代地倒退,查看每个提交以找到与 $1
不同的分支中的第一个提交,因此分支越长,它的成本就会越高。但是由于通常分支无论如何都应该是相对短暂的,所以这应该不是太大的问题。
另请注意,我使用的是 git branch --contains
,因此这也会找到共享共同基础但已经超越它的分支。要仅查找精确指向公共基础的分支,请使用 git branch --points-at
。
Git 带有几个 GUI 客户端,可以帮助您将其可视化。打开 GitGUI 并转到菜单 Repository → Visualize All Branch History。
基于 git show-branch -a
加上一些过滤器的解决方案有一个缺点:Git 可能会考虑短期分支的分支名称。
如果您有几个您关心的可能父母,您可以问自己这个类似的问题(可能是 OP 想知道的问题):
从所有分支的特定子集中,哪个是 git 分支的最近父级?
为简化起见,我将考虑“一个 git 分支”来指代 HEAD
(即当前分支)。
假设我们有以下分支:
HEAD
important/a
important/b
spam/a
spam/b
基于 git show-branch -a
+ 过滤器的解决方案可能会给出 HEAD
的最近父级是 spam/a
,但我们并不关心这一点。
如果我们想知道 important/a
和 important/b
中的哪一个是 HEAD
的最近父级,我们可以运行以下命令:
for b in $(git branch -a -l "important/*" | sed -E -e "s/\*//"); do
d1=$(git rev-list --first-parent ^${b} HEAD | wc -l);
d2=$(git rev-list --first-parent ^HEAD ${b} | wc -l);
echo "${b} ${d1} ${d2}";
done \
| sort -n -k2 -k3 \
| head -n1 \
| awk '{print $1}';
它能做什么:
1.) $(git branch -a -l "important/*" | sed -E -e "s/\*//")
:打印具有某种模式的所有分支的列表 ("important/*"
)。 (如果您碰巧在 important/*
分支之一上,git branch
将包含一个 * 来指示您当前的分支。命令替换 $()
然后会将其展开到您当前目录的内容中。sed
从 git branch
的输出中删除 *
。)
2.) d=$(git rev-list --first-parent ^${b} HEAD | wc -l);
:对于每个分支 ($b
),计算从 HEAD
到 $b
中最近提交的距离 ($d1
)(类似于计算点到线的距离)。您可能想在这里以不同的方式考虑距离:您可能不想使用 --first-parent
,或者可能想要从尖端到分支尖端的距离 ("${b}"...HEAD
),...
2.2) d2=$(git rev-list --first-parent ^HEAD ${b} | wc -l);
:对于每个分支 ($b
),计算从分支尖端到 HEAD
中最近提交的距离 ($d2
)。我们将使用此距离在距离 $d1
相等的两个分支之间进行选择。
3.) echo "${b} ${d1} ${d2}";
:打印每个分支的名称,然后是距离以便以后能够对它们进行排序(首先是 $d1
,然后是 $d2
)。
4.) | sort -n -k2 -k3
:对先前的结果进行排序,因此我们得到所有分支的排序(按距离)列表,然后是它们的距离(两者)。
5.) | head -n1
:上一步的第一个结果将是距离较小的分支,即最近的父分支。所以只需丢弃所有其他分支。
6.) | awk '{print $1}';
:我们只关心分支名称,而不关心距离,因此提取第一个字段,即父项的名称。这里是! :)
在多个分支中寻找第一次提交的 shell 函数:
# Get the first commit hash of a given branch.
# Uses `git branch --contains` to backward (starts from HEAD) check each commits
# and output that branch's name.
first_commit_of_branch() {
if [ $# -ne 1 ] || [ -z "${1}" ] ; then
(>&2 echo "Error: Missing or empty branch name.")
(>&2 echo "Usage: $0 branch_to_test")
return 2
fi
local branch_to_test="${1}"; shift
local commit_index_to_test
local maximum_number_of_commit_to_test
local branch_count_having_tested_commit
git rev-parse --verify --quiet "${branch_to_test}" 2>&1 > /dev/null || {
(>&2 echo "Error: Branch \"${branch_to_test}\" does not exists.")
return 2
}
commit_index_to_test=0
maximum_number_of_commit_to_test=$(git rev-list --count "${branch_to_test}")
while [ ${commit_index_to_test} -le ${maximum_number_of_commit_to_test} ] ; do
# Testing commit $branch_to_test~$commit_index_to_test…
# If it fails, it means we tested all commits from the most recent of
# branch $branch_to_test to the very first of the git DAG. So it must be it.
git rev-parse --verify --quiet ${branch_to_test}~${commit_index_to_test} 2>&1 > /dev/null || {
git rev-list --max-parents=0 "${branch_to_test}"
return 0
}
branch_count_having_tested_commit="$( \
git --no-pager branch --no-abbrev --verbose \
--contains ${branch_to_test}~${commit_index_to_test} \
| cut -c 3- \
| cut -d ' ' -f 2 \
| wc -l \
)"
# Tested commit found in more than one branch
if [ ${branch_count_having_tested_commit} -gt 1 ] ; then
if [ ${commit_index_to_test} -eq 0 ]; then
(>&2 echo "Error: The most recent commit of branch \"${branch_to_test}\" (${branch_to_test}~${commit_index_to_test}) is already in more than one branch. This is likely a new branch without any commit (yet). Cannot continue.")
return 1
else
# Commit $branch_to_test~$commit_index_to_test is in more than
# one branch, stopping there…
git rev-parse ${branch_to_test}~$((commit_index_to_test-1))
return 0
fi
fi
# else: Commit $branch_to_test~$commit_index_to_test is still only in
# branch ${branch_to_test} continuing…"
commit_index_to_test=$((commit_index_to_test+1))
done
}
警告:在具有子分支且此后没有新提交的分支上执行时失败。
A---B---C---D <- "main" branch
\ \
\ E---F <- "work1" branch
\ \
\ G---H <- "work1-b" branch
\
I---J <- "work2" branch
first_commit_of_branch main # C
first_commit_of_branch work1 # (Fails)
first_commit_of_branch work1-b # G
first_commit_of_branch work2 # I
这些天任何人都想这样做 - Atlassian 的 Sourcetree 应用程序向您展示了您的分支如何相互关联的出色可视化表示,即它们开始的位置以及它们当前在提交顺序中所处的位置(例如,HEAD 或 4 次提交, ETC。)。
如果您使用 Sourcetree,请查看您的提交详细信息 → 父母。然后你会看到下划线的提交号(链接)。
当我做了类似开发→发布-v1.0.0→功能-foo之类的事情时,这对我不起作用。它会一路回去发展。请注意,涉及到一个变基,我不确定这是否使我的问题更加复杂......
以下确实为我提供了正确的提交哈希:
git log --decorate \
| grep 'commit' \
| grep 'origin/' \
| head -n 2 \
| tail -n 1 \
| awk '{ print $2 }' \
| tr -d "\n"
<branch>.json
文件,我们使这变得容易多了。增加了一些混乱,但它给了我们极大的灵活性来定义额外的变量来存储信息和控制 CI 中的过程。我们制作了一个脚本来创建一个分支,创建这个文件,然后提交和推送,同时设置源。
替代:
git rev-list master | grep "$(git rev-list HEAD)" | head -1
获取我的分支和 master
(或您要指定的任何分支)的最后一次提交。
bash: /usr/bin/grep: Argument list too long
不定期副业成功案例分享