#git
Возможно ли вернуться в состояние до коммита и сделать иначе, или как исключить файлы
из коммита, чтобы включить эти файлы в другой коммит?
Ответы
Ответ 1
Да, исправить старый коммит можно следующим образом:
создать на исправляемом коммите ветку, встать на нее (checkout -b)
поправить код, залить коммит (но!) с опцией ammend last commit (есть
такой переключатель в UI, и в командной строке параметр)
все коммиты со старой ветки идущие после исправленного переместить на новую ветку
(rebase - переместить разом,
либо cherry-pick - по одному). Я предпочитаю по одному для лучшего
разрешения конфликтов. cherry-pick можно так же делать из UI - правой кнопкой на
коммите - и там будет такая опция.
Если вы хотите исключить из него файлы - на этапе правки кода - уберите их из коммита
- "спрячьте" командой git stash save, ammend last commit чтоб перезалить текущий, cherry-pick
до нужного коммита, в котором вы хотите файлы вставить и командой git stash pop вытащите
их из тайника и закомитьте как надо.
Ответ 2
Самый функциональный способ, который работает без исключений вида "если это последний
коммит", а подходит для любого коммита -- это команда git rebase.
Эта команда позволяет как угодно перекраивать коммиты: объединяя коммиты, переставляя
их местами, переносить файлы из одного коммита в другой.
Культура программирования -- это не только культура написания самого кода, но в связи
с широким распространением систем версионного контроля -- ещё и культура ведения разработки
в VCS. В командах часто важно не только чтобы код был хорош, но и был порядок в ваших
коммитах и ветках.
Важно! Помните о том, что в большинстве команд принято делать rebase
только для локальных изменений, чтобы не пришлось потом выполнять git
push -f.
Итак, как исключить файлы из коммита.
Прежде всего убедитесь, что нет отслеживаемых файлов с изменениями -- иначе rebase
просто не запустится.
Можете положить эти изменения в карман через git stash.
Если это требование выполнено -- можете запускать rebase в интерактивном режиме,
например:
git rebase -i HEAD~4
После чего нужно у коммита, из которого вы хотите выделить файлы pick на edit (можно
просто e) и выйти из редактора с сохранением изменений.
Дойдя до коммита, который отмечен буквой e гит остановится и будет ожидать ваших
действий по изменению коммита.
Самое просто будет -- это дать команду git reset HEAD~ и собрать в коммит все файлы
(как новые, так и изменённые), за исключением тех, которые вы хотите использовать в
другом коммите.
После того, как вы выполните добавление изменений через git add мы коммитим это как
обычно через git commit -m "message".
Затем оставшиеся файлы добавляем и оформляем ещё один коммит (если файлы пойдут в
два коммита -- значит повторим эту операцию несколько раз)
В какой-то момент в папке не останется никаких незакоммиченных изменений -- можно
взять и внести новые изменения и новые коммиты. Я часто пользуюсь этим, чтобы добавить
сразу несколько коммитов после какого-то выбранного.
Когда вы решите, что сделано достаточно -- можно дать команду git rebase --continue.
В этот момент гит начнёт добавлять все остальные коммиты, которые шли после редактируемого
и если не будет конфликтов -- то будет переходить к следующему и следующему, пока не
дойдёт до вашего последнего коммита. Если же конфликты будут -- их нужно будет резолвить
и заново продолжать операцию git rebase --continue.
Я рекомендую подобные операции делать на каких-то тестовых репозиториях, чтобы попрактиковаться
и не страшно было что-то сделать не так.
Для примера приведу некоторый пример выделения файлов из коммита.
Итак, начнём.
Допустим у нас есть такие коммиты:
pick 0517714 add file1.txt
pick 13ae975 add file2.txt and file3.txt
pick b967f0f add file4.txt and file5.txt
pick 412ab2d add file6.txt
Сделанные следующим образом:
$ git clone ak@localhost:aaa.git bbb
Cloning into 'bbb'...
warning: You appear to have cloned an empty repository.
$ cd bbb
$ touch .gitignore
$ git add .gitignore
$ git commit -m "empty .gitignore"
[master (root-commit) f3f251b] empty .gitignore
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 .gitignore
$ touch file1.txt
$ git add file1.txt
$ git commit -m "add file1.txt"
[master 0517714] add file1.txt
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 file1.txt
$ touch file2.txt
$ git add file2.txt
$ touch file2.txt
$ touch file3.txt
$ git add file3.txt
$ git commit -m "add file2.txt and file3.txt"
[master 13ae975] add file2.txt and file3.txt
2 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 file2.txt
create mode 100644 file3.txt
$ touch file4.txt
$ git add file4.txt
$ touch file5.txt
$ git add file5.txt
$ git commit -m "add file4.txt and file5.txt"
[master b967f0f] add file4.txt and file5.txt
2 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 file4.txt
create mode 100644 file5.txt
$ touch file6.txt
$ git add file6.txt
$ git commit -m "add file6.txt"
[master 412ab2d] add file6.txt
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 file6.txt
$
И, допустим, мы хотим перенести файл 5 в предыдущий коммит (где файлы 2 и 3).
Делаем git rebase -i HEAD~4:
$ git rebase -i HEAD~4
pick 0517714 add file1.txt
pick 13ae975 add file2.txt and file3.txt
pick b967f0f add file4.txt and file5.txt
pick 412ab2d add file6.txt
# Rebase f3f251b..412ab2d onto f3f251b
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
~
"~/bbb/.git/rebase-merge/git-rebase-todo" 22L, 742C
У коммита b967f0f заменяем pick на edit и сохраняем изменения, гит заново последовательно
применяет коммиты 0517714 и 13ae975 (вы можете даже не заметить это, так быстро мелькают
изменения на экране) и останаваливается на коммите b967f0f:
$ git rebase -i HEAD~4
Stopped at b967f0f... add file4.txt and file5.txt
You can amend the commit now, with
git commit --amend
Once you are satisfied with your changes, run
git rebase --continue
$
Тут мы делаем простой шаг:
$ git reset HEAD~
$ git status
# HEAD detached at 13ae975
# You are currently editing a commit while rebasing branch 'master' on 'f3f251b'.
# (use "git commit --amend" to amend the current commit)
# (use "git rebase --continue" once you are satisfied with your changes)
#
# Untracked files:
# (use "git add ..." to include in what will be committed)
#
# file4.txt
# file5.txt
nothing added to commit but untracked files present (use "git add" to track)
$
(git status просто для наглядности)
Составим новый коммит из файла 4:
$ clear && history -c
$ git add file4.txt
$ git commit -m "add file4.txt"
[detached HEAD 4c72f5d] add file4.txt
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 file4.txt
Остался файл 5, создадим из него новый коммит:
$ git status
# HEAD detached from 13ae975
# You are currently editing a commit while rebasing branch 'master' on 'f3f251b'.
# (use "git commit --amend" to amend the current commit)
# (use "git rebase --continue" once you are satisfied with your changes)
#
# Untracked files:
# (use "git add ..." to include in what will be committed)
#
# file5.txt
nothing added to commit but untracked files present (use "git add" to track)
$ git add file5.txt
$ git commit -m "add file5.txt"
[detached HEAD f0cd810] add file5.txt
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 file5.txt
$
После чего рабочий каталог чист и можно сказать гит, что можно продолжить операцию
rebase и накатывать следующие коммиты:
$ git rebase --continue
Successfully rebased and updated refs/heads/master.
$
Здесь вам не видно, между тем как вы нажали enter и перед тем, как появится надпись
'Successfully rebased' гит последовательно применит остающиеся коммиты -- в нашем случае
это только коммит 412ab2d.
Вот какая у нас получилась новая история:
pick 0517714 add file1.txt
pick 13ae975 add file2.txt and file3.txt
pick 4c72f5d add file4.txt
pick f0cd810 add file5.txt
pick 6952a0b add file6.txt
Операция перестановки местами коммитов 4 и 5 и слияния коммитов 13ae975 с последующим
f0cd810 тривиальна -- нужно снова дать команду rebase, переставить местами коммиты
4c72f5d и f0cd810 и сделать squash коммита f0cd810 в коммит 13ae975 -- вот тут я писал
подробнее в отдельном посте: Можно ли в GIT добавить еще один файл в последний локальный
комит?
При помощи rebase переносить можно не только новые файлы, но изменения и удаления.
Также важно помнить, что если вы что-то неправильно сделали в середине rebase'а --
то помните, что у вас есть возможность отменить rebase: нужно дать команду git rebase
--abort.
Ответ 3
Если это последний коммит и он находится локально, то без проблем.
Допустим, последний коммит у нас B, предшествующий ему коммит A:
Сбросим наш коммит командой git reset B.
Делаем git status и видим следующую картину:
Последним коммитом будет коммит A, все изменения останутся, но они
будут в состоянии not stagged.
Формируем новый индекс для создания нового первого коммита с помощью команды git
add список_файлов_через_пробел (можно каждый файл по одному добавлять).
Делаем git commit -m "C", это будет наш первый коммит.
Делаем git commit -am "D", это будет наш второй коммит.
Параметр -a - добавляет все изменённые файлы в индекс.
Альтернативные варианты шага 5:
git add .
git commit -m "D"
Или
git add название_файлов_через_пробел
git commit -m "D"
Или
git add название_файла_1
...
git add название_файла_N
git commit -m "D"