Git:查找曾经涉及到一系列代码行的提交记录

71
我在使用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行的提交列表,或者构建提取此类信息的脚本的最快方法是什么?暂时不考虑定义是否曾经有过多于或少于5行的情况。


1
你确定这就是你想要的吗?仅通过行号来识别变更只适用于文件的特定状态。如果你想要提交 12345 的第 15 至 20 行,那么这些行的代码在提交 12345^ 中可能会在第 55 至 60 行。 - asm
非常确定。这就是为什么我需要编写一个脚本来识别那个。为了简单起见,仍然假设定义从存储库中的初始提交开始从未移动过。 - joao
5
可能是 检索文件中特定行的提交日志? 的重复问题。 - Jesper Rønn-Jensen
我认为这是一个重复的问题,与https://dev59.com/9Goy5IYBdhLWcg3wnPSJ相同。 - Jesper Rønn-Jensen
6个回答

75

自 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


2
你的别名中最后一个 - 符号是什么意思? - Eugen Konkov

24
我认为这就是您想要的:
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 } }'

这会输出每个对你选择的行做出编辑的提交的修订号。

以下是步骤:

  1. 第一部分git rev-list HEAD -- foo.rb输出所选文件被编辑的所有修订版本。

  2. 第二部分将每个修订版本放入 git blame -l -L 1000,+5 $rev -- foo.rb | cut -d ' ' -f 1。 这是一个由两部分组成的命令。

    1. git blame -l -L 1000,+5 $rev -- foo.rb输出所选行的责任信息。通过提供修订号,我们告诉它从该提交开始并从那里开始,而不是从头开始。
    2. 由于blame输出了一堆我们不需要的信息,cut -d ' ' -f 1给出了blame输出的第一列(修订号)。
  3. awk '{ if (!h[$0]) { print $0; h[$0]=1 } }'删除非相邻重复行,同时保持它们出现的顺序。有关此命令的更多信息,请参见http://jeetworks.org/node/94

您可以在此处添加最后一步以获得更漂亮的输出。将所有内容传输到xargs -L 1 git log --oneline -1并获取修订号列表的相应提交消息。我在使用此最后一步时遇到了奇怪的问题,在输出几个修订版本后必须不断按下“下一个”。我不确定原因是什么,这就是为什么我没有将其包含在我的解决方案中的原因。


1
恭喜!非常好,简洁明了,接下来的步骤是自动计算更新的行范围。这是一个很好的开端。你有兴趣解决下一个谜题吗?我要不要开放另一个赏金? :-) - joao
这是一个非常有趣的问题!不幸的是,我这周工作非常忙碌,所以我没有机会去尝试解决它。不过我会一直记在心里,如果到下周还没有其他人解决它,我会回来尝试的。 - Jonathan Wren

12

不确定您想要做什么,但也许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.
您可以将更改(或更改的一部分)放入字符串中,这将列出曾经接触过此更改的提交记录。

抱歉,这并不是我想要的。但还是给你一个赞,谢谢你的尝试。 - joao
6
+1 是因为它可以帮助遇到类似问题的谷歌搜索者。 - wim

1

我喜欢这个谜题,它有它的微妙之处。从这个文件中获取源代码,输入init foo.rb 1000,1005并按照说明操作。完成后,文件@changes将按拓扑顺序列出正确的提交列表,@blames将显示每个提交的实际责任输出。

这比上面接受的解决方案要复杂得多。它产生的输出有时会更有用,难以复制,并且编码很有趣。

尝试在向后跟踪历史记录时自动跟踪行号范围的问题在于,如果更改块跨越了行号范围边界,您无法自动确定新范围边界在该块的哪个位置,您将不得不包含大范围的大添加,因此积累(有时是很多)不相关的更改,或者进入手动模式以确保它是正确的(当然会让你回到这里),或者在某些时候接受极端的损失。

如果您希望输出精确,请使用上面的答案和可信的正则表达式范围,例如`/^type function(/,/^}/'`,或者使用这个,它实际上并不那么糟糕,每次向后一步需要几秒钟。

虽然增加了一些复杂性,但它可以按照拓扑顺序生成命中列表,并且至少(相当成功地)尝试在每个步骤中缓解痛苦。例如,它从不运行冗余的 blame 命令,而 update-ranges 可以使调整行号更加容易。当然,还有一个可靠性,即必须逐个查看 hunks... :-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]="\033[0;30m"
    colors[1]="\033[0;34m"
    colors[2]="\033[0;32m"
    colors[3]="\033[0;33m"
    getline commits < "@changes"
    split(commits,commit,/\n/)
}
NR!=1 { print "" }
{
    thiscommit=gensub(/ .*/,"",1,commit[NR])
    printf "%s\n","\033[0;31m"commit[NR]"\033[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\033[0m\n",colors[color],line[n]
    }
}
EOD
awk -f @pretty @blames | less -R
}

我觉得这就是答案,但是需要测试一下,因为你没有提供示例。希望你能获得奖励,但是时间很快就到了,而且有一个被投了3票的回答(尽管它根本没有回答挑战!)。 - joao
抱歉,我刚刚检查了一下,它并不是真正的自动化,而且它依赖于vim。我将选择上面更简单的答案,它不考虑不同的行号,但对于问题陈述来说非常简单且完美地工作。 - joao
@JoaoTavora 仔细看一下,上面的手动检查更新步骤(以及所有复杂性)是没有用的,初始清单已经是正确的了。在进行更正后,我得到的答案与他的很像,只是允许跟踪漂移。事实证明,你可以通过自动跟踪来完成相当有用的工作,但正确的做法是只使用正则表达式边界--基于行号的跟踪会失去控制,因为只有正则表达式才有希望在添加的行中自动找到新的边界。 - jthill

1

虽然我认为在我发布问题的时候这个答案可能还没有出现,但它解决了我的问题。不过,它是否足够灵活,能够跟踪历史上的移动语言特定结构呢?移动是指起始和结束行范围不是静态的。 - joao

0
一些想法...
这听起来类似于此帖子,看起来你可能会用类似这样的东西接近:
git blame -L '/variable_name *= */',+1

只要你知道与正则表达式匹配的定义。
有一个讨论线程在这里,关于使用tiggit gui(显然可以处理这个问题)。我自己还没有尝试过,所以无法验证它(稍后我会尝试一下)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接