Git happens! 6 типичных ошибок Git и как их исправить

https://about.gitlab.com/2018/08/08/git-happens/
  • Перевод


Прим. перев.: На днях в блоге для инженеров любимого нами проекта 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. от переводчика


Читайте также в нашем блоге:

Флант 385,54
Специалисты по DevOps и высоким нагрузкам в вебе
Поделиться публикацией
Похожие публикации
Комментарии 43
    +8
    И это они называют «упс»? Это обыкновенные рабочие ситуации, а не упс. Вот когда вы тщательно настроили игнор, чтобы не тащить в репозиторий лишние файлы вроде программной документации в причудливом бинарном формате, а через некоторое время откатились через git reset --hard, вот это действительно «упс». Даже «упсище».
      +10
      Вот поэтому тут и «упс», а то что вы говорите — никак не ниже «Ох тыж… ё-моё!» :)
        0
        Знакомо :) Недавно решал похожую ситуацию, когда есть локальные файлы, которые не должны быть в .gitignore, но должны быть в репозитории. При этом, мне нужно периодически делать в них изменения, а git reset --hard как раз таки откатывает их к начальному состоянию. В итоге как-то удалось решить эту проблему, но это было больно…
          0
          У меня была ситуация, когда я запушил 10-мегабайтный архив в репозиторий, и после этого уже не мог его удалить, т.к. любое изменение формировало дифф в 300-400 мегабайт, который при попытки его применить на сервере падал по таймауту через полчаса.
            0
            Хм, а разве git reset --hard удаляет untracked файлы? Вот git clean -df да, это бывает упс.
            +1

            4 пункт можно исправить проще:


            git checkout -B future-branch
            git branch -f master HEAD~

            При этом нет необходимости commit'ить или stash'ить локальные изменения

              0
              Вот тоже сразу подумал о таком варианте. Особенно полезно, если репозиторий очень тяжёлый. Вариант в статье потребует двойной перетасовки рабочего каталога, что может сильно тормозить, а здесь состояние не меняется.
                +2
                не знаю, я бы заменил вот этим
                git checkout -b future-branch
                git checkout master
                git reset --hard origin/master
                

                не нужно заново коммитить + работает если наклепал больше 1 го коммита
                +2
                либо git push --force, что чревато потерей данных при работе с веткой нескольких человек…
                а как же --force-with-lease?
                  0
                  Да, эта опция гораздо лучше в этом случае. Но по опыту при поиске проблем с git push на том же StackOverflow ответов с --force сильно больше (справедливости ради, последнее время при этом делают пометку о возможной деструктивности такого решения). Поэтому решили лишний раз обратить внимание на то, что не следует бездумно этим пользоваться.
                  А так решений и советов можно предложить гораздо больше, и git pull лучше делать с --rebase для чистоты истории, и git rebase -i, и прочее, прочее. Но это уже более глубокая тема, а тут всё-таки перевод с небольшими дополнениями.
                    0

                    Я бросил бороться с упертыми коллегами которые предпочитают работать с rebase + push --force в feature ветках. Но есть у нас один беспардонный человек, он как слон в посудной лавке гитпушфорсит в develop, де мердж коммиты это кал а вот push --force это прелесть она так выглаживает историю коммитов. Этот Джон Сноу скоро доиграется и ему братья коллеги устроят темную.

                  +1

                  Я буду обновлять страницу перед отправкой комментария.


                  https://habr.com/company/flant/blog/419733/#comment_18981183

                    +2

                    Я ненавижу новый хабр за то что после отправки комментария форма ввода остается на месте!

                      +1
                      О, я про reflog не знал. Спасибо.
                        0
                        самое главное это ревьюить локальные изменения и стейдж (то что будет комититься).

                        ну ещё из специфичного — в Visual Studio нужно сделать Save All, иначе изменения в проектых не попадут на диск.
                          0
                          В любом текстовом (и не текстовом) редакторе нужно сохранять изменения, чтобы они записались в файл, git же не из памяти процессов должен изменения вычитывать.
                            –2

                            Студия по умолчанию сохраняет изменения только в непосредственно отредактированных файлах. А *.csproj — не барское это дело, пусть юзер сам озаботится.

                              –1
                              При запуске или построении проекта она сама всё сохраняет. А просто при редактировании не сохраняет ни один редактор. Если файл проекта изменён косвенно (добавились\удалились ссылки файлы) — то это всё равно редактирование, а сохранять или нет эти изменения — пользователю решать. То что она не помечает, что файл проекта изменен, если он не открыт во вкладке — это конечно может вызвать некоторые неудобство, но это не спицифика git-a.
                                –1
                                А просто при редактировании не сохраняет ни один редактор.

                                Есть контр-примеры.

                                  0
                                  Будем считать это неаккуратным округлением от «почти все».
                                  +1
                                  никто и не говорит, что это специфика git, но часто именно это поведение VS становится причиной поломанных билдов. и именно из-за этого в истории git вижу докомиты по одному забытому проектному файлу.

                                  я бы не сказал, что оно очевидно и удобно. когда ты редактируешь файл и в нем есть несохраненные изменения есть какая-то индикация этого, если изменил проектный файл ничего нет. добавил файл в проект нажал Ctrl-S, вроде бы должно хватать, а нет.
                                    –1
                                    Просто фокус у Вас на этом новом файле — она его и сохраняет. Выбрать проект в обозревателе проектов и нажать и нажать тот же Ctrl-S, и всё работает. Что нет индикации, что этот файл изменен — это минус, да я согласен. Ну а забывчивые, или кого это раздражает могут переназначить на Ctrl-S команду «Save Aal».
                                –1
                                В Rider это происходит автоматически.
                                0

                                Или использовать встроенный git-интерфейс для коммитов, там изменения "в памяти" учитываются

                                0
                                Как часто бывает — до конца дня не успел закончить задачу. Остается либо не коммитить ничего и попасть если не окажется доступа к файлам, либо коммитить, но часть [неработающего] кода.

                                Что с этим делать?
                                  0
                                  Или git-stash, чтобы сохранить изменения локально, или комитить и пушить во временную ветку.
                                    +1
                                    Коммитнуть в отдельную ветку, которую можно и пушнуть в remote, чтобы данные точно не потерялись. В коммите при этом можно указать, что он WIP. И вообще в такую свою dev-ветку можно коммитить хоть каждые 10 минут и с не очень информативными комментариями. А когда всё готово — чистить историю с помощью git rebase -i. Ну или в случае с Gitlab, он умеет делать squash коммитов MR при мёрже.
                                      0
                                      А если работаете с bitbucket, то можно сделать форк и плодить ветки в неограниченных количествах. GitLab тоже позволяет делать форк, но у него какие-то проблемы с автоматической синхронизацией — то работает, то отваливается.
                                      Когда работал в компании где был развернут bitbucket server, то пользовался форками с удовольствием.
                                    +1

                                    Про удаление remote — уже довольно долго можно делать git push origin --delete branch, что куда понятнее.

                                      0
                                      Пункт номер 4 можно было бы сделать значительно менее деструктивно. В частности, без каких либо последствий для текущего рабочего дерева (ничего не надо коммиттить и стэшить). В две команды.

                                      Вот так

                                      (тут мне следовало бы вставить пометку «я пиарюсь»)
                                        0

                                        Ой-ой. Начиная с пункта 4 — вещи, которые не следует делать, пока как следует не вкуришь git (а тогда эта статья уже не нужна).
                                        Я после своего первого git reset, наверное, полдня потратил на изучение git только для того, чтобы больше так не делать (пропала история… удалось восстановить) и полюбил mercurial, в котором на такие грабли не наступал ("святость истории" — хотя, конечно, и там есть послабления).

                                          +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


                                          В новом репозитории файл будет физически удалён.

                                          Уж не знаю, насколько это правильно. Но помогло.
                                            0
                                            Способ нормальный, разве что никакой git clone там не нужен. Он правилен ровно настолько же, насколько любые другие манипуляции, приводящие к потере id коммитов, ребейз, например. На ветках, доступных другим разработчикам, использовать по возможности не стоит.

                                            Минус filter-branch — gpg-подписи теряются (rebase же пробует переподписать новые коммиты, например).
                                              0
                                              Без клонирования размер репозитория не изменится после удаления файла (файл как бы физически остаётся). Поэтому, как я понимаю, и делается клонирование.
                                                0
                                                Тогда понятно, но достаточно выполнить git gc --prune=now, если хочется удалить сразу (без этого garbage collector удалит его только через две недели).
                                              0

                                              Слишком сложно, кмк. Можно сделать коммит с удалением файла, потом через интерактивный rebase передвинуть этот коммит ниже до нужного и объединить, можно через него же с отметкой нужного коммита "edit", можно вручную сделать ветку от нужного коммита, в ней commit --amend с исправлениями, потом через cherry-pick добавить все следующие коммиты.

                                                0
                                                А это физически исключит файл? У меня удаляемый файл привёл к увеличению объёма на 50 Мб. Просто убрать файл из истории не помогало никак.
                                                  0

                                                  Есть команды git gc и git prune, но я ими не пользовался, потому про нюансы не в курсе.

                                                +1

                                                Я для удаления файлов и sensitive info из истории использовал BFG Repo-Cleaner – неплохая штука.

                                                +3

                                                Тот случай, когда чуть ли не с первого дня пользуешься этим в GUI, где оно делается просто и естественно, а оказывается с этим бывают проблемы.


                                                На --amend галочка в диалоге коммита, на файл галочка в списке файлов, причем git add для untracked автоматически выполняется, на отмену изменений пункт меню "Revert", на переименование ветки F2 в списке веток, на удаление пункт меню там же, и для удаленной тоже, reset на коммит пункт меню в списке коммитов, "Show reflog" вообще рядом с "Show log", и иногда случайно на него тыкаешь.
                                                Коммит не в ту ветку требует аналогичных действий, нестандартный checkout или reset надо конечно в консоли делать.


                                                PS: "Опять кто-то влез с GUI", но статья для неопытных пользователей, и кому-то это поможет сохранить время и нервы)

                                                  +1

                                                  Емнип, наиболее приличные gui ещё и показывают, какие команды выполняются — так что можно потихоньку перетаскивать рутинные задачи в консоль (чтобы делать меньше телодвижений).

                                                  +3
                                                  Чтобы git pull не делал merge — используйте git pull --rebase.
                                                    0

                                                    Это наверное самая полезная фича при работе в команде.
                                                    Я бы даже посоветовал сделать
                                                    git config --global pull.rebase true

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

                                                  Самое читаемое