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


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

Комментарии (39)


  1. OldFisher
    10.08.2018 11:10
    +1

    И это они называют «упс»? Это обыкновенные рабочие ситуации, а не упс. Вот когда вы тщательно настроили игнор, чтобы не тащить в репозиторий лишние файлы вроде программной документации в причудливом бинарном формате, а через некоторое время откатились через git reset --hard, вот это действительно «упс». Даже «упсище».


    1. gsmetal Автор
      10.08.2018 11:23

      Вот поэтому тут и «упс», а то что вы говорите — никак не ниже «Ох тыж… ё-моё!» :)


    1. theWaR_13
      10.08.2018 16:19
      -1

      Знакомо :) Недавно решал похожую ситуацию, когда есть локальные файлы, которые не должны быть в .gitignore, но должны быть в репозитории. При этом, мне нужно периодически делать в них изменения, а git reset --hard как раз таки откатывает их к начальному состоянию. В итоге как-то удалось решить эту проблему, но это было больно…


  1. Deosis
    10.08.2018 12:34

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


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

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


    1. CaptainFlint
      10.08.2018 13:54

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


    1. Senyaak
      10.08.2018 18:18

      не знаю, я бы заменил вот этим

      git checkout -b future-branch
      git checkout master
      git reset --hard origin/master
      

      не нужно заново коммитить + работает если наклепал больше 1 го коммита


  1. WebSpider
    10.08.2018 14:06

    либо git push --force, что чревато потерей данных при работе с веткой нескольких человек…
    а как же --force-with-lease?


    1. gsmetal Автор
      10.08.2018 14:30

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


  1. k12th
    10.08.2018 14:10

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


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


  1. k12th
    10.08.2018 14:10

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


  1. amarao
    10.08.2018 14:43
    +1

    О, я про reflog не знал. Спасибо.


  1. Raimon
    10.08.2018 14:47

    самое главное это ревьюить локальные изменения и стейдж (то что будет комититься).

    ну ещё из специфичного — в Visual Studio нужно сделать Save All, иначе изменения в проектых не попадут на диск.


    1. fedorro
      10.08.2018 15:57

      В любом текстовом (и не текстовом) редакторе нужно сохранять изменения, чтобы они записались в файл, git же не из памяти процессов должен изменения вычитывать.


      1. k12th
        10.08.2018 16:20
        -1

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


        1. fedorro
          10.08.2018 16:29
          -1

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


          1. k12th
            10.08.2018 16:33
            -1

            А просто при редактировании не сохраняет ни один редактор.

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


            1. fedorro
              10.08.2018 16:43
              -1

              Будем считать это неаккуратным округлением от «почти все».


          1. Raimon
            10.08.2018 16:45
            -1

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

            я бы не сказал, что оно очевидно и удобно. когда ты редактируешь файл и в нем есть несохраненные изменения есть какая-то индикация этого, если изменил проектный файл ничего нет. добавил файл в проект нажал Ctrl-S, вроде бы должно хватать, а нет.


            1. fedorro
              10.08.2018 16:53

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


      1. Free_ze
        13.08.2018 13:06
        -1

        В Rider это происходит автоматически.


    1. DarkWanderer
      11.08.2018 10:49

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


  1. springimport
    10.08.2018 16:52

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

    Что с этим делать?


    1. fedorro
      10.08.2018 16:56

      Или git-stash, чтобы сохранить изменения локально, или комитить и пушить во временную ветку.


    1. gsmetal Автор
      10.08.2018 16:59

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


      1. Hokum
        10.08.2018 21:43

        А если работаете с bitbucket, то можно сделать форк и плодить ветки в неограниченных количествах. GitLab тоже позволяет делать форк, но у него какие-то проблемы с автоматической синхронизацией — то работает, то отваливается.
        Когда работал в компании где был развернут bitbucket server, то пользовался форками с удовольствием.


  1. ElegantBoomerang
    10.08.2018 18:13

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


  1. aragaer
    10.08.2018 18:41

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

    Вот так

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


  1. aamonster
    10.08.2018 20:02

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


  1. 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


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

    Уж не знаю, насколько это правильно. Но помогло.


    1. Hilbert
      11.08.2018 02:11

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

      Минус filter-branch — gpg-подписи теряются (rebase же пробует переподписать новые коммиты, например).


      1. da-nie
        11.08.2018 08:21

        Без клонирования размер репозитория не изменится после удаления файла (файл как бы физически остаётся). Поэтому, как я понимаю, и делается клонирование.


        1. Hilbert
          12.08.2018 03:35

          Тогда понятно, но достаточно выполнить git gc --prune=now, если хочется удалить сразу (без этого garbage collector удалит его только через две недели).


    1. michael_vostrikov
      11.08.2018 06:22

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


      1. da-nie
        11.08.2018 08:17

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


        1. michael_vostrikov
          11.08.2018 11:19

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


    1. ookami_kb
      11.08.2018 13:36
      +1

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


  1. michael_vostrikov
    11.08.2018 06:09
    +3

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


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


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


    1. aamonster
      11.08.2018 11:18
      +1

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


  1. celebrate
    11.08.2018 15:28
    +2

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