Як я можу легко зафіксувати минулий вчинок?


116

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

Так, але тоді команда rebase повторно перевірить майстер і перезавантажить всі наступні комісії на фіксовану комісію. Чи не так, як ви їдете rebase -i?
CB Bailey

Насправді, з цією відповіддю є потенційна проблема, я думаю, що це має бути rebase --onto tmp bad-commit master. Як було написано, він спробує застосувати погане ув'язнення до стаціонарного стану.
CB Bailey

Ось ще один інструмент для автоматизації процесу налаштування / відновлення: stackoverflow.com/a/24656286/1058622
Mika Eloranta

Відповіді:


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навколо своєї бази даних, щоб більше не вимагати чистого робочого каталогу
Tobias Kienzler

@TobiasKienzler: Про використання git stashта git stash pop: ви маєте рацію, але, на жаль git stash, у Windows набагато повільніше, ніж у Linux чи OS / X. Оскільки мій робочий каталог зазвичай чистий, я пропустив цей крок, щоб не уповільнити команду.
Фріріх Раабе

Я можу це підтвердити, особливо під час роботи в мережі: - /
Tobias Kienzler

1
Приємно. Я випадково це зробив git rebase -i --fixup, і він відхилився від фіксованого комітету як вихідного пункту, тому аргумент ша не був потрібен у моєму випадку.
fwielstra

1
Для людей, які часто використовують --autosquash, може бути корисним, щоб це було поведінкою за замовчуванням: git config --global rebase.autosquash true
taktak004

31

Що я роблю:

git add ... # Додати виправлення.
git počin # Виконано, але в неправильному місці.
git rebase -i HEAD ~ 5 # Вивчіть останні 5 комісій для повторного використання.

Ваш редактор відкриє список останніх 5 комісій, готових до втручання. Змінити:

вибір 08e833c Хороша зміна 1.
вибір 9134ac9 Хороша зміна 2.
вибрати 5adda55 Погана зміна!
вибрати 400bce4 Хороша зміна 3.
pick 2bc82n1 Виправлення поганих змін.

... до:

вибір 08e833c Хороша зміна 1.
вибір 9134ac9 Хороша зміна 2.
вибрати 5adda55 Погана зміна!
f 2bc82n1 Виправлення поганих змін. # Перемістіть вгору та змініть "pick" на "f" для "fixup".
вибрати 400bce4 Хороша зміна 3.

Збережіть і вийдіть із редактора, і виправлення буде повернуто до коміту, до якого належить.

Після того, як ви зробите це кілька разів, ви зробите це за лічені секунди. Інтерактивний релізинг - це особливість, яка насправді продала мене на git. Це неймовірно корисно для цього та іншого ...


9
Очевидно, ви можете змінити HEAD ~ 5 на HEAD ~ n, щоб повернутися далі. Ви не хочете втручатися в будь-яку історію, яку ви просунули вгору за течією, тому я зазвичай набираю 'git rebase -i origin / master', щоб переконатися, що я змінюю лише незапущену історію.
Кріс Дженкінс

4
Це дуже схоже на те, що я завжди робив; FWIW, можливо, вас зацікавить --autosquashперемикач git rebase, який автоматично переставляє кроки в редакторі для вас. Дивіться мою відповідь щодо сценарію, який використовує це для реалізації git fixupкоманди.
Фріріх Раабе

Я не знав, що ви можете просто переоформити хеши, мило!
Аарон Франке

Це чудово! Тільки для того, щоб переконатися, що всі роботи з ребазами виконані, це окрема галузь функції. І не возитися зі спільною гілкою, як майстер.
Джей Моді

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

Ви можете змінити псевдонім, щоб він автоматично зберігався, замість того, щоб подавати попередження. Однак якщо виправлення не застосовується чисто, вам потрібно буде виправити скриньку вручну після виправлення конфліктів. Збереження та вивільнення вручну здається більш послідовним та менш заплутаним.


Це дуже корисно. Для мене найпоширенішим випадком використання є встановлення змін на попередньому комітеті, тому для 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 +1, які ви хочете змінити.

Примітка: git rebase --autosquash без -i не працює, але з -i працював, що дивно.


2016 рік і --autosquashбез нього -iще не працює.
Джонатан Крос

1
Як говорить підручна сторінка: Цей параметр дійсний лише тоді, коли використовується опція - інтерактив. Але є простий спосіб пропустити редактор:EDITOR=true git rebase --autosquash -i
joeytwiddle

Другий крок для мене не працює, кажучи: Будь ласка, вкажіть, на яку гілку ви хочете відновити.
djangonaut

git rebase --autosquash -i HEAD ~ 2 (де 2 - кількість комітетів +1, вставлених, які ви хочете змінити.
Sérgio

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 , як комміттера (Комміт ви хочете зафіксувати поточні зміни с) посилання , то голова буде зміщуватися після Fixup фіксації була створена і вам потрібно буде використовувати HEAD ~ 3 для позначення одного і того ж коміту.


Цікава альтернатива. +1
VonC

4

Ви можете уникнути інтерактивного етапу за допомогою редактора "null":

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

Це використовуватиме /bin/trueяк редактор, а не /usr/bin/vim. Він завжди приймає все, що підказує, без підказки.


Дійсно, це саме те, що я зробив у своїй "оригінальній відповіді" на відповідь сценарію Python від 30 вересня 2010 року (зверніть увагу на те, як внизу написано сценарій call(["set", "GIT_EDITOR=true", "&&", git, "rebase", "-i" ...).
Фріріх Раабе

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захищає комірки від мовчазного скидання в рідкісних ситуаціях (коли ви відхилили нову гілку, а хтось уже відмовився від минулих комісій).


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: добре звучить, я про це не знаю--autosquash
Tobias Kienzler

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.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.