Прим. перев.: На днях в блоге для инженеров любимого нами проекта GitLab появилась небольшая, но весьма полезная заметка с инструкциями, которые помогают сохранить время и нервы в случае различных проблем, случающихся по мере работы с Git. Вряд ли они будут новы для опытных пользователей, но обязательно найдутся и те, кому они пригодятся. А в конец этого материала мы добавили небольшой бонус от себя. Хорошей всем пятницы!
Все мы делаем ошибки, особенно при работе с такими сложными системами, как Git. Но помните: Git happens!
Если вы только начинаете путь с Git, обучитесь основам работы с ним в командной строке. А здесь я расскажу о том, как можно исправить шесть наиболее распространённых ошибок в Git.
1. Упс… Я ошибся в сообщении к последнему коммиту
После нескольких часов кодинга легко допустить ошибку в сообщении коммита. К счастью, это легко исправить:
git commit --amend
С этой командой откроется текстовый редактор и позволит внести изменения в сообщение к последнему коммиту. И никто не узнает, что вы написали «addded» с тремя «d».
2. Упс… Я забыл добавить файл к последнему коммиту
Другая популярная ошибка в Git — слишком поспешный коммит. Вы забыли добавить файл, забыли его сохранить или должны внести небольшое изменение, чтобы коммит стал осмысленным? Вашим другом снова будет
--amend
.Добавьте недостающий файл и выполните эту верную команду:
git add missed-file.txt
git commit --amend
Теперь вы можете либо откорректировать сообщение, либо просто сохранить его в прежнем виде (с добавленным файлом).
3. Упс… Я добавил файл, который не должен быть в этом репозитории
Но что, если у вас обратная ситуация? Что, если вы добавили файл, который не хотите коммитить? Обманчивый ENV-файл, директорию сборки или фото с котом, что было случайно сохранено в неправильном каталоге… Всё решаемо.
Если вы сделали только stage для файла и ещё не коммитнули его, всё делается через простой reset нужного файла (находящегося в stage):
git reset /assets/img/misty-and-pepper.jpg
Если же вы всё-таки коммитнули изменение, потребуется дополнительный предварительный шаг:
git reset --soft HEAD~1
git reset /assets/img/misty-and-pepper.jpg
rm /assets/img/misty-and-pepper.jpg
git commit
Коммит будет откачен, картинка удалена, а затем сделан новый коммит.
Прим. перев.: Как замечено в комментариях к оригинальной статье, эту проблему тоже можно решить с помощью уже упомянутого
--amend
. По всей видимости, данным пунктом автор хотел показать, какие ещё есть способы изменения истории коммитов для исправления ошибки.4. Упс… Я коммитнул изменения в master
Итак, вы работаете над новой фичей и поспешили, забыв создать новую ветку для неё. Вы уже коммитнули кучу файлов и все эти коммиты оказались в master'е. К счастью, GitLab может предотвращать push'ы прямо в master. Поэтому мы можем откатить все нужные изменения в новую ветку следующими тремя командами:
Примечание: Убедитесь, что сначала коммитнули или stash'нули свои изменения — иначе все они будут утеряны!
git branch future-brunch
git reset HEAD~ --hard
git checkout future-brunch
Будет создана новая ветка, в master'е — произведён откат до состояния, в котором он был до ваших изменений, а затем сделан checkout новой ветки со всеми вашими изменениями.
5. Упс… Я сделал ошибку в названии ветки
Самые внимательные могли заметить в предыдущем примере ошибку в названии ветки. Уже почти 15:00, а я всё ещё не обедал, поэтому мой голод назвал новую ветку (branch) как future-brunch. Вкуснотища!
Переименуем эту ветку аналогичным способом, что используется при переименовании файла с помощью команды mv, то есть поместив её в новое место с правильным названием:
git branch -m future-brunch feature-branch
Если вы уже push'нули эту ветку, понадобится пара дополнительных шагов. Мы удалим старую ветку из remote и push'нем новую:
git push origin --delete future-brunch
git push origin feature-branch
Прим. перев.: Удалить ветку из remote ещё можно с помощью:
git push origin :future-brunch
6. Oops… I did it again!
Последняя команда на тот случай, когда всё пошло не так. Когда вы накопировали и навставляли кучу решений со Stack Overflow, после чего в репозитории всё стало ещё хуже, чем было в начале. Все мы однажды сталкивались с подобным…
git reflog
показывает список всех выполненных вами операций. Затем он позволяет использовать магические возможности Git'а по путешествию во времени, т.е. вернуться к любому моменту из прошлого. Должен отметить, что это ваша последняя надежда — не стоит прибегать к ней в простых случаях. Итак, чтобы получить список, выполните:git reflog
Каждый наш шаг находится под чутким наблюдением Git'а. Запуск команды на проекте выше выдал следующее:
3ff8691 (HEAD -> feature-branch) HEAD@{0}: Branch: renamed refs/heads/future-brunch to refs/heads/feature-branch
3ff8691 (HEAD -> feature-branch) HEAD@{2}: checkout: moving from master to future-brunch
2b7e508 (master) HEAD@{3}: reset: moving to HEAD~
3ff8691 (HEAD -> feature-branch) HEAD@{4}: commit: Adds the client logo
2b7e508 (master) HEAD@{5}: reset: moving to HEAD~1
37a632d HEAD@{6}: commit: Adds the client logo to the project
2b7e508 (master) HEAD@{7}: reset: moving to HEAD
2b7e508 (master) HEAD@{8}: commit (amend): Added contributing info to the site
dfa27a2 HEAD@{9}: reset: moving to HEAD
dfa27a2 HEAD@{10}: commit (amend): Added contributing info to the site
700d0b5 HEAD@{11}: commit: Addded contributing info to the site
efba795 HEAD@{12}: commit (initial): Initial commit
Обратите внимание на самый левый столбец — это индекс. Если вы хотите вернуться к любому моменту в истории, выполните следующую команду, заменив
{index}
на соответствующее значение (например, dfa27a2
):git reset HEAD@{index}
Итак, теперь у вас есть шесть способов выбраться из самых частых Gitfalls (игра слов: pitfall переводится как «ловушка, ошибка» — прим. перев.).
Бонус от переводчика
Во-первых, ценное замечание ко всему написанному выше (кроме пункта 5). Нужно учитывать, что эти действия меняют историю коммитов, поэтому их следует проводить, только если изменения не были отправлены в remote (push'нуты). В противном случае старый плохой коммит уже будет на remote-ветке и придётся либо выполнять
git pull
(который сделает merge, и тогда попытка «почистить» историю приведёт к худшим последствиям), либо git push --force
, что чревато потерей данных при работе с веткой нескольких человек…Теперь — небольшие полезные дополнения из нашего опыта:
- Если вы (случайно или нет) сменили ветку и вам нужно вернуться на предыдущую, самый быстрый способ — использовать
git checkout -
. - Если вы случайно добавили к коммиту файл, который не должен быть туда добавлен, но ещё не сделали коммит — используйте
git reset HEAD path/to/file
. Похожая ситуация описана в пункте 3, но в действительности она шире, т.к. относится к любым ненужным изменениям в коммите (не только к случаю лишнего файла). - Хорошей практикой, чтобы не закоммитить лишнего, является использование параметра
-p
при добавлении файла к коммиту (git add -p
). Это позволяет сделать review каждого изменения, которое уйдёт в коммит. Но стоит помнить, что он не добавляет к коммиту untracked-файлы — их нужно добавлять без этого параметра. - Ряд хороших рекомендаций (в том числе и более сложных), можно найти в статье 2014 года «Git Tutorial: 10 Common Git Problems and How to Fix Them». В частности, обратите внимание на использование
git revert
иgit rebase -i
.
P.S. от переводчика
Читайте также в нашем блоге:
- «GitLab CI для непрерывной интеграции и доставки в production»: Часть 1 (наш пайплайн); Часть 2 (преодолевая трудности);
- «Лучшие практики CI/CD с Kubernetes и GitLab (обзор и видео доклада)»;
- «Сборка проектов с GitLab CI: один .gitlab-ci.yml для сотни приложений».
Комментарии (39)
Deosis
10.08.2018 12:344 пункт можно исправить проще:
git checkout -B future-branch git branch -f master HEAD~
При этом нет необходимости commit'ить или stash'ить локальные изменения
CaptainFlint
10.08.2018 13:54Вот тоже сразу подумал о таком варианте. Особенно полезно, если репозиторий очень тяжёлый. Вариант в статье потребует двойной перетасовки рабочего каталога, что может сильно тормозить, а здесь состояние не меняется.
Senyaak
10.08.2018 18:18не знаю, я бы заменил вот этим
git checkout -b future-branch git checkout master git reset --hard origin/master
не нужно заново коммитить + работает если наклепал больше 1 го коммита
WebSpider
10.08.2018 14:06либо git push --force, что чревато потерей данных при работе с веткой нескольких человек…
а как же --force-with-lease?gsmetal Автор
10.08.2018 14:30Да, эта опция гораздо лучше в этом случае. Но по опыту при поиске проблем с
git push
на том же StackOverflow ответов с--force
сильно больше (справедливости ради, последнее время при этом делают пометку о возможной деструктивности такого решения). Поэтому решили лишний раз обратить внимание на то, что не следует бездумно этим пользоваться.
А так решений и советов можно предложить гораздо больше, иgit pull
лучше делать с--rebase
для чистоты истории, иgit rebase -i
, и прочее, прочее. Но это уже более глубокая тема, а тут всё-таки перевод с небольшими дополнениями.
k12th
10.08.2018 14:10Я буду обновлять страницу перед отправкой комментария.
https://habr.com/company/flant/blog/419733/#comment_18981183
k12th
10.08.2018 14:10Я ненавижу новый хабр за то что после отправки комментария форма ввода остается на месте!
Raimon
10.08.2018 14:47самое главное это ревьюить локальные изменения и стейдж (то что будет комититься).
ну ещё из специфичного — в Visual Studio нужно сделать Save All, иначе изменения в проектых не попадут на диск.fedorro
10.08.2018 15:57В любом текстовом (и не текстовом) редакторе нужно сохранять изменения, чтобы они записались в файл, git же не из памяти процессов должен изменения вычитывать.
k12th
10.08.2018 16:20-1Студия по умолчанию сохраняет изменения только в непосредственно отредактированных файлах. А *.csproj — не барское это дело, пусть юзер сам озаботится.
fedorro
10.08.2018 16:29-1При запуске или построении проекта она сама всё сохраняет. А просто при редактировании не сохраняет ни один редактор. Если файл проекта изменён косвенно (добавились\удалились ссылки файлы) — то это всё равно редактирование, а сохранять или нет эти изменения — пользователю решать. То что она не помечает, что файл проекта изменен, если он не открыт во вкладке — это конечно может вызвать некоторые неудобство, но это не спицифика git-a.
Raimon
10.08.2018 16:45-1никто и не говорит, что это специфика git, но часто именно это поведение VS становится причиной поломанных билдов. и именно из-за этого в истории git вижу докомиты по одному забытому проектному файлу.
я бы не сказал, что оно очевидно и удобно. когда ты редактируешь файл и в нем есть несохраненные изменения есть какая-то индикация этого, если изменил проектный файл ничего нет. добавил файл в проект нажал Ctrl-S, вроде бы должно хватать, а нет.fedorro
10.08.2018 16:53Просто фокус у Вас на этом новом файле — она его и сохраняет. Выбрать проект в обозревателе проектов и нажать и нажать тот же Ctrl-S, и всё работает. Что нет индикации, что этот файл изменен — это минус, да я согласен. Ну а забывчивые, или кого это раздражает могут переназначить на Ctrl-S команду «Save Aal».
DarkWanderer
11.08.2018 10:49Или использовать встроенный git-интерфейс для коммитов, там изменения "в памяти" учитываются
springimport
10.08.2018 16:52Как часто бывает — до конца дня не успел закончить задачу. Остается либо не коммитить ничего и попасть если не окажется доступа к файлам, либо коммитить, но часть [неработающего] кода.
Что с этим делать?fedorro
10.08.2018 16:56Или git-stash, чтобы сохранить изменения локально, или комитить и пушить во временную ветку.
gsmetal Автор
10.08.2018 16:59Коммитнуть в отдельную ветку, которую можно и пушнуть в remote, чтобы данные точно не потерялись. В коммите при этом можно указать, что он WIP. И вообще в такую свою dev-ветку можно коммитить хоть каждые 10 минут и с не очень информативными комментариями. А когда всё готово — чистить историю с помощью
git rebase -i
. Ну или в случае с Gitlab, он умеет делать squash коммитов MR при мёрже.Hokum
10.08.2018 21:43А если работаете с bitbucket, то можно сделать форк и плодить ветки в неограниченных количествах. GitLab тоже позволяет делать форк, но у него какие-то проблемы с автоматической синхронизацией — то работает, то отваливается.
Когда работал в компании где был развернут bitbucket server, то пользовался форками с удовольствием.
ElegantBoomerang
10.08.2018 18:13Про удаление remote — уже довольно долго можно делать
git push origin --delete branch
, что куда понятнее.
aamonster
10.08.2018 20:02Ой-ой. Начиная с пункта 4 — вещи, которые не следует делать, пока как следует не вкуришь git (а тогда эта статья уже не нужна).
Я после своего первого git reset, наверное, полдня потратил на изучение git только для того, чтобы больше так не делать (пропала история… удалось восстановить) и полюбил mercurial, в котором на такие грабли не наступал ("святость истории" — хотя, конечно, и там есть послабления).
da-nie
10.08.2018 20:40+1Если же вы всё-таки коммитнули изменение, потребуется дополнительный предварительный шаг:
А если вы сделали уже несколько коммитов и тут заметили, что лишний файл там лежит? :)
Я в этом случае нашёл рецепт такой:
Удаление файла TeamControlClient.sdf из всех коммитов репозитория в e:\TeamControlClient.
Сначала удалим файл:
git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch TeamControlClient.sdf' \
--prune-empty --tag-name-filter cat — --all
Теперь создалась новая «ветка» с места появления файла.
А теперь клонируем репозиторий с этой веткой, не заходя в старую.
git clone file://e:/TeamControlClient e:/TeamControlClientNew
В новом репозитории файл будет физически удалён.
Уж не знаю, насколько это правильно. Но помогло.Hilbert
11.08.2018 02:11Способ нормальный, разве что никакой
git clone
там не нужен. Он правилен ровно настолько же, насколько любые другие манипуляции, приводящие к потере id коммитов, ребейз, например. На ветках, доступных другим разработчикам, использовать по возможности не стоит.
Минусfilter-branch
— gpg-подписи теряются (rebase
же пробует переподписать новые коммиты, например).da-nie
11.08.2018 08:21Без клонирования размер репозитория не изменится после удаления файла (файл как бы физически остаётся). Поэтому, как я понимаю, и делается клонирование.
Hilbert
12.08.2018 03:35Тогда понятно, но достаточно выполнить
git gc --prune=now
, если хочется удалить сразу (без этого garbage collector удалит его только через две недели).
michael_vostrikov
11.08.2018 06:22Слишком сложно, кмк. Можно сделать коммит с удалением файла, потом через интерактивный rebase передвинуть этот коммит ниже до нужного и объединить, можно через него же с отметкой нужного коммита "edit", можно вручную сделать ветку от нужного коммита, в ней commit --amend с исправлениями, потом через cherry-pick добавить все следующие коммиты.
da-nie
11.08.2018 08:17А это физически исключит файл? У меня удаляемый файл привёл к увеличению объёма на 50 Мб. Просто убрать файл из истории не помогало никак.
michael_vostrikov
11.08.2018 11:19Есть команды
git gc
иgit prune
, но я ими не пользовался, потому про нюансы не в курсе.
ookami_kb
11.08.2018 13:36+1Я для удаления файлов и sensitive info из истории использовал BFG Repo-Cleaner – неплохая штука.
michael_vostrikov
11.08.2018 06:09+3Тот случай, когда чуть ли не с первого дня пользуешься этим в GUI, где оно делается просто и естественно, а оказывается с этим бывают проблемы.
На
--amend
галочка в диалоге коммита, на файл галочка в списке файлов, причемgit add
для untracked автоматически выполняется, на отмену изменений пункт меню "Revert", на переименование ветки F2 в списке веток, на удаление пункт меню там же, и для удаленной тоже,reset
на коммит пункт меню в списке коммитов, "Show reflog" вообще рядом с "Show log", и иногда случайно на него тыкаешь.
Коммит не в ту ветку требует аналогичных действий, нестандартныйcheckout
илиreset
надо конечно в консоли делать.
PS: "Опять кто-то влез с GUI", но статья для неопытных пользователей, и кому-то это поможет сохранить время и нервы)
aamonster
11.08.2018 11:18+1Емнип, наиболее приличные gui ещё и показывают, какие команды выполняются — так что можно потихоньку перетаскивать рутинные задачи в консоль (чтобы делать меньше телодвижений).
OldFisher
И это они называют «упс»? Это обыкновенные рабочие ситуации, а не упс. Вот когда вы тщательно настроили игнор, чтобы не тащить в репозиторий лишние файлы вроде программной документации в причудливом бинарном формате, а через некоторое время откатились через git reset --hard, вот это действительно «упс». Даже «упсище».
gsmetal Автор
Вот поэтому тут и «упс», а то что вы говорите — никак не ниже «Ох тыж… ё-моё!» :)
theWaR_13
Знакомо :) Недавно решал похожую ситуацию, когда есть локальные файлы, которые не должны быть в .gitignore, но должны быть в репозитории. При этом, мне нужно периодически делать в них изменения, а git reset --hard как раз таки откатывает их к начальному состоянию. В итоге как-то удалось решить эту проблему, но это было больно…