Как я могу легко зафиксировать прошлую фиксацию?

Я только что прочитал изменение одного файла в прошлом в git, но, к сожалению, принятое решение "переупорядочивает" коммиты, чего я не хочу. Итак, вот мой вопрос:

Время от времени я замечаю ошибку в своем коде во время работы над (несвязанной) функцией. Быстрый git blame затем показывает, что ошибка была введена за несколько месяцев назад (я совершаю довольно много, так что обычно это не последняя фиксация, которая ввела ошибку). На этом этапе я обычно делаю это:

git stash                      # temporarily put my work aside
git rebase -i <bad_commit>~1   # rebase one step before the bad commit
                               # mark broken commit for editing
vim <affected_sources>         # fix the bug
git add <affected_sources>     # stage fixes
git commit -C <bad_commit>     # commit fixes using same log message as before
git rebase --continue          # base all later changes onto this

Однако это происходит так часто, что вышеприведенная последовательность раздражает. Особенно "интерактивная перебаза" скучна. Есть ли ярлык для вышеупомянутой последовательности, которая позволяет мне изменить произвольную фиксацию в прошлом с поэтапными изменениями? Я прекрасно понимаю, что это меняет историю, но я часто ошибаюсь, что мне очень нравится иметь что-то вроде

vim <affected_sources>             # fix bug
git add -p <affected_sources>      # Mark my 'fixup' hungs for staging
git fixup <bad_commit>             # amend the specified commit with staged changes,
                                   # rebase any successors of bad commit on rewritten 
                                   # commit.

Может быть, умный script, который может переписать, фиксирует использование инструментов сантехники или так?

Ответ 1

ОБНОВЛЕННЫЙ ОТВЕТ

Некоторое время назад к git commit был добавлен новый аргумент --fixup, который можно использовать для построения фиксации с сообщением журнала, подходящим для git rebase --interactive --autosquash. Итак, самый простой способ исправить прошлую фиксацию:

$ git add ...                           # Stage a fix
$ git commit --fixup=a0b1c2d3           # Perform the commit to fix broken a0b1c2d3
$ git rebase -i --autosquash a0b1c2d3~1 # Now merge fixup commit into broken commit

ОРИГИНАЛЬНЫЙ ОТВЕТ

Вот немного Python script, который я написал некоторое время назад, который реализует эту логику git fixup, на которую я надеялся в своем первоначальном вопросе. script предполагает, что вы выполнили некоторые изменения, а затем примените эти изменения к данным фиксации.

ПРИМЕЧАНИЕ: Этот script относится к Windows; он ищет git.exe и устанавливает переменную среды GIT_EDITOR, используя set. Отрегулируйте это по мере необходимости для других операционных систем.

Используя этот script, я могу точно реализовать "исправленные исправленные источники, исправления этапа, запустить рабочий процесс git fixup", который я запросил:

#!/usr/bin/env python
from subprocess import call
import sys

# Taken from http://stackoverflow.com/questions/377017/test-if-executable-exists-in python
def which(program):
    import os
    def is_exe(fpath):
        return os.path.exists(fpath) and os.access(fpath, os.X_OK)

    fpath, fname = os.path.split(program)
    if fpath:
        if is_exe(program):
            return program
    else:
        for path in os.environ["PATH"].split(os.pathsep):
            exe_file = os.path.join(path, program)
            if is_exe(exe_file):
                return exe_file

    return None

if len(sys.argv) != 2:
    print "Usage: git fixup <commit>"
    sys.exit(1)

git = which("git.exe")
if not git:
    print "git-fixup: failed to locate git executable"
    sys.exit(2)

broken_commit = sys.argv[1]
if call([git, "rev-parse", "--verify", "--quiet", broken_commit]) != 0:
    print "git-fixup: %s is not a valid commit" % broken_commit
    sys.exit(3)

if call([git, "diff", "--staged", "--quiet"]) == 0:
    print "git-fixup: cannot fixup past commit; no fix staged."
    sys.exit(4)

if call([git, "diff", "--quiet"]) != 0:
    print "git-fixup: cannot fixup past commit; working directory must be clean."
    sys.exit(5)

call([git, "commit", "--fixup=" + broken_commit])
call(["set", "GIT_EDITOR=true", "&&", git, "rebase", "-i", "--autosquash", broken_commit + "~1"], shell=True)

Ответ 2

Что я делаю:

git add ...           # Add the fix.
git commit            # Committed, but in the wrong place.
git rebase -i HEAD~5  # Examine the last 5 commits for rebasing.

Ваш редактор откроется со списком последних 5 коммитов, готовых к вмешательству. Изменение:

pick 08e833c Good change 1.
pick 9134ac9 Good change 2.
pick 5adda55 Bad change!
pick 400bce4 Good change 3.
pick 2bc82n1 Fix of bad change.

... в:

pick 08e833c Good change 1.
pick 9134ac9 Good change 2.
pick 5adda55 Bad change!
f 2bc82n1 Fix of bad change. # Move up, and change 'pick' to 'f' for 'fixup'.
pick 400bce4 Good change 3.

Сохраните и выйдите из своего редактора, и исправление будет возвращено в фиксацию, к которой принадлежит.

После того, как вы сделали это несколько раз, вы сделаете это за несколько секунд во сне. Интерактивная перезагрузка - это функция, которая действительно продала меня на git. Это невероятно полезно для этого и более...

Ответ 3

Немного поздно для вечеринки, но вот решение, которое работает, как предполагал автор.

Добавьте это в свой .gitconfig:

[alias]
    fixup = "!sh -c '(git diff-files --quiet || (echo Unstaged changes, please commit or stash with --keep-index; exit 1)) && COMMIT=$(git rev-parse $1) && git commit --fixup=$COMMIT && git rebase -i --autosquash $COMMIT~1' -"

Пример использования:

git add -p
git fixup HEAD~5

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

git add -p
git stash --keep-index
git fixup HEAD~5
git stash pop

Вы можете автоматически изменить псевдоним, чтобы он был автоматически, вместо того, чтобы давать предупреждение. Однако, если исправление не применяется чисто, вам понадобится вытащить тайник вручную после устранения конфликтов. Выполнение как сохранения, так и выпадающего вручную кажется более последовательным и менее запутанным.

Ответ 4

Чтобы исправить один коммит:

git commit --fixup a0b1c2d3 .
git rebase --autosquash -i HEAD~2

где a0b1c2d3 - это коммит, для которого вы хотите исправить, а где 2 - количество вставленных коммитов +1, которые вы хотите изменить.

Примечание: git rebase --autosquash без -i не работает, но с -i работает, что странно.

Ответ 5

UPDATE: Теперь можно найти более чистую версию script: https://github.com/deiwin/git-dotfiles/blob/docs/bin/git-fixup.

Я искал нечто подобное. Этот Python script кажется слишком сложным, поэтому я собрал свое собственное решение:

Во-первых, мои псевдонимы git выглядят так (заимствованные из здесь):

[alias]
  fixup = !sh -c 'git commit --fixup=$1' -
  squash = !sh -c 'git commit --squash=$1' -
  ri = rebase --interactive --autosquash

Теперь функция bash становится довольно простой:

function gf {
  if [ $# -eq 1 ]
  then
    if [[ "$1" == HEAD* ]]
    then
      git add -A; git fixup $1; git ri $1~2
    else
      git add -A; git fixup $1; git ri $1~1
    fi
  else
    echo "Usage: gf <commit-ref> "
  fi
}

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

Часть if [[ "$1" == HEAD* ]] (взятая из здесь) используется, потому что если вы используете, например, HEAD ~ 2 в качестве вашей фиксации (фиксация, которую вы хотите исправить текущие изменения с помощью), тогда HEAD будет смещен после фиксации фиксации, и вам нужно будет использовать HEAD ~ 3 для ссылки на тот же commit.

Ответ 6

Вы можете избежать интерактивного этапа, используя редактор "null":

$ EDITOR=true git rebase --autosquash -i ...

В качестве редактора вместо /usr/bin/vim будет использоваться /bin/true. Он всегда принимает любые git предложения, без подсказки.

Ответ 7

То, что действительно беспокоило меня о рабочем процессе fixup, заключалось в том, что мне приходилось выяснять, какой из них я хочу, чтобы каждый раз перескакивал это изменение. Я создал команду "git fixup", которая помогает с этим.

Эта команда создает фиксацию fixup, с добавленной магией, которая использует git-deps, чтобы автоматически найти соответствующую фиксацию, поэтому частой рабочий процесс сводится к:

# discover and fix typo in a previously committed change
git add -p # stage only typo fix
git fixup

# at some later point squash all the fixup commits that came up
git rebase --autosquash master

Это работает только в том случае, если поэтапные изменения могут быть однозначно приписаны конкретному фиксации на рабочем дереве (между мастером и HEAD). Я нахожу это очень часто для типа небольших изменений, которые я использую для этого, например. опечатки в комментариях или именах недавно введенных (или переименованных) методов. Если это не так, он, по крайней мере, отобразит список коммитов-кандидатов.

Я использую это много в моем ежедневном рабочем процессе, чтобы быстро интегрировать небольшие изменения в ранее измененные строки в фиксации моей рабочей ветки. script не так красив, как он мог быть, и он написан в zsh, но он хорошо справлялся со мной для меня, но я никогда не чувствовал необходимости переписывать его:

https://github.com/Valodim/git-fixup

Ответ 8

commit --fixup и rebase --autosquash велики, но их недостаточно. Когда у меня есть последовательность коммитов A-B-C, и я пишу еще несколько изменений в моем рабочем дереве, которые принадлежат одному или нескольким из существующих коммитов, мне приходится вручную смотреть на историю, решать, какие изменения принадлежат, в которой они совершают, сценить их и создайте фиксацию fixup!. Но git уже имеет доступ к достаточной информации, чтобы иметь возможность делать все это для меня, поэтому я написал Perl script, который делает именно это.

Для каждого столбца в git diff script используется git blame, чтобы найти фиксацию, которая последний раз коснулась соответствующих строк, и вызывает git commit --fixup для записи соответствующих fixup! коммиттов, по существу делающих то же самое, что и я прежде чем делать.

Если вы сочтете это полезным, пожалуйста, не стесняйтесь улучшать и повторять его, и, возможно, однажды мы получим такую ​​функцию в git. Мне бы хотелось увидеть инструмент, который может понять, как конфликт слияния должен быть разрешен, когда он был введен с помощью интерактивной rebase.

Ответ 9

Я написал небольшую функцию оболочки под названием gcf, чтобы выполнить фиксацию исправления и автоматическую перебазировку:

$ git add -p

  ... select hunks for the patch with y/n ...

$ gcf <earlier_commit_id>

  That commits the fixup and does the rebase.  Done!  You can get back to coding.

Например, вы можете установить второй коммит до последнего с помощью: gcf HEAD~~

Вот функция. Вы можете вставить его в свой ~/.bashrc

git_commit_immediate_fixup() {
  local commit_to_amend="$1"
  if [ -z "$commit_to_amend" ]; then
    echo "You must provide a commit to fixup!"; return 1
  fi

  # Get a static commit ref in case the commit is something relative like HEAD~
  commit_to_amend="$(git rev-parse "${commit_to_amend}")" || return 2

  #echo ">> Committing"
  git commit --no-verify --fixup "${commit_to_amend}" || return 3

  #echo ">> Performing rebase"
  EDITOR=true git rebase --interactive --autosquash --autostash \
                         --preserve-merges "${commit_to_amend}~"
}

alias gcf='git_commit_immediate_fixup'

Он использует --autostash для хранения и извлечения любых незафиксированных изменений, если это необходимо.

--autosquash требует перебазирования --interactive, но мы избегаем взаимодействия, используя манекен EDITOR.

Ответ 10

Я не знаю об автоматическом способе, но здесь решение, которое проще всего можно было бы сделать человеком-ботией:

git stash
# write the patch
git add -p <file>
git commit -m"whatever"   # message doesn't matter, will be replaced via 'fixup'
git rebase -i <bad-commit-id>~1
# now cut&paste the "whatever" line from the bottom to the second line
# (i.e. below <bad-commit>) and change its 'pick' into 'fixup'
# -> the fix commit will be merged into the <bad-commit> without changing the
# commit message
git stash pop

Ответ 11

Вы можете создать исправление для определенного файла, используя этот псевдоним.

[alias]
...
# fixup for a file, using the commit where it was last modified
fixup-file = "!sh -c '\
        [ $(git diff          --numstat $1 | wc -l) -eq 1 ] && git add $1 && \
        [ $(git diff --cached --numstat $1 | wc -l) -eq 1 ] || (echo No changes staged. ; exit 1) && \
        COMMIT=$(git log -n 1 --pretty=format:"%H" $1) && \
            git commit --fixup=$COMMIT && \
            git rebase -i --autosquash $COMMIT~1' -"

Если вы внесли некоторые изменения в myfile.txt, но не хотите помещать их в новую фиксацию, git fixup-file myfile.txt создаст fixup! для фиксации, где myfile.txt был последним изменен, а затем он будет rebase --autosquash.