接下来会发生什么
要点:
为什么会有人想阅读这篇长文?
因为虽然以前的答案很清楚
理解原始问题的问题,
他们没有得到正确/有意义的结果;
或准确地解决一个不同的问题。
请随意查看第一部分;
它解决了“找东西”的问题,
应该突出问题的范围。
对于某些人来说,这可能就足够了。
这将向您展示一种方法
从 git 中提取正确且有意义的结果
(你可能不喜欢它们),
并展示一种申请方式
你对惯例的了解
对那些结果
提取您真正需要的内容。
封面以下部分:
-
一个公正的问题和解决方案:
- 最近的git分支使用
git show-branch。
- 预期结果应该是什么样子
- 示例图表和结果
-
批处理分支:解决
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 的父级是约定俗成的问题。
因此
- 简单的工具会产生忽略偏差的响应
- 更复杂的工具包含惯例(偏差)。
一个公正的问题
首先,我必须首先承认 Joe Chrysler 的回应、这里的其他回应以及周围的许多 cmets/建议;
他们启发了我并为我指明了方向!
请允许我重新表述 Joe 的表述,考虑到与最近提交相关的多个分支(它发生了!):
"驻留在除
当前分支,那是哪个分支?”
或者,换句话说:
第一季度
给定一个分支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
- 给定一个有序集(唯一)
BRANCHES,不包括BASE_BRANCH
- 为简洁起见,让
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 解析麻烦
以及用于调试/可读性。
- 将
*替换为+;基础分支总是在最后一列,
这就足够了。此外,如果不理会它会通过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]。
示例图表和结果
考虑以下有点做作的示例图:
- 将使用有偏见的名称,因为它们有助于(我)权衡和考虑结果。
- 假定合并存在并且被忽略。
- 该图通常会尝试突出显示分支(分叉),
没有在视觉上暗示偏好/层次结构;
讽刺的是,
master 在我做完这件事后脱颖而出。
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 with C in their history? |
| feature/a |
H |
feature/b |
| feature/b |
H |
feature/a |
| feature/c |
P |
feature/d |
| feature/d |
P |
feature/c |
| feature/e |
O |
feature/c, feature/d |
| feature/f |
C |
feature/a, feature/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, feature/b, feature/f, feature/g, hotfix, master, release/2 |
| release/4 |
L |
feature/c, feature/d, feature/e, support/1 |
| support/1 |
L |
feature/c, feature/d, feature/e, release/4 |
批处理分支
[现阶段呈现
因为此时它最适合最终脚本。
此部分不是必需的,请随意跳过。]
git show-branch 将自身限制为 29 个分支。
这对某些人来说可能是一个障碍(没有判断力,只是说!)。
在某些情况下,我们可以改善结果,
通过将分支分组为批次。
- BASE_BRANCH 必须与每个分支一起提交。
- 如果repo中有大量分支
这本身的价值可能有限。
- 如果您找到其他方法,可能会提供更多价值
限制分支(将被批处理)。
- 上一点适合我的用例,
冲锋陷阵!
这个机制并不完美,
随着结果大小接近最大值(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)元素
- 致电
nearestCommonBranches 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 "$@"
示例图的偏差结果
让我们考虑有序集
P = { ^release/.*$ ^support/.*$ ^master$ }
假设脚本(所有部分)在可执行文件gitr中,然后运行:
gitr <baseBranch> '^release/.*$' '^support/.*$' '^master$'
对于不同的分支B我们得到如下结果:
| GIVEN B |
Shared Commit C |
Branches P with C in their history (in order) |
| 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 |
release/3, master |
| release/3 |
C |
release/2, master |
| release/4 |
L |
support/1 |
| support/1 |
L |
release/4 |
这越来越接近一个明确的答案;发布分支的响应并不理想。让我们更进一步。
BASE_NAME 和 P 的偏差
采取这个的一个方向可能是使用不同的P
基本名称。让我们为此制定一个设计。
约定
免责声明:我不是 git flow 纯粹主义者,请原谅我
- 支持分支应从主分支分支。
- 修补程序分支应从支持分支或主分支分支出来。
- 发布分支应从支持分支或主分支分支出来。
- 可能有多个发布分支共享一个共同的提交;
即同时从master分支出来。
- 一个错误修复分支应该从一个发布分支分支出来。
- 功能分支可以分支功能、发布、支持或主控:
- 出于“父母”的目的,
一个特性分支不能被建立为
父母高于其他人(见初步讨论)。
- 因此:跳过功能分支和
在发布、支持和/或主分支中寻找“父”。
- 任何其他被视为工作分支的分支名称,
具有与功能分支相同的约定。
让我们看看我们git 能做到多远:
| Base Branch Pattern |
Parent Branches, Ordered |
Comment(s) |
| ^master$ |
n/a |
no parent |
| ^support/.*$ |
^master$ |
|
| ^hotfix/.*$ |
^support/.*$ ^master$ |
give preference to a support branch over master (ordering) |
| ^release/.*$ |
^support/.*$ ^master$ |
give preference to a support branch over master (ordering) |
| ^bugfix/.*$ |
^release/.*$ |
|
| ^feature/.*$ |
^release/.*$ ^support/.*$ ^master$ |
|
| ^.*$ |
^release/.*$ ^support/.*$ ^master$ |
Redundant, but keep design concerns separate |
脚本
合并以前的代码和下面的新代码可用作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 |
Branches P with C in their history (in order) |
| 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 |
|
(blank, no value) |
| release/2 |
C |
master |
| release/3 |
C |
master |
| release/4 |
L |
support/1 |
| support/1 |
L |
master |
为胜利重构!
机会!
在最后一个示例中,发布分支共享一个公共提交
与其他多个:发布、支持或主分支。
让我们“重构”或重新评估所使用的约定,并把它们收紧一点。
考虑一下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 世界中。
您的里程可能会有所不同!
离别的想法
- 要点
-
最重要的是:我得出的结论是,
除了这里介绍的,
在某些时候,人们可能需要接受可能有多个
分支确实处理。
- 也许可以对所有潜在的分支进行验证;
“至少一个”或“全部”还是??可能会应用规则。
-
像这样的几周,我真的认为该是我学习 Python 的时候了。