Git: обнаружение, которое совершает когда-либо затрагивало ряд строк

Мне сложно понять, как использовать git blame для получения набора коммитов, которые когда-либо касались заданного диапазона строк. Есть такие же вопросы, как этот, но принятый ответ не приносит мне гораздо больше.

Скажем, у меня есть определение, которое начинается в строке 1000 foo.rb. Это всего лишь 5 строк, но количество коммитов, которые когда-либо меняли эти строки, огромно. Если я делаю

git blame foo.rb -L 1000,+5

Я получаю ссылки на (максимум) пять отдельных коммитов, которые меняли эти строки, но меня также интересуют коммиты "позади".

Аналогично,

git rev-list HEAD -- foo.rb | xargs git log --oneline

- это почти то, что я хочу, но я не могу указать диапазоны строк для git rev-list

Могу ли я передать флаг git blame, чтобы получить список коммитов, которые когда-либо касались этих пяти строк, или самый быстрый способ создания script, который извлекает такую ​​информацию? Пусть на данный момент игнорирует возможность того, что определение однажды имело более или менее 5 строк.

Ответ 1

Поскольку Git 1.8.4, git log имеет -L, чтобы просмотреть эволюцию диапазона строк.

Например, предположим, что вы смотрите вывод git blame:

((aa27064...))[[email protected]:~/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 "[email protected]" &

И вы хотите узнать историю того, что теперь является строкой 155.

Тогда:

((aa27064...))[[email protected]:~/w/mlm/git]
$ git log --topo-order --graph -u -L 155,155:git-web--browse.sh
* commit 81f42f11496b9117273939c98d270af273c8a463
| Author: Giuseppe Bilotta <[email protected]>
| 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 <[email protected]>
|     Signed-off-by: Junio C Hamano <[email protected]>
| 
| 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 <[email protected]>
| 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 <[email protected]>
|     Signed-off-by: Junio C Hamano <[email protected]>
| 
| 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 <[email protected]>
  Date:   Sat Feb 2 07:32:53 2008 +0100

      Rename 'git-help--browse.sh' to 'git-web--browse.sh'.

      Signed-off-by: Christian Couder <[email protected]>
      Signed-off-by: Junio C Hamano <[email protected]>

  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

Я думаю, что это то, что вы хотите:

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 и получите соответствующее сообщение фиксации для списка изменений. У меня была странная проблема с использованием этого последнего шага, когда мне приходилось продолжать следующие несколько изменений, которые были выведены. Я не уверен, почему это было, поэтому я не включил его в свое решение.

Ответ 3

Не уверен, что вы хотите сделать, но, возможно, 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.

Вы можете поместить в строку изменение (или часть изменения), которое вы пытаетесь выполнить, и это будет перечислять коммиты, которые когда-либо касались этого изменения.

Ответ 4

Мне понравилась эта головоломка, она получила тонкости. Отправьте этот файл, скажем init foo.rb 1000,1005 и следуйте инструкциям. Когда вы закончите, файл @changes будет иметь правильный список коммитов в топологическом порядке, а @blames будет иметь фактический результат вины от каждого.

Это значительно сложнее, чем принятое решение выше. Он производит выходные данные, которые иногда будут более полезными, и их трудно воспроизвести, и было весело кодировать.

Проблема с попыткой отслеживания диапазонов номеров строк автоматически при переходе назад по истории заключается в том, что если ханг изменения пересекает границы диапазона с номерами строк, вы не можете автоматически определить, где в этом hunk должна быть граница нового диапазона, либо придется включать большой диапазон для больших дополнений и таким образом накапливать (иногда много) нерелевантные изменения или переходить в ручной режим, чтобы быть уверенным, что это правильно (что, конечно же, возвращает вас сюда), или время от времени принимает экстремальный урон.

Если вы хотите, чтобы ваш результат был точным, используйте приведенный выше ответ с надежными диапазонами регулярных выражений, такими как `/^ type function (/,/^}/', или используйте это, что на самом деле не так уж плохо, пару секунд на шаг назад во времени.

В обмен на дополнительную сложность он создает список попаданий в топологической последовательности и, по крайней мере, (довольно успешно), пытается улучшить боль на каждом шаге. Например, он никогда не запускает избыточную вину, а диапазоны обновлений упрощают настройку номеров строк. И, конечно же, надежность того, что нужно было индивидуально разглядывать куски...:-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 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
}

Ответ 6

Несколько мыслей.

Это похоже на этот пост, и похоже, что вы можете приблизиться к чему-то вроде этого:

git blame -L '/variable_name *= */',+1

До тех пор, пока вы знаете, какое определение должно соответствовать (для регулярного выражения).

Существует тема обсуждение здесь, об использовании tig и git gui (которые, по-видимому, могут справиться с этим). Я еще не пробовал это сам, поэтому не могу проверить его (я попробую позже).