When I’ve worked a bit with my source code, I did my usual thing commit and then I pushed to a remote repository. But then I noticed I forgot to organize my imports in the source code. So I do the amend command to replace the previous commit:
> git commit --amend
Unfortunately the commit can’t be pushed back to the repository. It is rejected like this:
> git push origin
To //my.remote.repo.com/stuff.git/
! [rejected] master -> master (non-fast forward)
error: failed to push some refs to '//my.remote.repo.com/stuff.git/'
What should I do? (I can access the remote repository.)
asked Oct 31, 2008 at 10:23
6
I actually once pushed with --force
and .git
repository and got scolded by Linus BIG TIME. In general this will create a lot of problems for other people. A simple answer is «Don’t do it».
I see others gave the recipe for doing so anyway, so I won’t repeat them here. But here is a tip to recover from the situation after you have pushed out the amended commit with —force (or +master).
- Use
git reflog
to find the old commit that you amended (call itold
, and we’ll call the new commit you created by amendingnew
). - Create a merge between
old
andnew
, recording the tree ofnew
, likegit checkout new && git merge -s ours old
. - Merge that to your master with
git merge master
- Update your master with the result with
git push . HEAD:master
- Push the result out.
Then people who were unfortunate enough to have based their work on the commit you obliterated by amending and forcing a push will see the resulting merge will see that you favor new
over old
. Their later merges will not see the conflicts between old
and new
that resulted from your amending, so they do not have to suffer.
answered Jan 11, 2009 at 7:36
12
You are seeing a Git safety feature. Git refuses to update the remote branch with your branch, because your branch’s head commit is not a direct descendent of the current head commit of the branch that you are pushing to.
If this were not the case, then two people pushing to the same repository at about the same time would not know that there was a new commit coming in at the same time and whoever pushed last would lose the work of the previous pusher without either of them realising this.
If you know that you are the only person pushing and you want to push an amended commit or push a commit that winds back the branch, you can ‘force’ Git to update the remote branch by using the -f
switch.
git push -f origin master
Even this may not work as Git allows remote repositories to refuse non-fastforward pushes at the far end by using the configuration variable receive.denynonfastforwards
. If this is the case the rejection reason will look like this (note the ‘remote rejected’ part):
! [remote rejected] master -> master (non-fast forward)
To get around this, you either need to change the remote repository’s configuration or as a dirty hack you can delete and recreate the branch thus:
git push origin :master
git push origin master
In general the last parameter to git push
uses the format <local_ref>:<remote_ref>
, where local_ref
is the name of the branch on the local repository and remote_ref
is the name of the branch on the remote repository. This command pair uses two shorthands. :master
has a null local_ref which means push a null branch to the remote side master
, i.e. delete the remote branch. A branch name with no :
means push the local branch with the given name to the remote branch with the same name. master
in this situation is short for master:master
.
answered Oct 31, 2008 at 21:58
CB BaileyCB Bailey
733k101 gold badges626 silver badges651 bronze badges
6
Quick rant: The fact that no one has posted the simple answer here demonstrates the desperate user-hostility exhibited by the Git CLI.
Anyway, the «obvious» way to do this, assuming you haven’t tried to force the push, is to pull first. This pulls the change that you amended (and so no longer have) so that you have it again.
Once you have resolved any conflicts, you can push again.
So:
git pull
If you get errors in pull, maybe something is wrong in your local repository configuration (I had a wrong ref in the .git/config branch section).
And after
git push
Maybe you will get an extra commit with the subject telling about a «Trivial merge».
5
Short answer: Don’t push amended commits to a public repo.
Long answer: A few Git commands, like git commit --amend
and git rebase
, actually rewrite the history graph. This is fine as long as you haven’t published your changes, but once you do, you really shouldn’t be mucking around with the history, because if someone already got your changes, then when they try to pull again, it might fail. Instead of amending a commit, you should just make a new commit with the changes.
However, if you really, really want to push an amended commit, you can do so like this:
$ git push origin +master:master
The leading +
sign will force the push to occur, even if it doesn’t result in a «fast-forward» commit. (A fast-forward commit occurs when the changes you are pushing are a direct descendant of the changes already in the public repo.)
answered Oct 31, 2008 at 14:35
mipadimipadi
392k89 gold badges519 silver badges477 bronze badges
3
Here is a very simple and clean way to push your changes after you have already made a commit --amend
:
git reset --soft HEAD^
git stash
git push -f origin master
git stash pop
git commit -a
git push origin master
Which does the following:
- Reset branch head to parent commit.
- Stash this last commit.
- Force push to remote. The remote now doesn’t have the last commit.
- Pop your stash.
- Commit cleanly.
- Push to remote.
Remember to change origin
and master
if applying this to a different branch or remote.
Neuron
4,8435 gold badges36 silver badges54 bronze badges
answered Jun 21, 2015 at 14:41
FaizaFaiza
7315 silver badges5 bronze badges
3
I have solved it by discarding my local amended commit and adding the new changes on top:
# Rewind to commit before conflicting
git reset --soft HEAD~1
# Pull the remote version
git pull
# Add the new commit on top
git add ...
git commit
git push
answered Sep 24, 2012 at 15:46
barabara
2,8842 gold badges25 silver badges23 bronze badges
4
I had the same problem.
- Accidentally amended the last commit that was already pushed
- Done a lot of changes locally, committed some five times
- Tried to push, got an error, panicked, merged remote, got a lot of not-my-files, pushed, failed, etc.
As a Git-newbie, I thought it was complete FUBAR.
Solution: Somewhat like @bara suggested + created a local backup branch
# Rewind to commit just before the pushed-and-amended one.
# Replace <hash> with the needed hash.
# --soft means: leave all the changes there, so nothing is lost.
git reset --soft <hash>
# Create new branch, just for a backup, still having all changes in it.
# The branch was feature/1234, new one - feature/1234-gone-bad
git checkout -b feature/1234-gone-bad
# Commit all the changes (all the mess) not to lose it & not to carry around
git commit -a -m "feature/1234 backup"
# Switch back to the original branch
git checkout feature/1234
# Pull the from remote (named 'origin'), thus 'repairing' our main problem
git pull origin/feature/1234
# Now you have a clean-and-non-diverged branch and a backup of the local changes.
# Check the needed files from the backup branch
git checkout feature/1234-gone-bad -- the/path/to/file.php
Maybe it’s not a fast and clean solution, and I lost my history (1 commit instead of 5), but it saved a day’s work.
answered Feb 20, 2014 at 10:24
daviscadavisca
1,07410 silver badges16 bronze badges
If you have not pushed the code to your remote branch (GitHub/Bitbucket) you can change the commit message on the command line as below.
git commit --amend -m "Your new message"
If you’re working on a specific branch, do this:
git commit --amend -m "BRANCH-NAME: new message"
If you’ve already pushed the code with a wrong message then you need to be careful when changing the message. i.e after you change the commit message and try pushing it again you end up with having issues. To make it smooth follow the following steps.
Please read the entire answer before doing it
git commit --amend -m "BRANCH-NAME : your new message"
git push -f origin BRANCH-NAME # Not a best practice. Read below why?
Important note: When you use the force push directly you might end up with code issues that other developers are working on the same branch. So to avoid those conflicts you need to pull the code from your branch before making the force push:
git commit --amend -m "BRANCH-NAME : your new message"
git pull origin BRANCH-NAME
git push -f origin BRANCH-NAME
This is the best practice when changing the commit message, if it was already pushed.
answered Jan 13, 2015 at 7:20
4
If the message to be changed is for the latest commit to the repository, then the following commands are to be executed:
git commit --amend -m "New message"
git push --force repository-name branch-name
Note: using —force is not recommended unless you are absolutely sure that no one else has cloned your repository after the latest commit.
A safer alternative is to use:
git push --force-with-lease repository-name branch-name
Unlike --force
, which will destroy any changes someone else has pushed to the branch, --force-with-lease
will abort if there was an upstream change to the repository.
answered Mar 16, 2022 at 10:34
M-RazaviM-Razavi
3,1072 gold badges37 silver badges44 bronze badges
You are getting this error because the Git remote already has these commit files. You have to force push the branch for this to work:
git push -f origin branch_name
Also make sure you pull the code from remote as someone else on your team might have pushed to the same branch.
git pull origin branch_name
This is one of the cases where we have to force push the commit to remote.
answered Jan 21, 2016 at 6:35
1
If you know nobody has pulled your un-amended commit, use the --force-with-lease
option of git push
.
In TortoiseGit, you can do the same thing under «Push…» options «Force: May discard» and checking «known changes».
Force (May discard known changes) allows the remote repository to accept a safer non-fast-forward push. This can cause the remote repository to lose commits; use it with care. This can prevent from losing unknown changes from other people on the remote. It checks if the server branch points to the same commit as the remote-tracking branch (known changes). If yes, a force push will be performed. Otherwise it will be rejected. Since git does not have remote-tracking tags, tags cannot be overwritten using this option.
answered Jun 7, 2016 at 0:03
You can do it in a simple and safe way by doing:
- Amend your last commit with
git commit --amend
and whatever options you need to add git pull
to sync your local repo with your remote repo.- After pull, you will have conflicts between local and remote. You just have to solve them by accepting current changes and commit again.
git push
Now your local and remote repo are updated with no need to change your repo history.
answered May 12, 2022 at 7:14
2
Here is a very simple and clean way to push your changes after you have already made a git add "your files"
and git commit --amend
:
git push origin master -f
or:
git push origin master --force
kenorb
149k80 gold badges668 silver badges723 bronze badges
answered Jan 18, 2016 at 8:29
crakencraken
1,39310 silver badges16 bronze badges
1
I had to fix this problem with pulling from the remote repo and deal with the merge conflicts that arose, commit and then push. But I feel like there is a better way.
answered Oct 31, 2008 at 11:39
SpoikeSpoike
119k44 gold badges138 silver badges158 bronze badges
1
I just kept doing what Git told me to do. So:
- Can’t push because of amended commit.
- I do a pull as suggested.
- Merge fails. so I fix it manually.
- Create a new commit (labeled
«merge») and push it. - It seems to work!
Note: The amended commit was the latest one.
answered Dec 2, 2016 at 16:29
RolfRolf
5,4765 gold badges39 silver badges58 bronze badges
1
The following worked for me when changing Author and Committer of a commit.
git push -f origin master
Git was smart enough to figure out that these were commits of identical deltas which only differed in the meta information section.
Both the local and remote heads pointed to the commits in question.
answered Feb 3, 2020 at 3:36
MadPhysicistMadPhysicist
5,1839 gold badges39 silver badges98 bronze badges
In this case, you should —force.
Based on:
If it is an individual project, I would do this:
git push origin <branch-name> -f
If you are working with your team, or other peers are reviewing and using your code the force
flag is not recommended. Mainly because you always want a clean git history.
What I would do?
- If more people are working in the same branch or others reviewing your code, I would
git commit --amend
, thengit push -f ...
and let people know that they need togit pull --rebase
to be able to see your changes. - If something like this happens while reviewing a PR or MR, add new clean commit and at the end
squash
to clean up history.
answered Oct 3, 2022 at 18:19
Here, How I fixed an edit in a previous commit:
-
Save your work so far.
-
Stash your changes away for now if made:
git stash
Now your working copy is clean at the state of your last commit. -
Make the edits and fixes.
-
Commit the changes in «amend» mode:
git commit --all --amend
-
Your editor will come up asking for a log message (by default, the old log message). Save and quit the editor when you’re happy with it.
The new changes are added on to the old commit. See for yourself with
git log
andgit diff HEAD^
-
Re-apply your stashed changes, if made:
git stash apply
answered Mar 13, 2019 at 6:34
Harshal WaniHarshal Wani
2,2392 gold badges25 silver badges39 bronze badges
To avoid forced push, in the remote bare repository remove the last commit (the one to be amended) using:
git update-ref HEAD HEAD^
then push the amended commit with no conflict.
Note: This assumes no one has pulled the wrong commit in the meantime. If they have, they will have to similarly rewind and pull again, possibly merging their own changes.
answered Feb 28, 2022 at 0:42
FNiaFNia
1636 bronze badges
Коллеги, приветствую. Сегодня мы немного поговорим о работе с коммитами в системе контроля версиями — Git.
Целью этой статьи является обзор популярных сценариев проблем и методов их решения. Мы узнаем о том, как можно решить проблемы связанные с человеческими ошибками при создании коммитов в Git, какой способ устранения ошибок лучше применять в конкретных ситуациях.
Возникающие проблемы
В ходе разработки каких-либо проектов в Git, мы можем столкнуться с различными ситуациями, такими как:
- Неправильное сообщение коммита
- Забыли добавить некоторые файлы
- Забыли удалить некоторые файлы
- Необходимость удалить коммит
- Необходимость объединить коммиты
- и т.д
Безопасные изменения для локального репозитория
Изменить последний коммит
Давайте предположим, что мы работаем над каким-либо проектом локально.
Вдруг мы понимаем, что в момент создания сообщения для последнего коммита мы допустили ошибку, либо сообщение вовсе не подходит. В таком случае выполним команду:
git commit --amend
Перед нами откроется текстовый редактор с предложением изменить сообщение. Также мы можем увидеть список файлов которые будут зафиксированы.
Изменим сообщение коммита, выйдем из текстового редактора и проверим историю. Для этого введем команду:
git log --oneline
Но что делать, если мы забыли добавить или изменить какие-то файлы в наш последний коммит?
Для этого импортируем нужные файлы в наш проект или изменяем уже существующие и добавляем их в индекс.
Для теста я добавлю файл forgotten file.txt в проект. После этого еще раз выполним команду:
git commit --amend
Git сообщит нам, что вновь добавленный файл в индекс будет зафиксирован. Также мы можем переименовать сообщение нашего коммита, как мы делали это ранее. Воспользуемся этой возможностью и изменим сообщение.
Проверяем историю.
В случае необходимости удаления ненужного файла из последнего коммита, мы можем удалить его из проекта, добавить изменения в индекс и воспользоваться все той же командой:
git commit --amend
Удалить коммит
Для того, чтобы удалить один или несколько коммитов, мы можем воспользоваться командой git reset. Существует два типа выполнения reset:
- Soft — удаляет коммит из истории и переводит файлы из него в состояние рабочего каталога.
- Hard — удаляет коммит из истории вместе с файлами.
Сначала разберем soft reset. Возвращаемся к нашему проекту.
В один момент мы осознали, что допустили ошибку и хотели бы переделать последний коммит.
Для этого выполним команду:
git reset HEAD~1
Проверим историю коммитов. Мы можем убедится, что последний коммит был удален.
Файлы, которые были созданы или изменены в удаленном коммите, перешли в состояние рабочего каталога. Мы можем продолжить работу и, например, создать новый коммит на основе этих файлов.
Подобные действия мы можем выполнять для нескольких коммитов сразу. Для примера введем команду:
git reset HEAD~3
3 последних коммита пропали из истории.
Проверим состояние файлов, которых коснулась процедура удаления коммитов.
Мы можем полностью удалить коммиты без сохранения связанных с ними файлов. Для этого выполним процедуру hard reset:
git reset --hard HEAD~3
И проверим состояние.
Массовое изменение коммитов
Вернемся к нашему проекту и предположим, что настала пора закачать на удаленный репозиторий наши изменения. Безусловно, мы можем выполнять эту процедуру хоть сейчас, однако, мы бы хотели провести небольшую оптимизацию. Например, сократить количество коммитов, исправить ошибки в сообщениях коммитов и т.д.
Для этой задачи хорошо подходит выполнение команды git rebase. Ее запуск в интерактивном режиме позволит нам последовательно сообщить Git’у как бы мы хотели выполнить оптимизацию.
Мы считаем, что 4 последних коммита можно смело объединить в один.
Для этого выполним команду:
git rebase -i HEAD~4
Перед нами откроется текстовый редактор по умолчанию в котором мы будем выполнять изменения. Используем коммит с хэшем aab958a как начальный, путем указания команды pick напротив него и укажем напротив трех остальных команду squash. Как мы можем видеть ниже в описании, команда suqash объединит коммит с предыдущим в списке.
После выполнения — выходим из текстового редактора.
Теперь Git попросит от нас ввести сообщения коммитов. Просто для наглядности мы немного переименуем наш объединенный коммит. Также Git сообщит о том, какие файлы подвергнуться изменениям.
Снова выходим из текстового редактора.
Похоже, что наше объединение нескольких коммитов прошло успешно. Давайте проверим это командой:
git log --oneline
Мы уже были готовы к публикации наших изменений, однако, посчитали, что необходимо объединить другие 3 коммита в 1 и изменить его сообщение. При этом, мы бы не хотели затрагивать уже объединенный.
Снова вводим команду:
git rebase HEAD~4
В этот раз в качестве начального коммита будет использоваться с коммит хэшем 4c17043. Для его переименования укажем напротив команду reword. Затем объединяем с ним два последующих коммита командой squash. Последний коммит мы никак не изменяем.
После переименования выходим из текстового редактора и проверяем историю.
Похоже, что нам удалось провести историю коммитов в порядок и теперь нет причин откладывать загрузку нашего проекта на удаленный репозиторий. 🙂
Безопасные изменения для удаленных репозиториев
До этого мы рассматривали случаи локальной разработки. По большому счету, пока мы не публикуем изменения, наши действия — не могут повлиять на деятельность наших коллег по проекту. Применение вышеописанных способов до публикации изменений на удаленном репозитории — вполне оправдано.
Однако, что делать, если мы совместно работаем над проектом и только спустя какое-то время понимаем, что один из коммитов — лишний? Если мы воспользуемся вышеописанными методами, это может спровоцировать ряд проблем в работе над проектом у наших коллег.
В подобных случаях имеет смысл откатывать коммит. Подобная процедура создает еще один коммит, который отменит изменения ненужного. Таким образом мы не порушим историю проекта и избавим наших коллег от лишних проблем.
Снова вовзращаемся к исходному состоянию проекта.
Предположим, что изменения в коммите с хэшем и 5140d80 — лишние. Выполним команду:
git revert HEAD~2
Git сообщит нам об изменениях, после отката.
Проверим историю. Как мы можем убедиться, был создан новый коммит, «отменяющий» изменения ненужного.
К сожалению, в реальности могут возникнуть проблемы при откате коммитов, требующие дополнительных мер для их решения. В этой статье мы рассмотрели достаточно простые ситуации, чтобы наглядно увидеть что и как делает каждая команда.
Итоги
В этой статье мы узнали о четырех способах изменять коммиты:
- git commit —amend — позволяет изменять последний коммит.
- git reset — позволяет удалять коммиты.
- git rebase — позволяет производить массовое изменение коммитов, приводить в порядок историю.
- git revert — позволяет откатывать изменения коммитов.
Первые три команды — подойдут для использования в условиях локальной разработки.
Последняя команда — лучший способ, не создавать лишних проблем коллегам при совместной разработке.
Перезапись истории
Неоднократно при работе с Git, вам может потребоваться по какой-то причине внести исправления в историю коммитов.
Одно из преимуществ Git заключается в том, что он позволяет вам отложить принятие решений на самый последний момент.
Область индексирования позволяет вам решить, какие файлы попадут в коммит непосредственно перед его выполнением; благодаря команде git stash
вы можете решить, что не хотите продолжать работу над какими-то изменениями; также можете внести изменения в сделанные коммиты так, чтобы они выглядели как будто они произошли по-другому.
В частности, можно изменить порядок коммитов, сообщения или изменённые в коммитах файлы, объединить вместе или разбить на части, полностью удалить коммит — но только до того, как вы поделитесь своими наработками с другими.
В этом разделе вы познакомитесь со способами решения всех этих задач и научитесь перед публикацией данных приводить историю коммитов в нужный вам вид.
Примечание |
Не отправляйте свои наработки, пока вы ими не довольны Одно из основных правил Git заключается в том, что, так как большую часть работы вы делаете в своём локальном репозитории, то вы вольны переписывать свою историю локально. |
Изменение последнего коммита
Изменение вашего последнего коммита, наверное, наиболее частое исправление истории, которое вы будете выполнять.
Наиболее часто с вашим последним коммитом вам будет нужно сделать две основные операции: изменить сообщение коммита или изменить только что сделанный снимок, добавив, изменив или удалив файлы.
Если вы хотите изменить только сообщение вашего последнего коммита, это очень просто:
Эта команда откроет в вашем текстовом редакторе сообщение вашего последнего коммита, для того, чтобы вы могли его исправить.
Когда вы сохраните его и закроете редактор, будет создан новый коммит, содержащий это сообщение, который теперь и будет вашим последним коммитом.
Если вы создали коммит и затем хотите изменить зафиксированный снимок, добавив или изменив файлы (возможно, вы забыли добавить вновь созданный файл, когда совершали изначальный коммит), то процесс выглядит в основном так же.
Вы добавляете в индекс необходимые изменения, редактируя файл и выполняя для него git add
или git rm
для отслеживаемого файла, а последующая команда git commit --amend
берет вашу текущую область подготовленных изменений и делает её снимок для нового коммита.
Вы должны быть осторожными, используя этот приём, так как при этом изменяется SHA-1 коммита.
Поэтому, как и с операцией rebase
— не изменяйте ваш последний коммит, если вы уже отправили его в общий репозиторий.
Подсказка |
Изменённый коммит может потребовать изменения сообщения коммита При изменении коммита существует возможность изменить как его содержимое, так и сообщение коммита. С другой стороны, если изменения незначительны (исправление опечаток, добавление в коммит забытого файла), то текущее сообщение вполне можно оставить; чтобы лишний раз не вызывать редактор, просто добавьте измененные файлы в индекс и выполните команду:
|
Изменение сообщений нескольких коммитов
Для изменения коммита, расположенного раньше в вашей истории, вам нужно обратиться к более сложным инструментам.
В Git отсутствуют инструменты для переписывания истории, но вы можете использовать команду rebase
, чтобы перебазировать группу коммитов туда же на HEAD, где они были изначально, вместо перемещения их в другое место.
С помощью интерактивного режима команды rebase
, вы можете останавливаться после каждого нужного вам коммита и изменять сообщения, добавлять файлы или делать что-то другое, что вам нужно.
Вы можете запустить rebase
в интерактивном режиме, добавив опцию -i
к git rebase
.
Вы должны указать, какие коммиты вы хотите изменить, передав команде коммит, на который нужно выполнить перебазирование.
Например, если вы хотите изменить сообщения последних трёх коммитов, или сообщение какого-то одного коммита этой группы, то передайте как аргумент команде git rebase -i
родителя последнего коммита, который вы хотите изменить — HEAD~2^
или HEAD~3
.
Может быть, проще будет запомнить ~3
, так как вы хотите изменить последние три коммита; но не забывайте, что вы, в действительности, указываете четвертый коммит с конца — родителя последнего коммита, который вы хотите изменить:
Напомним, что это команда перебазирования — каждый коммит, входящий в диапазон HEAD~3..HEAD
, будет изменён вне зависимости от того, изменили вы сообщение или нет.
Не включайте в такой диапазон коммит, который уже был отправлен на центральный сервер: сделав это, вы можете запутать других разработчиков, предоставив вторую версию одних и тех же изменений.
Выполнение этой команды отобразит в вашем текстовом редакторе список коммитов, в нашем случае, например, следующее:
pick f7f3f6d Change my name a bit
pick 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file
# Rebase 710f0f8..a5f4a0d onto 710f0f8
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# 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
Важно отметить, что коммиты перечислены в порядке, противоположном порядку, который вы обычно видите при использовании команды log
.
Если вы выполните log
, то увидите следующее:
$ git log --pretty=format:"%h %s" HEAD~3..HEAD
a5f4a0d Add cat-file
310154e Update README formatting and add blame
f7f3f6d Change my name a bit
Обратите внимание на обратный порядок.
Команда rebase
в интерактивном режиме предоставит вам скрипт, который она будет выполнять.
Она начнет с коммита, который вы указали в командной строке (HEAD~3
) и повторит изменения, внесённые каждым из коммитов, сверху вниз.
Наверху отображается самый старый коммит, а не самый новый, потому что он будет повторен первым.
Вам необходимо изменить скрипт так, чтобы он остановился на коммите, который вы хотите изменить.
Для этого измените слово pick
на слово edit
напротив каждого из коммитов, после которых скрипт должен остановиться.
Например, для изменения сообщения только третьего коммита, измените файл следующим образом:
edit f7f3f6d Change my name a bit
pick 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file
Когда вы сохраните сообщение и выйдете из редактора, Git переместит вас к самому раннему коммиту из списка и вернёт вас в командную строку со следующим сообщением:
$ git rebase -i HEAD~3
Stopped at f7f3f6d... Change my name a bit
You can amend the commit now, with
git commit --amend
Once you're satisfied with your changes, run
git rebase --continue
Эти инструкции говорят вам в точности то, что нужно сделать.
Выполните:
Измените сообщение коммита и выйдите из редактора.
Затем выполните:
Эта команда автоматически применит два оставшиеся коммита и завершится.
Если вы измените pick
на edit
в других строках, то можете повторить эти шаги для соответствующих коммитов.
Каждый раз Git будет останавливаться, позволяя вам исправить коммит, и продолжит, когда вы закончите.
Упорядочивание коммитов
Вы также можете использовать интерактивное перебазирование для изменения порядка или полного удаления коммитов.
Если вы хотите удалить коммит «Add cat-file» и изменить порядок, в котором были внесены два оставшихся, то вы можете изменить скрипт перебазирования с такого:
pick f7f3f6d Change my name a bit
pick 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file
на такой:
pick 310154e Update README formatting and add blame
pick f7f3f6d Change my name a bit
Когда вы сохраните скрипт и выйдете из редактора, Git переместит вашу ветку на родителя этих коммитов, применит 310154e
, затем f7f3f6d
и после этого остановится.
Вы, фактически, изменили порядок этих коммитов и полностью удалили коммит «Add cat-file».
Объединение коммитов
С помощью интерактивного режима команды rebase
также можно объединить несколько коммитов в один.
Git добавляет полезные инструкции в сообщение скрипта перебазирования:
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# 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
Если вместо «pick» или «edit» вы укажете «squash», Git применит изменения из текущего и предыдущего коммитов и предложит вам объединить их сообщения.
Таким образом, если вы хотите из этих трёх коммитов сделать один, вы должны изменить скрипт следующим образом:
pick f7f3f6d Change my name a bit
squash 310154e Update README formatting and add blame
squash a5f4a0d Add cat-file
Когда вы сохраните скрипт и выйдете из редактора, Git применит изменения всех трёх коммитов и затем вернёт вас обратно в редактор, чтобы вы могли объединить сообщения коммитов:
# This is a combination of 3 commits.
# The first commit's message is:
Change my name a bit
# This is the 2nd commit message:
Update README formatting and add blame
# This is the 3rd commit message:
Add cat-file
После сохранения сообщения, вы получите один коммит, содержащий изменения всех трёх коммитов, существовавших ранее.
Разбиение коммита
Разбиение коммита отменяет его и позволяет затем по частям индексировать и фиксировать изменения, создавая таким образом столько коммитов, сколько вам нужно.
Например, предположим, что вы хотите разбить средний коммит на два.
Вместо одного коммита «Update README formatting and add blame» вы хотите получить два разных: первый — «Update README formatting», и второй — «Add blame».
Вы можете добиться этого, изменив в скрипте rebase -i
инструкцию для разбиваемого коммита на «edit»:
pick f7f3f6d Change my name a bit
edit 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file
Затем, когда скрипт вернёт вас в командную строку, вам нужно будет отменить индексацию изменений этого коммита, и создать несколько коммитов на основе этих изменений.
Когда вы сохраните скрипт и выйдете из редактора, Git переместится на родителя первого коммита в вашем списке, применит первый коммит (f7f3f6d
), применит второй (310154e
), и вернёт вас в консоль.
Здесь вы можете отменить коммит с помощью команды git reset HEAD^
, которая, фактически, отменит этот коммит и удалит из индекса изменённые файлы.
Теперь вы можете добавлять в индекс и фиксировать файлы, пока не создадите требуемые коммиты, а после этого выполнить команду git rebase --continue
:
$ git reset HEAD^
$ git add README
$ git commit -m 'Update README formatting'
$ git add lib/simplegit.rb
$ git commit -m 'Add blame'
$ git rebase --continue
Git применит последний коммит (a5f4a0d
) из скрипта, и ваша история примет следующий вид:
$ git log -4 --pretty=format:"%h %s"
1c002dd Add cat-file
9b29157 Add blame
35cfb2b Update README formatting
f7f3f6d Change my name a bit
И снова, при этом изменились SHA-1 хеши всех коммитов в вашем списке, поэтому убедитесь, что ни один коммит из этого списка ранее не был отправлен в общий репозиторий.
Обратите внимание, что последний коммит в списке (f7f3f6d
) не изменился.
Несмотря на то, что коммит был в списке перебазирования, он был отмечен как «pick» и применён до применения перебазирования, поэтому Git оставил его нетронутым.
Удаление коммита
Если вы хотите избавиться от какого-либо коммита, то удалить его можно во время интерактивного перебазирования rebase -i
.
Напишите слово «drop» перед коммитом, который хотите удалить, или просто удалите его из списка:
pick 461cb2a This commit is OK
drop 5aecc10 This commit is broken
Из-за того, как Git создаёт объекты коммитов, удаление или изменение коммита влечёт за собой перезапись всех последующих коммитов.
Чем дальше вы вернётесь в историю ваших коммитов, тем больше коммитов потребуется переделать.
Это может вызвать множество конфликтов слияния, особенно если у вас много последующих коммитов, которые зависят от удалённого.
Если во время подобного перебазирования вы поняли, что это была не очень хорошая идея, то всегда можно остановиться.
Просто выполните команду git rebase --abort
и ваш репозиторий вернётся в то состояние, в котором он был до начала перебазирования.
Если вы завершили перебазирование, а затем решили, что полученный результат это не то, что вам нужно — воспользуйтесь командой git reflog
, чтобы восстановить предыдущую версию вашей ветки.
Дополнительную информацию по команде reflog
можно найти в разделе Восстановление данных главы 10.
Примечание |
Дрю Дево создал практическое руководство с упражнениями по использованию |
Продвинутый инструмент: filter-branch
Существует ещё один способ переписывания истории, который вы можете использовать при необходимости изменить большое количество коммитов каким-то программируемым способом — например, изменить глобально ваш адрес электронной почты или удалить файл из всех коммитов.
Для этого существует команда filter-branch
, и она может изменять большие периоды вашей истории, поэтому вы, возможно, не должны её использовать кроме тех случаев, когда ваш проект ещё не стал публичным и другие люди ещё не имеют наработок, основанных на коммитах, которые вы собираетесь изменить.
Однако, эта команда может быть очень полезной.
Далее вы ознакомитесь с несколькими обычными вариантами использованиями этой команды, таким образом, вы сможете получить представление о том, на что она способна.
Внимание |
Команда |
Удаление файла из каждого коммита
Такое случается довольно часто.
Кто-нибудь случайно зафиксировал огромный бинарный файл, неосмотрительно выполнив git add .
, и вы хотите отовсюду его удалить.
Возможно, вы случайно зафиксировали файл, содержащий пароль, а теперь хотите сделать ваш проект общедоступным.
В общем, утилиту filter-branch
вы, вероятно, захотите использовать, чтобы привести к нужному виду всю вашу историю.
Для удаления файла passwords.txt
из всей вашей истории вы можете использовать опцию --tree-filter
команды filter-branch
:
$ git filter-branch --tree-filter 'rm -f passwords.txt' HEAD
Rewrite 6b9b3cf04e7c5686a9cb838c3f36a8cb6a0fc2bd (21/21)
Ref 'refs/heads/master' was rewritten
Опция --tree-filter
выполняет указанную команду после переключения на каждый коммит и затем повторно фиксирует результаты.
В данном примере, вы удаляете файл passwords.txt
из каждого снимка вне зависимости от того, существует он или нет.
Если вы хотите удалить все случайно зафиксированные резервные копии файлов, созданные текстовым редактором, то вы можете выполнить нечто подобное git filter-branch --tree-filter 'rm -f *~' HEAD
.
Вы можете посмотреть, как Git изменит деревья и коммиты, а затем уже переместить указатель ветки.
Как правило, хорошим подходом будет выполнение всех этих действий в тестовой ветке и, после проверки полученных результатов, установка на неё указателя основной ветки.
Для выполнения filter-branch
на всех ваших ветках, вы можете передать команде опцию --all
.
Установка подкаталога как корневого каталога проекта
Предположим, вы выполнили импорт из другой системы контроля версий и получили в результате подкаталоги, которые не имеют никакого смысла (trunk
, tags
и так далее).
Если вы хотите сделать подкаталог trunk
корневым для каждого коммита, команда filter-branch
может помочь вам в этом:
$ git filter-branch --subdirectory-filter trunk HEAD
Rewrite 856f0bf61e41a27326cdae8f09fe708d679f596f (12/12)
Ref 'refs/heads/master' was rewritten
Теперь вашим новым корневым каталогом проекта будет являться подкаталог trunk
.
Git также автоматически удалит коммиты, которые не затрагивали этот подкаталог.
Глобальное изменение адреса электронной почты
Ещё один типичный случай возникает, когда вы забыли выполнить git config
для настройки своего имени и адреса электронной почты перед началом работы, или, возможно, хотите открыть исходные коды вашего рабочего проекта и изменить везде адрес вашей рабочей электронной почты на персональный.
В любом случае вы можете изменить адрес электронный почты сразу в нескольких коммитах с помощью команды filter-branch
.
Вы должны быть осторожны, чтобы изменить только свои адреса электронной почты, для этого используйте опцию --commit-filter
:
$ git filter-branch --commit-filter '
if [ "$GIT_AUTHOR_EMAIL" = "schacon@localhost" ];
then
GIT_AUTHOR_NAME="Scott Chacon";
GIT_AUTHOR_EMAIL="schacon@example.com";
git commit-tree "$@";
else
git commit-tree "$@";
fi' HEAD
Эта команда пройдёт по всем коммитам и установит в них ваш новый адрес.
Так как коммиты содержат значения SHA-1-хешей их родителей, эта команда изменяет хеш SHA-1 каждого коммита в вашей истории, а не только тех, которые соответствовали адресам электронной почты.
В этом разделе мы обсудим доступные стратегии и команды Git для выполнения отмены изменений. Прежде всего необходимо отметить, что в Git не существует традиционной системы отмены, как в текстовых редакторах. Лучше воздержаться от сопоставления операций Git с какой бы то ни было традиционной концепцией отмены изменений. Кроме того, Git имеет собственную систему терминов для операций отмены, и в обсуждении лучше всего использовать их. В числе таких терминов — сброс (reset), возврат (revert), переключение (checkout), очистка (clean) и другие.
Git можно рассматривать как инструмент для управления временной шкалой. Коммиты — это снимки моментов времени или точек интереса на временной шкале истории проекта. Кроме того, с помощью веток можно управлять несколькими временными шкалами. Когда вы выполняете операцию отмены в Git, вы, как правило, перемещаетесь назад во времени или на другую временную шкалу, где ошибок не было.
Это руководство предоставляет все необходимые навыки для работы с предыдущими версиями проекта по разработке программного обеспечения. Сначала мы рассмотрим, как исследовать старые коммиты, а затем изучим разницу между отменой публичных коммитов в истории проекта и сбросом неопубликованных изменений на локальном компьютере.
Поиск утерянного: просмотр старых коммитов
В основе любой системы управления версиями лежит идея хранения «безопасных» копий проекта, чтобы у разработчиков не возникало опасений безвозвратно испортить базу кода. Когда в проекте сохранена история коммитов, можно повторно оценивать и анализировать любые ранее выполненные коммиты. Один из лучших инструментов для просмотра истории репозитория Git — команда git log
. В примере ниже мы используем команду git log
для получения последних коммитов популярной графической библиотеки с открытым исходным кодом.
git log --oneline
e2f9a78fe Replaced FlyControls with OrbitControls
d35ce0178 Editor: Shortcuts panel Safari support.
9dbe8d0cf Editor: Sidebar.Controls to Sidebar.Settings.Shortcuts. Clean up.
05c5288fc Merge pull request #12612 from TyLindberg/editor-controls-panel
0d8b6e74b Merge pull request #12805 from harto/patch-1
23b20c22e Merge pull request #12801 from gam0022/improve-raymarching-example-v2
fe78029f1 Fix typo in documentation
7ce43c448 Merge pull request #12794 from WestLangley/dev-x
17452bb93 Merge pull request #12778 from OndrejSpanel/unitTestFixes
b5c1b5c70 Merge pull request #12799 from dhritzkiv/patch-21
1b48ff4d2 Updated builds.
88adbcdf6 WebVRManager: Clean up.
2720fbb08 Merge pull request #12803 from dmarcos/parentPoseObject
9ed629301 Check parent of poseObject instead of camera
219f3eb13 Update GLTFLoader.js
15f13bb3c Update GLTFLoader.js
6d9c22a3b Update uniforms only when onWindowResize
881b25b58 Update ProjectionMatrix on change aspect
Каждый коммит имеет уникальный идентифицирующий хеш SHA-1. Эти идентификаторы используются для перемещения по временной шкале коммитов и возвращения к коммитам. По умолчанию git log
показывает только коммиты текущей выбранной ветки. Но не исключено, что искомый коммит находится в другой ветке. Для просмотра всех коммитов во всех ветках используется команда git log --branches=*
. Команда git branch
используется для просмотра и посещения других веток. Так, команда git branch -a
возвращает список имен всех известных веток. Просмотреть весь журнал коммитов одной из этих веток можно с помощью команды git log <имя_ветки>
.
После того как вы нашли ссылку на нужный коммит в истории, для перехода к нему можно использовать команду git checkout
. Команда git checkout
— это простой способ «загрузить» любой из этих сохраненных снимков на компьютер разработчика. При стандартном процессе разработки указатель HEAD
обычно указывает на главную ветку main
или другую локальную ветку. Но при переключении на предыдущий коммит HEAD
указывает уже не на ветку, а непосредственно на сам коммит. Такая ситуация называется состоянием открепленного указателя HEAD
, и ее можно представить так:
Переход к старой версии файла не перемещает указатель HEAD
. Он остается в той же ветке и в том же коммите, что позволяет избежать открепления указателя HEAD. После этого можно выполнить коммит старой версии файла в новый снимок состояния, как и в случае других изменений. Соответственно, такое использование команды git checkout
применительно к файлу позволяет откатиться к прежней версии отдельного файла. Для получения дополнительной информации об этих двух режимах посетите страницу команды git checkout
.
Просмотр старых версий
В этом примере предполагается, что вы начали разработку безумного эксперимента, но не уверены, хотите его сохранить или нет. Чтобы принять решение, вы хотите взглянуть на состояние проекта до начала эксперимента. Прежде всего, нужно найти идентификатор редакции, которую вы хотите просмотреть.
Допустим, история вашего проекта выглядит примерно так:
b7119f2 Continue doing crazy things
872fa7e Try something crazy
a1e8fb5 Make some important changes to hello.txt
435b61d Create hello.txt
9773e52 Initial import
Для просмотра коммита «Make some important changes to hello.txt» можно использовать команду git checkout
в следующем виде:
Это приведет к тому, что ваш рабочий каталог будет в точности соответствовать состоянию коммита a1e8fb5
. Вы можете просматривать файлы, компилировать проект, запускать тесты и даже редактировать файлы, не боясь потерять текущее состояние проекта. Никакие внесенные здесь изменения не будут сохранены в репозитории. Чтобы продолжить разработку, необходимо вернуться к текущему состоянию проекта:
Предположим, вы ведете разработку в главной ветке main
по умолчанию. При каждом возвращении в ветку main
можно использовать команду git revert
или git reset
для отмены нежелательных изменений.
Отмена коммита снимка
Технически существует несколько стратегий отмены коммитов. В дальнейших примерах предполагается, что история коммитов выглядит следующим образом:
git log --oneline
872fa7e Try something crazy
a1e8fb5 Make some important changes to hello.txt
435b61d Create hello.txt
9773e52 Initial import
Займемся отменой коммита 872fa7e Try something crazy
. Возможно, безумный эксперимент зашел слишком далеко.
Отмена коммита с помощью git checkout
С помощью команды git checkout
мы можем перейти к предыдущему коммиту , a1e8fb5,
и вернуть репозиторий в состояние, предшествовавшее этому безумному коммиту. Переход к отдельному коммиту переведет репозиторий в состояние открепленного указателя HEAD. Работа при этом перестает принадлежать какой-либо из веток. При открепленном указателе HEAD все новые коммиты будут оставаться без родителя, пока вы не вернете ветки в положенное состояние. «Сборщик мусора» в Git удаляет коммиты без родителя. Этот сервис работает с определенными интервалами и удаляет такие коммиты без возможности восстановления. Чтобы такие коммиты не были удалены «сборщиком мусора», перед их выполнением нужно убедиться, что мы работаем в ветке.
При наличии открепленного указателя HEAD можно выполнить команду git checkout -b new_branch_without_crazy_commit
. Она создаст новую ветку с именем new_branch_without_crazy_commit
и совершит переход в это состояние. Теперь репозиторий находится на новой временной шкале, где коммита 872fa7e
не существует. На этом этапе мы можем продолжить работу в новой ветке, где коммита 872fa7e
не существует и его можно считать «отмененным». К сожалению, если вам нужна предыдущая ветка (возможно, это главная ветка main
), такая стратегия не подходит. Поэтому рассмотрим другие стратегии отмены. Более детальную информацию и примеры см. в нашей подробной статье о git checkout
.
Отмена публичного коммита с помощью git revert
Предположим, мы вернулись к исходному примеру истории коммитов. Истории, в которую входит коммит 872fa7e
. В этот раз попробуем отмену путем обратной операции. При исполнении команды git revert HEAD
Git создаст новый коммит с операцией, обратной последнему коммиту. В текущую историю ветки будет добавлен новый коммит, и она будет выглядеть следующим образом:
git log --oneline
e2f9a78 Revert "Try something crazy"
872fa7e Try something crazy
a1e8fb5 Make some important changes to hello.txt
435b61d Create hello.txt
9773e52 Initial import
На этом этапе мы снова технически «отменили» коммит 872fa7e
. Хотя коммит 872fa7e
по-прежнему существует в истории, новый коммит e2f9a78
отменил изменения 872fa7e
. В отличие от нашей предыдущей стратегии переключения с помощью команды checkout, мы можем продолжить работать с этой же веткой, поэтому данная стратегия является удовлетворительной. Это идеальный способ отмены при работе в открытых общих репозиториях, однако если у вас есть требование вести минимальную «очищенную» историю Git, эта стратегия может не подойти.
Отмена коммита с помощью git reset
Рассмотрение этой стратегии отмены мы продолжим на нашем рабочем примере. Команда git reset
— это расширяемая команда с разнообразными функциями и вариантами использования. Если мы выполним команду git reset --hard a1e8fb5
, история коммитов будет сброшена до указанного коммита. Просмотр истории коммитов с помощью команды git log
теперь будет выглядеть так:
git log --oneline
a1e8fb5 Make some important changes to hello.txt
435b61d Create hello.txt
9773e52 Initial import
Вывод команды log показывает, что коммиты e2f9a78
и 872fa7e
больше не существуют в истории. На этом этапе мы можем продолжить работу и создавать новые коммиты так, словно «безумных» коммитов никогда не было. Этот метод отмены изменений оставляет историю максимально чистой. Отмена с помощью команды reset отлично подходит для локальных изменений, но при работе в общем удаленном репозитории создает сложности. Если у нас есть общий удаленный репозиторий, в котором с помощью команды push опубликован коммит 872fa7e
, и мы попытаемся выполнить команду git push
для ветки, в которой с помощью команды reset была сброшена история, система Git обнаружит это и выдаст ошибку. Git будет считать, что публикуемая ветка не была обновлена, поскольку в ней отсутствуют коммиты. В таких случаях лучше использовать отмену с помощью команды git revert
.
Отмена последнего коммита
В предыдущем разделе мы рассмотрели различные стратегии отмены коммитов. Эти стратегии также можно применять и к последнему коммиту. Однако иногда последний коммит можно не удалять и не сбрасывать. Например, если вы просто сделали коммит преждевременно. В этом случае его можно исправить. После того как вы внесете дополнительные изменения в рабочий каталог и добавите их в раздел проиндексированных файлов с помощью команды git add
, выполните команду git commit --amend
. При этом Git откроет настроенный системный редактор, где вы сможете изменить комментарий к последнему коммиту. Новые изменения будут добавлены в исправленный коммит.
Отмена неотправленных изменений
Пока не выполнен коммит изменений в историю репозитория, они находятся в разделе проиндексированных файлов и в рабочем каталоге. Вам может потребоваться отменить изменения в этих двух областях. Раздел проиндексированных файлов и рабочий каталог являются внутренними механизмами управления состоянием Git. Подробную информацию о том, как работают эти два механизма, см. на странице git reset
, где приводится их подробное описание.
Рабочий каталог
Рабочий каталог обычно синхронизируется с локальной файловой системой. Чтобы отменить изменения в рабочем каталоге, можно просто отредактировать файлы с помощью привычного редактора. Git имеет два инструмента для управления рабочим каталогом. Это команда git clean
— удобная утилита для отмены изменений в рабочем каталоге, и команда git reset
, которую можно вызвать с параметрами --mixed
или --hard
, чтобы сбросить изменения в рабочем каталоге.
Раздел проиндексированных файлов
Команда git add
используется для добавления изменений в раздел проиндексированных файлов. Команда git reset
предназначена главным образом для отмены изменений в разделе проиндексированных файлов. Команда reset с параметром --mixed
перемещает все ожидающие изменения из раздела проиндексированных файлов обратно в рабочий каталог.
Отмена публичных изменений
При командной работе в удаленных репозиториях необходимо подходить к отмене изменений с особой осторожностью. Команда git reset
, как правило, считается методом локальной отмены. Ее следует использовать для отмены изменений в частной ветке. Она безопасно изолирует удаление коммитов от других веток, которые могут использоваться другими разработчиками. Проблемы возникают, когда команда reset выполняется в общей ветке и затем эта ветка удаленно публикуется с помощью команды git push
. В этом случае Git блокирует выполнение команды push и сообщает, что публикуемая ветка устарела, поскольку в ней отсутствуют коммиты, которые есть в удаленной ветке.
Предпочтительная команда для отмены общей истории коммитов — git revert
. Команда revert безопаснее, чем reset, так как она не удаляет коммиты из общей истории. Команда revert сохраняет отменяемые вами коммиты и создает новый коммит с операцией, обратной последнему коммиту. Этот метод можно безопасно применять в общих распределенных рабочих средах, так как удаленный разработчик может выполнить пул ветки и получить новый коммит, который отменяет его нежелательный коммит.
Резюме
Мы рассмотрели множество общих стратегий отмены изменений в Git. Важно помнить, что отменить изменения в проекте Git можно несколькими способами. Кроме того, здесь были затронуты и более сложные темы, которые подробно рассматриваются на страницах, посвященных соответствующим командам Git. Наиболее часто используемые инструменты для отмены — это команды git checkout, git revert
и git reset
. Вот несколько ключевых моментов, о которых следует помнить.
- Обычно после коммита внесенные изменения отменить невозможно
- Используйте
git checkout
для переходов и просмотра истории коммитов - Команда
git revert
— лучший инструмент для отмены общих публичных изменений. - Команду
git reset
лучше всего использовать для отмены локальных частных изменений.
Помимо основных команд отмены, мы рассмотрели другие команды Git: git log
для поиска потерянных коммитов, git clean
для отмены изменений, не подтвержденных коммитами, и git add
для изменения индексирования.
Каждая из этих команд имеет собственную подробную документацию. Чтобы узнать больше о той или иной команде, пройдите по соответствующим ссылкам.
Система контроля версий — неотъемлемый инструмент в рабочем процессе любого разработчика. С его помощью вы можете сохранять состояния вашего проекта, переключаться между ними и поддерживать разработку несколькими людьми. Самой популярной системой контроля версий на текущий момент является git.
В этом посте мы не будем изучать основы git — есть множество туториалов, в том числе интерактивных. Предполагается, что вы знаете как создавать коммиты, просматривать историю и выполнять другие базовые команды. Мы займемся не самой распространенной проблемой при использовании git. Бывают ситуации, когда по невнимательности или чьей-то некомпетентности в репозитории оказывается нежелательная информация. Возможно даже, что после этого она была удалена и в текущей версии проекта ее нет, но она осталась в истории git и, откатившись к определенной версии, ее можно будет восстановить. Изменять историю мы будем с помощью git-команды rebase в режиме interactive.
Какие проблемы будем решать
Для большей реалистичности мы решим эту задачу на примере реальных проблем, которые могут возникнуть в вашей программистской практике.
- Исправление опечатки в commit message. Это достаточно безобидный вариант, но он может сильно раздражать, если вы регулярно просматриваете историю своего репозитория. Схожая ситуация, когда вы принципиально неправильно описали коммит, и тем самым можете сбить столку ваших коллег или сообщество в случае open source проекта. Главное осознать эту ошибку до того, как она навредит кому-либо, а как исправить ее мы рассмотрим далее.
- Удаление приватной информации из файла. Если в вашем публичном репозитории имеется информация, с помощью которой злоумышленник может навредить вам и вашем бизнесу, это большая проблема. Причем, даже если вы заметили, что разместили ее в публичном доступе и после этого создали новый коммит, в котором ее удалили, это не защитит вас, так как в истории она сохранится.
- Удаление ненужных файлов. Это может быть как частный случай предыдущей проблемы, так и вариант, когда вы поместили в репозиторий файлы, с помощью которых вам нельзя будет навредить, но и не несущие никакой пользы другим. Отсутствие пользы в данном случае приравнивается лишнему времени при клонировании репозитория и месту на диске. Например, это могут быть логи, файлы IDE или временный вывод при построении или тестировании проекта. Для предотвращения такой проблемы следует использовать файл .gitignore, но бывает, что вы что-то не заметили и ненужные файлы уже давно поселились в истории репозитория.
Предостережение
Перезапись истории — очень опасный прием в том случае, если вы изменяете коммиты, которые уже находятся в публичном репозитории. Если есть разработчики, которые пользуются вашим проектом, то обновление истории в удаленном репозитории может сильно осложнить им жизнь, когда они получат несогласованные изменения. Поэтому используйте интерактивный rebase и любые другие способы изменения истории на коммитах, которые существуют только в вашем локальном репозитории либо, если вы знаете всех людей, которые занимаются проектом и вы можете согласовать с ними этот процесс.
Исправить опечатку в commit message
Для демонстрации возможностей interactive rebase я создал тестовый репозиторий, в котором буду моделировать описанные выше проблемы.
Начнем с ситуации, когда вы сделали коммит и сразу осознали, что допустили ошибку в его message:
* 0be0764 | add info about tet in readme (HEAD -> commit-message-typo) [Alexey Kalina]
* 972078a | add test on division by zero [Alexey Kalina]
* 005fd5c | init (master) [Alexey Kalina]
В этом случае использование interactive rebase даже не потребуется. Все, что вам нужно — использовать команду git commit —amend, которая позволяет изменить предыдущий коммит. После вызова команды откроется дефолтный редактор, в котором нужно исправить текст сообщения, сохраниться и выйти.
add info about test in readme
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Sun Nov 3 11:31:50 2019 +0300
#
# On branch commit-message-typo
# Changes to be committed:
# modified: README.md
#
Аналогично решаются и остальные проблемы в случае, когда неудачный коммит — последний (перед вызовом git commit —amend сделайте необходимые изменения и добавьте файлы в stage-область). Поэтому далее будем рассматривать только вариант, когда после проблемного коммита были и другие.
Итак, после коммита с опечаткой было сделано несколько других изменений:
* 27917ff | update readme (HEAD -> commit-message-typo) [Alexey Kalina]
* 84e261c | add new calculation test [Alexey Kalina]
* 0be0764 | add info about tet in readme [Alexey Kalina]
* 972078a | add test on division by zero [Alexey Kalina]
* 005fd5c | init (master) [Alexey Kalina]
Здесь в дело вступает интерактивный rebase. Команда rebase позволяет перемещать коммиты между ветками, в данном случае мы будем их перемещать на то же самое место. А благодаря интерактивному режиму (флаг —interactive/-i), каждый из перемещаемых коммитов можно редактировать, изменяя текст сообщения, используемые файлы и их содержимое.
Необходимо вызвать rebase на коммите, предшествующем тому, в котором нужно, что-то изменить. Для этого нужно либо передать хэш этого коммита (972078a), либо насколько он отстает от текущего (HEAD~3). Я предпочитаю первый вариант, поскольку история может быть гораздо длиннее, а считать коммиты не хочется.
git rebase -i 972078a
Вновь откроется дефолтный редактор, в котором необходимо выбрать коммиты для изменения.
pick 0be0764 add info about tet in readme
pick 84e261c add new calculation test
pick 27917ff update readme
# Rebase 972078a..27917ff onto 972078a (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# 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
Как можно увидеть, вся необходимая информация описана в этом файле. Изначально все коммиты берутся без изменений (об этом говорит слово pick). Для тех коммитов, которые требуют исправлений, замените pick на соответствующий вариант. Так как мы хотим изменить сообщение коммита 0be0764, заменим pick на reword либо в сокращенном варианте – r:
reword 0be0764 add info about tet in readme
После того, как вы сохранитесь и выйдете, откроется редактор с точно таким же содержимым, как в случае commit —amend. Остается только изменить текст сообщения и выйти. История отредактирована:
* dcb51d5 | update readme (HEAD -> commit-message-typo) [Alexey Kalina]
* 9a01464 | add new calculation test [Alexey Kalina]
* 6212924 | add info about test in readme [Alexey Kalina]
* 972078a | add test on division by zero [Alexey Kalina]
* 005fd5c | init (master) [Alexey Kalina]
Удалить приватную информацию из файла
Допустим, в какой-то момент мы добавили в публичный доступ логин и пароль к нашему сервису. Так выглядит история:
* 11b5cef | add version settings (HEAD -> private-info) [Alexey Kalina]
* b3b8d34 | add test settings [Alexey Kalina]
* b94ffdb | add login and password [Alexey Kalina]
* bfe4560 | add config [Alexey Kalina]
* 005fd5c | init (master) [Alexey Kalina]
Снова воспользуемся interactive rebase. Вызов на коммите bfe4560, откроет редактор со следующим содержимым:
pick b94ffdb add login and password
pick df4044b add test settings
pick d9f0ff4 add version settings
У нас есть два варианта. Если коммит b94ffdb содержит и другие изменения помимо нежелательных, то следует заменить pick на edit, что означает исправление данного коммита, и переименовать коммит соответствующим образом. В случае, если это единственная правка в коммите, то его следует удалить (для этого используйте слово drop).
Если вы решили исправить коммит, то после закрытия редактора, репозиторий окажется в состоянии соответствующем этому коммиту. Сделайте изменения и выполните команду git commit —amend, тем самым переписав коммит. Далее выполните команду git rebase —continue, которая продолжает процесс rebase, перемещаясь вверх по истории. Если изменения, которые вы произвели пересекаются с изменениями в следующих коммитах, вам будет предложено исправить конфликты. Например:
{
<<<<<<< HEAD
"configuration": "prod"
=======
"configuration": "prod",
"login": "admin",
"password": "password",
"test_dir": "test/"
>>>>>>> b3b8d34... add test settings
}
Исправьте состояние файла на текущем коммите:
{
"configuration": "prod",
"test_dir": "test/"
}
Далее добавьте файл в stage-область с помощью git add и продолжите процесс командой git rebase —continue. После завершения rebase получаем вывод:
Successfully rebased and updated refs/heads/private-info
В случае удаления коммита процесс будет аналогичным.
Избавиться от ненужных файлов
У нас имеется репозиторий, в который в определенный момент был добавлен файл с логами. После этого разработка продолжалась, причем этот файл тоже менялся. Потом мы наконец заметили, что тащим с собой логи, и удалили их. После этого мы все же решили, что это не лучшее решение и будет правильнее удалить логи из всего репозитория.
* e22c934 | delete logs (HEAD -> log-files) [Alexey Kalina]
* 4c5f6eb | add another important feature [Alexey Kalina]
* 6618972 | add important feature [Alexey Kalina]
* 005fd5c | init (master) [Alexey Kalina]
Для этого для начала найдем, в каких коммитах файл с логами менялся (в первую очередь нас интересует, когда он появился). Это можно сделать с помощью команды git log. Укажите путь к файлу, который ищем (используйте —follow, если файл мог быть переименован).
git log -- 0.log
Результат:
* e22c934 | delete logs (HEAD -> log-files) [Alexey Kalina]
* 4c5f6eb | add another important feature [Alexey Kalina]
* 6618972 | add important feature [Alexey Kalina]
Теперь, зная к какому коммиту откатываться, воспользуемся интерактивным rebase.
git rebase -i 005fd5c
Заменим pick на edit для коммита 6618972 и укажем drop вместо pick для коммита e22c934. После этого мы перенесемся в состояние репозитория на коммите, в котором впервые добавили ненужный файл. Здесь нам необходимо физически удалить файл с диска и закоммитить это изменение с помощью git commit —amend. Тут не стоит бояться, что файл пропадет. То, что мы удалили этот файл в промежуточном состоянии никак не повлияет на то, что он останется у вас на диске в итоговом состоянии. Продолжаем rebase с помощью git rebase —continue.
Далее во всех коммитах, в которых этот файл изменялся, необходимо удалять его из stage-области (git reset HEAD path_to_file) и продолжать процесс. В итоге вы очистите историю от ненужного файла, а сам файл в своем конечном состоянии останется на диске.
Другие возможности interactive rebase
С помощью интерактивного rebase можно существенно изменить историю git-репозитория. Из других наиболее полезных возможностей этой команды я бы выделил объединение и разделение коммитов. Благодаря этим приемам историю можно сделать более аккуратной. С полным списком возможностей можно ознакомиться в документации.
Written on
November
3rd
,
2019
by
Alexey Kalina
Feel free to share!