Как я могу легко исправить прошлый коммит? - PullRequest
95 голосов
/ 23 июня 2010

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

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

Ответы [ 11 ]

144 голосов
/ 30 сентября 2010

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

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

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

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

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

# Taken from /372755/proverit-suschestvuet-li-ispolnyaemyi-fail-v-python 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)
28 голосов
/ 29 сентября 2010

То, что я делаю, это:

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!
<b>f 2bc82n1 Fix of bad change.</b> # Move up, and change 'pick' to 'f' for 'fixup'.
pick 400bce4 Good change 3.

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

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

18 голосов
/ 16 января 2014

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

Добавьте это к вашему .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

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

7 голосов
/ 24 ноября 2015

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

git commit --fixup a0b1c2d3 .
git rebase --autosquash -i

где a0b1c2d3 - фиксация, которую вы хотите исправить.

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

6 голосов
/ 02 декабря 2012

ОБНОВЛЕНИЕ: Более чистую версию скрипта теперь можно найти здесь: 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 для ссылки на ту же фиксацию.

4 голосов
/ 07 ноября 2016

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

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

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

3 голосов
/ 20 июля 2017

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

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

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

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

1 голос
/ 07 ноября 2016

Я написал небольшую функцию оболочки под названием 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
  fi

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

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

  echo ">> Performing rebase"
  # --autosquash requires -i, but we can avoid interaction with a dummy EDITOR
  EDITOR=true git rebase --interactive --autosquash --autostash \
                         --preserve-merges "${commit_to_amend}~"
}

alias gcf='git_commit_immediate_fixup'

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

1 голос
/ 30 марта 2016

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

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

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

0 голосов
/ 19 октября 2015

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

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

...