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

117

Я только что прочитал изменение одного файла в прошлом коммите в 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.

Может быть, умный скрипт, который может переписывать коммиты с помощью сантехнических инструментов?

Фрерих Раабе
источник
Что вы имеете в виду под "переупорядочиванием" коммитов? Если вы меняете историю, то все коммиты, так как измененные коммиты должны быть разными, но принятый ответ на связанный вопрос не меняет порядок коммитов в каком-либо значимом смысле.
CB Bailey,
1
@Charles: Я имел в виду переупорядочение, как в: если я заметил, что HEAD ~ 5 - это сломанная фиксация, следующий принятый ответ в связанном вопросе сделает HEAD (кончик ветки) фиксированной фиксацией. Однако я бы хотел, чтобы HEAD ~ 5 был фиксированной фиксацией - это то, что вы получаете при использовании интерактивной перебазировки и редактировании одной фиксации для исправления.
Frerich Raabe
Да, но тогда команда rebase повторно проверит мастер и перебазирует все последующие коммиты на фиксированную фиксацию. Разве это не то, как ты водишь rebase -i?
CB Bailey,
На самом деле, с этим ответом есть потенциальная проблема, я думаю, так и должно быть rebase --onto tmp bad-commit master. Как написано, он попытается применить плохую фиксацию к фиксированному состоянию фиксации.
CB Bailey,
Вот еще один инструмент для автоматизации процесса исправления / перебазирования: stackoverflow.com/a/24656286/1058622
Мика Элоранта

Ответы:

166

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

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

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

Используя этот скрипт, я могу реализовать именно тот рабочий процесс, который я просил: «исправить неработающие источники, исправить этапы, запустить 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 stashи git stash popвокруг своей переустановки, чтобы больше не требовался чистый рабочий каталог
Тобиас Кинцлер, 01
@TobiasKienzler: Об использовании git stashи git stash pop: вы правы, но, к сожалению git stash, в Windows он работает намного медленнее, чем в Linux или OS / X. Поскольку мой рабочий каталог обычно чистый, я пропустил этот шаг, чтобы не замедлить выполнение команды.
Фрерих Раабе
Я могу подтвердить это, особенно при работе с сетевым
ресурсом
1
Ницца. Я случайно это сделал git rebase -i --fixup, и он переместился из фиксированного коммита в качестве отправной точки, поэтому аргумент sha в моем случае не нужен.
cthulhu
1
Для людей, часто использующих --autosquash, может быть полезно установить его как поведение по умолчанию: git config --global rebase.autosquash true
taktak004
31

Что я делаю:

git add ... # Добавить исправление.
git commit # Зафиксировано, но не в том месте.
git rebase -i HEAD ~ 5 # Проверить последние 5 коммитов на предмет перебазирования.

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

pick 08e833c Хорошая перемена 1.
pick 9134ac9 Хорошая перемена 2.
выбрать 5adda55 Плохая сдача!
pick 400bce4 Хорошая перемена 3.
pick 2bc82n1 Исправление плохих изменений.

... чтобы:

pick 08e833c Хорошая перемена 1.
pick 9134ac9 Хорошая перемена 2.
выбрать 5adda55 Плохая сдача!
f 2bc82n1 Исправление плохих изменений. # Поднимитесь вверх и измените 'pick' на 'f' для 'fixup'.
pick 400bce4 Хорошая перемена 3.

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

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

Крис Дженкинс
источник
9
Очевидно, вы можете изменить HEAD ~ 5 на HEAD ~ n, чтобы вернуться дальше. Вы не захотите вмешиваться в какую-либо историю, которую вы отправили вверх по течению, поэтому я обычно набираю git rebase -i origin / master, чтобы убедиться, что я изменяю только не отправленную историю.
Крис Дженкинс,
4
Это очень похоже на то, что я делал всегда; FWIW, вам может быть интересен --autosquashпереключатель для git rebase, который автоматически меняет порядок шагов в редакторе для вас. См. Мой ответ на сценарий, который использует это для реализации git fixupкоманды.
Frerich Raabe
Я не знал, что вы можете просто изменить порядок хэшей коммитов, хорошо!
Аарон Франке
Это отлично! Только чтобы убедиться, что вся работа по перебазированию выполнена, это отдельная ветка функций. И не связываться с общей веткой, как мастер.
Джей Моди
22

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

Добавьте это в свой .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

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

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

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

dschlyter
источник
Это очень полезно. Для меня наиболее распространенный вариант использования - исправить изменения в предыдущем коммите, поэтому git fixup HEADя создал псевдоним для этого. Полагаю, я мог бы также использовать поправку для этого.
кузнечик
Спасибо! Я также чаще всего использую его при последней фиксации, но у меня есть другой псевдоним для быстрого исправления. amend = commit --amend --reuse-message=HEADЗатем вы можете просто ввести git amendили git amend -aи пропустить редактор сообщения о фиксации.
dschlyter
3
Проблема с поправкой в ​​том, что я не помню, как ее произносить. Мне всегда приходится думать, поправить или поправить, а это нехорошо.
кузнечик
12

Чтобы исправить одну фиксацию:

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

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

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

Сержио
источник
2016 г. и --autosquashбез него по- -iпрежнему не работает.
Джонатан Кросс,
1
Как говорится на странице руководства: Эта опция действительна только при использовании опции --interactive. Но есть простой способ пропустить редактор:EDITOR=true git rebase --autosquash -i
joeytwiddle
Второй шаг у меня не работает, я говорю: «Пожалуйста, укажите, какую ветку вы хотите перебазировать».
djangonaut
git rebase --autosquash -i HEAD ~ 2 (где 2 - количество коммитов +1, вставленных, которые вы хотите изменить.
Сержио
6

ОБНОВЛЕНИЕ: более чистую версию скрипта теперь можно найти здесь: https://github.com/deiwin/git-dotfiles/blob/docs/bin/git-fixup .

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

Во-первых, мои псевдонимы 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
}

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

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

Дейвин
источник
Интересная альтернатива. +1
VonC
4

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

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

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

joeytwiddle
источник
Действительно, это именно то, что я сделал в своем «исходном ответе» скрипта Python от 30 сентября 2010 года (обратите внимание, как в конце скрипта написано call(["set", "GIT_EDITOR=true", "&&", git, "rebase", "-i" ...).
Frerich Raabe
4

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

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

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

Валодим
источник
2

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

[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.

Альваро
источник
Очень умно, хотя я бы предпочел, чтобы git rebaseэто не производилось автоматически.
Hurikhan77
2

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

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

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

Окталист
источник
Еще я мечтал об автоматизации: git должен просто попытаться вернуть ее как можно глубже в историю, не ломая патч. Но ваш метод, наверное, более разумный. Рад видеть, что вы это сделали. Я это попробую! (Конечно, бывают случаи, когда патч исправления появляется в другом месте файла, и только разработчик знает, к какому коммиту он принадлежит. Или, возможно, новый тест в наборе тестов может помочь машине определить, где должно быть исправление.)
joeytwiddle
1

Я написал небольшую функцию оболочки, 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 \
                --rebase-merges --no-fork-point "${commit_to_amend}~"
}

alias gcf='git_commit_immediate_fixup'

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

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

--no-fork-pointзащищает коммиты от молчаливого сброса в редких ситуациях (когда вы создали новую ветку, и кто-то уже перебазировал предыдущие коммиты).

joeytwiddle
источник
0

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

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
Тобиас Кинцлер
источник
См. Мой ответ на сценарий, который использует это для реализации git fixupкоманды.
Frerich Raabe
@Frerich Raabe: звучит хорошо, я не знаю об этом--autosquash
Тобиас Кинцлер
0

Я бы рекомендовал https://github.com/tummychow/git-absorb :

Высота лифта

У вас есть функциональная ветка с несколькими коммитами. Ваш товарищ по команде просмотрел ветку и указал на несколько ошибок. У вас есть исправления ошибок, но вы не хотите запихивать их все в непрозрачный коммит, в котором говорится об исправлениях, потому что вы верите в атомарные коммиты. Вместо того, чтобы вручную находить SHA фиксации git commit --fixupили запускать ручную интерактивную перебазировку, сделайте следующее:

  • git add $FILES_YOU_FIXED

  • git absorb --and-rebase

  • или: git rebase -i --autosquash master

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

Петски
источник