我很难弄清楚如何使用git blame
来获得曾经触及给定范围的行的提交集。还有类似的问题,但公认的答案并没有让我走得更远。
假设我有一个定义,从foo.rb
的第1000行开始。它只有5行长,但是修改这些行的提交次数是巨大的。如果我做
git blame foo.rb -L 1000,+5
我得到了(最多)五个不同的提交的引用,这些提交改变了这些行,但我也对"它们背后"的提交感兴趣。
相似的,
git rev-list HEAD -- foo.rb | xargs git log --oneline
几乎是我想要的,但我不能指定行范围git rev-list
我可以传递一个标志到git blame
,以获得曾经触及这五行的提交列表,或者构建提取这些信息的脚本的最快方法是什么?让我们暂时忽略定义曾经多于或少于5行的可能性。
从Git 1.8.4开始,git log
有-L
来查看一系列行的演变。
例如,假设您查看git blame
的输出:
((aa27064...))[mlm@macbook:~/w/mlm/git]
$ git blame -L150,+11 -- git-web--browse.sh
a180055a git-web--browse.sh (Giuseppe Bilotta 2010-12-03 17:47:36 +0100 150) die "The browser $browser is not
a180055a git-web--browse.sh (Giuseppe Bilotta 2010-12-03 17:47:36 +0100 151) fi
5d6491c7 git-browse-help.sh (Christian Couder 2007-12-02 06:07:55 +0100 152) fi
5d6491c7 git-browse-help.sh (Christian Couder 2007-12-02 06:07:55 +0100 153)
5d6491c7 git-browse-help.sh (Christian Couder 2007-12-02 06:07:55 +0100 154) case "$browser" in
81f42f11 git-web--browse.sh (Giuseppe Bilotta 2010-12-03 17:47:38 +0100 155) firefox|iceweasel|seamonkey|iceape)
5d6491c7 git-browse-help.sh (Christian Couder 2007-12-02 06:07:55 +0100 156) # Check version because firefox < 2.0 do
5d6491c7 git-browse-help.sh (Christian Couder 2007-12-02 06:07:55 +0100 157) vers=$(expr "$($browser_path -version)"
5d6491c7 git-browse-help.sh (Christian Couder 2007-12-02 06:07:55 +0100 158) NEWTAB='-new-tab'
5d6491c7 git-browse-help.sh (Christian Couder 2007-12-02 06:07:55 +0100 159) test "$vers" -lt 2 && NEWTAB=''
a0685a4f git-web--browse.sh (Dmitry Potapov 2008-02-09 23:22:22 -0800 160) "$browser_path" $NEWTAB "$@" &
你想知道现在155行的历史。
:
((aa27064...))[mlm@macbook:~/w/mlm/git]
$ git log --topo-order --graph -u -L 155,155:git-web--browse.sh
* commit 81f42f11496b9117273939c98d270af273c8a463
| Author: Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
| Date: Fri Dec 3 17:47:38 2010 +0100
|
| web--browse: support opera, seamonkey and elinks
|
| The list of supported browsers is also updated in the documentation.
|
| Signed-off-by: Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
| Signed-off-by: Junio C Hamano <gitster@pobox.com>
|
| diff --git a/git-web--browse.sh b/git-web--browse.sh
| --- a/git-web--browse.sh
| +++ b/git-web--browse.sh
| @@ -143,1 +143,1 @@
| -firefox|iceweasel)
| +firefox|iceweasel|seamonkey|iceape)
|
* commit a180055a47c6793eaaba6289f623cff32644215b
| Author: Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
| Date: Fri Dec 3 17:47:36 2010 +0100
|
| web--browse: coding style
|
| Retab and deindent choices in case statements.
|
| Signed-off-by: Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
| Signed-off-by: Junio C Hamano <gitster@pobox.com>
|
| diff --git a/git-web--browse.sh b/git-web--browse.sh
| --- a/git-web--browse.sh
| +++ b/git-web--browse.sh
| @@ -142,1 +142,1 @@
| - firefox|iceweasel)
| +firefox|iceweasel)
|
* commit 5884f1fe96b33d9666a78e660042b1e3e5f9f4d9
Author: Christian Couder <chriscool@tuxfamily.org>
Date: Sat Feb 2 07:32:53 2008 +0100
Rename 'git-help--browse.sh' to 'git-web--browse.sh'.
Signed-off-by: Christian Couder <chriscool@tuxfamily.org>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
diff --git a/git-web--browse.sh b/git-web--browse.sh
--- /dev/null
+++ b/git-web--browse.sh
@@ -0,0 +127,1 @@
+ firefox|iceweasel)
如果你经常使用这个功能,你可能会发现git别名很有用。要做到这一点,输入~/.gitconfig
:
[alias]
# Follow evolution of certain lines in a file
# arg1=file, arg2=first line, arg3=last line or blank for just the first line
follow = "!sh -c 'git log --topo-order -u -L $2,${3:-$2}:"$1"'" -
现在你可以直接输入git follow git-web--browse.sh 155
我想这就是你想要的:
git rev-list HEAD -- foo.rb | (
while read rev; do
git blame -l -L 1000,+5 $rev -- foo.rb | cut -d ' ' -f 1
done;
) | awk '{ if (!h[$0]) { print $0; h[$0]=1 } }'
这将为您所选择的行输出具有编辑的每个提交的修订号。
步骤如下:
第一部分
git rev-list HEAD -- foo.rb
输出所选文件被编辑的所有修订。每个这些修订然后进入第二部分,其中每个修订并将其放入
git blame -l -L 1000,+5 $rev -- foo.rb | cut -d ' ' -f 1
。这是由两部分组成的命令。-
git blame -l -L 1000,+5 $rev -- foo.rb
输出所选线路的故障。通过提供修订号,我们告诉它从那个提交开始,从那里开始,而不是从头部开始。 - 由于责备输出了一堆我们不需要的信息,
cut -d ' ' -f 1
给出了责备输出的第一列(修订号)。
-
-
awk '{ if (!h[$0]) { print $0; h[$0]=1 } }'
删除不相邻的重复行,同时保持它们出现的顺序。有关此命令的更多信息,请参见http://jeetworks.org/node/94。
您可以在这里添加最后一步以获得更漂亮的输出。将所有内容管道到xargs -L 1 git log --oneline -1
中,并获得相应的修订列表的提交消息。我在使用最后一个步骤时遇到了一个奇怪的问题,我必须每隔几个版本就按下一个。我不知道为什么会这样,所以我没有把它包含在我的解中。
不知道你想做什么,但也许git log -S可以为你做的技巧:
-S<string> Look for differences that introduce or remove an instance of <string>. Note that this is different than the string simply appearing in diff output; see the pickaxe entry in gitdiffcore(7) for more details.
你可以把你要做的修改(或部分修改)放在字符串中,这样就会列出所有涉及到这个修改的提交。
我喜欢这个谜题,它有它的微妙之处。源此文件,说init foo.rb 1000,1005
,并按照说明。完成后,文件@changes
将按照拓扑顺序拥有正确的提交列表,而@blames
将拥有每个提交的实际错误输出。
这比上面公认的解决方案要复杂得多。它产生的输出有时会更有用,而且很难重现,编写代码很有趣。
尝试在回溯历史时自动跟踪行数范围的问题是,如果更改块跨越行数范围边界,您无法自动确定新范围边界应该在该块中的位置,您要么必须包含大范围的大添加,因此积累(有时是很多)不相关的更改,或者进入手动模式以确保它是正确的(这当然会让您回到这里)。或者有时接受极端的损失。
如果你希望你的输出是精确的,使用上面的答案和可靠的正则表达式范围,比如'/^type function(/,/^}/',或者使用这个,它实际上并没有那么糟糕,每一步倒退几秒。
作为额外复杂性的交换,它确实按拓扑顺序生成命中列表,并且至少(相当成功地)尝试改善每一步的痛苦。例如,它从不运行冗余的责备,并且update-range使调整行号更容易。当然,必须单独观察这些大块头也很可靠……: - p
要自动运行,输入{ init foo.rb /^class foo/,/^end/; auto; } 2>&-
### functions here create random @-prefix files in the current directory ###
#
# git blame history for a range, finding every change to that range
# throughout the available history. It's somewhat, ahh, "intended for
# customization", is that enough of a warning? It works as advertised
# but drops @-prefix temporary files in your current directory and
# defines new commands
#
# Source this file in a subshell, it defines functions for your use.
# If you have @-prefix files you care about, change all @ in this file
# to something you don't have and source it again.
#
# init path/to/file [<start>,<end>] # range optional
# update-ranges # check range boundaries for the next step
# cycle [<start>,<end>] # range unchanged if not supplied
# prettyblame # pretty colors,
# blue="child commit doesn't have this line"
# green="parent commit doesn't have this line"
# brown=both
# shhh # silence the pre-cycle blurb
#
# For regex ranges, you can _usually_ source this file and say `init
# path/to/file /startpattern/,/endpattern/` and then cycle until it says 0
# commits remain in the checklist
#
# for line-number ranges, or regex ranges you think might be unworthy, you
# need to check and possibly update the range before each cycle. File
# @next is the next blame start-point revision text; and command
# update-ranges will bring up vim with the current range V-selected. If
# that looks good, `@M` is set up to quit even while selecting, so `@M` and
# cycle. If it doesn't look good, 'o' and the arrow keys will make getting
# good line numbers easy, or you can find better regex's. Either way, `@M`
# out and say `cycle <start>,<end>` to update the ranges.
init () {
file=$1;
range="$2"
rm -f @changes
git rev-list --topo-order HEAD -- "$file"
| tee @checklist
| cat -n | sort -k2 > @sequence
git blame "-ln${range:+L$range}" -- "$file" > @latest || echo >@checklist
check-cycle
cp @latest @blames
}
update-latest-checklist() {
# update $latest with the latest sha that actually touched our range,
# and delete that and everything later than that from the checklist.
latest=$(
sed s,^^,, @latest
| sort -uk1,1
| join -1 2 -o1.1,1.2 @sequence -
| sort -unk1,1
| sed 1q
| cut -d" " -f2
)
sed -i 1,/^$latest/d @checklist
}
shhh () { shhh=1; }
check-cycle () {
update-latest-checklist
sed -n q1 @checklist || git log $latest~..$latest --format=%H %s | tee -a @changes
next=`sed 1q @checklist`
git cat-file -p `git rev-parse $next:"$file"` > @next
test -z "$shh$shhh$shhhh" && {
echo "A blame from the (next-)most recent alteration (id `git rev-parse --short $latest`) to '$file'"
echo is in file @latest, save its contents where you like
echo
echo you will need to look in file @next to determine the correct next range,
echo and say '`cycle its-start-line,its-end-line`' to continue
echo the "update-ranges" function starts you out with the range selected
} >&2
ncommits=`wc -l @checklist | cut -d -f1`
echo $ncommits commits remain in the checklist >&2
return $((ncommits==0))
}
update-ranges () {
start="${range%,*}"
end="${range#*,}"
case "$start" in
*/*) startcmd="1G$start"$'n' ;;
*) startcmd="${start}G" ;;
esac
case "$end" in
*/*) endcmd="$end"$'n' ;;
[0-9]*) endcmd="${end}G" ;;
+[0-9]*) endcmd="${end}j" ;;
*) endcmd="echohl Search|echo "can't" get to '${end}'"|echohl None" ;;
esac
vim -c 'set buftype=nofile|let @m=":|q'$'n"' -c "norm!${startcmd}V${endcmd}z.o" @next
}
cycle () {
sed -n q1 @checklist && { echo "No more commits to check"; return 1; }
range="${1:-$range}"
git blame "-ln${range:+L$range}" $next -- "$file" >@latest || echo >@checklist
echo >>@blames
cat @latest >>@blames
check-cycle
}
auto () {
while cycle; do true; done
}
prettyblames () {
cat >@pretty <<-EOD
BEGIN {
RS=""
colors[0]=" 33[0;30m"
colors[1]=" 33[0;34m"
colors[2]=" 33[0;32m"
colors[3]=" 33[0;33m"
getline commits < "@changes"
split(commits,commit,/n/)
}
NR!=1 { print "" }
{
thiscommit=gensub(/ .*/,"",1,commit[NR])
printf "%sn"," 33[0;31m"commit[NR]" 33[0m"
split($0,line,/n/)
for ( n=1; n<=length(line); ++n ) {
color=0
split(line[n],key,/[1-9][0-9]*)/)
if ( NR!=1 && !seen[key[1]] ) color+=1
seen[key[1]]=1;
linecommit = gensub(/ .*/,"",1,line[n])
if (linecommit==thiscommit) color+=2
printf "%s%s 33[0mn",colors[color],line[n]
}
}
EOD
awk -f @pretty @blames | less -R
}
请参考此处发布的答案,列出特定文件的所有提交。这正是你需要的。
几点想法…
这听起来和这篇文章很相似,看起来你可能会接近这样的内容:
git blame -L '/variable_name *= */',+1
只要你知道要匹配的定义(对于正则表达式)。
这里有一个线程讨论,关于使用tig
和git gui
(显然可以处理这个)。我自己还没有尝试过,所以不能验证它(我稍后会给这个尝试)。