company_banner

Переписывание истории репозитория кода, или почему иногда можно git push -f



    Одно из первых наставлений, которое молодой падаван получает вместе с доступом к git-репозиториям, звучит так: «никогда не ешь жёлтый снег делай git push -f». Поскольку это одна из сотен максим, которые нужно усвоить начинающему инженеру-разработчику ПО, никто не тратит время на уточнение, почему именно так нельзя делать. Это как младенцы и огонь: «спички детям не игрушки» и баста. Но мы растём и развиваемся как люди и как профессионалы, и однажды вопрос «а почему, собственно?» встаёт в полный рост. Эта статья написана по мотивам нашего внутреннего митапа, на тему: «Когда можно и нужно переписывать историю коммитов».



    Я слышал, что умение ответить на этот вопрос на собеседовании в некоторых компаниях является критерием прохождения собеседования на сеньорские позиции. Но чтобы лучше понять ответ на него, нужно разобраться, почему вообще плохо переписывание истории?

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

    На самом низком уровне git-репо представляет собой набор объектов и указателей на них. Каждый объект имеет свой уникальный 40-значный хэш (20 байт, записанные в 16-ричной системе), который вычисляется на основе содержимого объекта.



    Иллюстрация взята из The Git Community Book

    Основные типы объектов — это blob (просто содержимое файла), tree (набор указателей на blobs и другие trees) и commit. Объект типа commit представляет собой только указатель на tree, на предыдущий коммит и служебную информацию: дата/время, автор и комментарий.

    Где здесь ветки и тэги, которыми мы привыкли оперировать? А они не являются объектами, они являются просто указателями: ветка указывает на последний коммит в ней, тэг — на произвольный коммит в репо. То есть когда мы в IDE или GUI-клиенте видим красиво нарисованные веточки с кружочками-коммитами на них — они строятся на лету, пробегая по цепочкам коммитов от концов веток вниз к «корню». Самый первый коммит в репо не имеет предыдущего, вместо указателя там null.

    Важный для понимания момент: один и тот же коммит может фигурировать в нескольких ветках одновременно. Коммиты не копируются при создании новой ветки, она просто начинает «расти» с того места, где был HEAD в момент отдачи команды git checkout -b <branch-name>.

    Итак, почему же переписывание истории репозитория вредно?



    Во-первых, и это очевидно, при загрузке новой истории в репозитории, с которым работает команда инженеров, другие люди могут просто потерять свои изменения. Команда git push -f удаляет из ветки на сервере все коммиты, которых нет в локальной версии, и записывает новые.

    Почему-то мало кто знает, что довольно давно у команды git push существует «безопасный» ключ --force-with-lease, который заставляет команду завершиться с ошибкой, если в удалённом репозитории есть коммиты, добавленные другими пользователями. Я всегда рекомендую использовать его вместо -f/--force.

    Вторая причина, по которой команда git push -f считается вредной, заключается в том, что при попытке слияния (merge) ветки с переписанной историей с ветками, где она сохранилась (точнее, сохранились коммиты, удалённые из переписанной истории), мы получим адское число конфликтов (по числу коммитов, собственно). На это есть простой ответ: если аккуратно соблюдать Gitflow или Gitlab Flow, то такие ситуации, скорее всего, даже не возникнут.

    И наконец есть неприятная побочка переписывания истории: те коммиты, которые как бы удаляются при этом из ветки, на самом деле, никуда не исчезают и просто остаются навечно висеть в репо. Мелочь, но неприятно. К счастью, эту проблему разработчики git тоже предусмотрели, введя команду сборки мусора git gc --prune. Большинство git-хостингов, как минимум GitHub и GitLab, время от времени производят эту операцию в фоне.

    Итак, развеяв опасения перед изменением истории репозитория, можно, наконец, перейти к главному вопросу: зачем оно нужно и когда оправдано?

    На самом деле, я уверен, что практически каждый из более-менее активных пользователей git хоть раз, да изменял историю, когда вдруг оказывалось, что в последнем коммите что-то пошло не так: вкралась досадная опечатка в код, сделал коммит не от того пользователя (с личного e-mail вместо рабочего или наоборот), забыл добавить новый файл (если вы, как я, любите пользоваться git commit -a). Даже изменение описания коммита приводит к необходимости его перезаписи, ведь хэш считается и от описания тоже!

    Но это тривиальный случай. Давайте рассмотрим более интересные.

    Допустим, вы сделали большую фичу, которую пилили несколько дней, отсылая ежедневно результаты работы в репозиторий на сервере (4-5 коммитов), и отправили свои изменения на ревью. Двое-трое неутомимых ревьюверов закидали вас крупными и мелкими рекомендациями правок, а то и вовсе нашли косяки (ещё 4-5 коммитов). Затем QA нашли несколько краевых случаев, тоже требующих исправлений (ещё 2-3 коммита). И наконец при интеграции выяснились какие-то несовместимости или попадали автотесты, которые тоже надо пофиксить.

    Если теперь нажать, не глядя, кнопку Merge, то в главную ветку (у многих она по старинке называется master) вольются полтора десятка коммитов типа «My feature, day 1», «Day 2», «Fix tests», «Fix review» и т.д. От этого, конечно, помогает режим squash, который сейчас есть и в GitHub, и в GitLab, но с ним надо быть осторожными: во-первых, он может заменить описание коммита на что-то непредсказуемое, а во-вторых — заменить автора фичи на того, кто нажал кнопку Merge (у нас это вообще робот, помогающий релиз-инженеру собрать сегодняшний деплой). Поэтому самым простым будет перед окончательной интеграцией в релиз схлопнуть все коммиты ветки в один при помощи git rebase.

    Но бывает также, что к код-ревью вы уже подошли с историей репо, напоминающей салат «Оливье». Такое бывает, если фича пилилась несколько недель, ибо была плохо декомпозирована или, хотя за это в приличных коллективах бьют канделябром, требования изменились в процессе разработки. Вот, например, реальный merge request, который приехал ко мне на ревью две недели назад:



    У меня рука машинально потянулась к кнопке «Report abuse», потому что как ещё можно охарактеризовать реквест из 50 коммитов с почти 2000 изменённых строк? И как его, спрашивается, ревьюить?

    Честно говоря, у меня ушло два дня просто на то, чтобы заставить себя приступить к этому ревью. И это нормальная реакция для инженера; кто-то в подобной ситуации, просто не глядя, жмёт Approve, понимая, что за разумное время всё равно не сможет сделать работу по обзору этого изменения с достаточным качеством.

    Но есть способ облегчить жизнь товарищу. Помимо предварительной работы по лучшей декомпозиции задачи, уже после завершения написания основного кода можно привести историю его написания в более логичный вид, разбив на атомарные коммиты с зелёными тестами в каждом: «создал новый сервис и транспортный уровень для него», «построил модели и написал проверку инвариантов», «добавил валидацию и обработку исключений», «написал тесты».
    Каждый из таких коммитов можно ревьюить по отдельности (и GitHub, и GitLab это умеют) и делать это набегами в моменты переключения между своими задачами или в перерывах.

    Сделать это всё нам поможет всё тот же git rebase с ключом --interactive. В качестве параметра надо передать ему хэш коммита, начиная с которого нужно будет переписать историю. Если речь о последних 50 коммитах, как в примере на картинке, можно написать git rebase --interactive HEAD~50 (подставьте вместо “50” вашу цифру).

    Кстати, если вы в процессе работы над задачей подливали к себе ветку master, то сначала надо будет сделать rebase на эту ветку, чтобы merge-коммиты и коммиты из мастера не путались у вас под ногами.

    Вооружившись знаниями о внутреннем устройстве git-репозитория, понять принцип действия rebase на master будет несложно. Эта команда берёт все коммиты в нашей ветке и меняет родителя первого из них на последний коммит в ветке master. См. схему:




    Иллюстрации взяты из книги Pro Git

    Если изменения в C4 и C3 конфликтуют, то после разрешения конфликтов коммит C4 изменит своё содержание, поэтому он переименован на второй схеме в C4’.

    Таким образом, вы получите ветку, состоящую только из ваших изменений, и растущую из вершины master. Само собой, master должен быть актуальным. Можно просто использовать версию с сервера: git pull --rebase origin/master (как известно, git pull равносилен git fetch && git merge, а ключ --rebase заставит git сделать rebase вместо merge).

    Вернёмся наконец к git rebase --interactive. Его делали программисты для программистов, и понимая, какой стресс люди будут испытывать в процессе, постарались максимально сохранить нервы пользователя и избавить его от необходимости чрезмерно напрягаться. Вот что вы увидите на экране:


    Это репозиторий популярного пакета Guzzle. Похоже, что rebase ему не помешал бы…

    В текстовом редакторе открывается сформированный файл. Внизу вас ожидает подробная справка о том, что тут вообще делать. Далее в режиме простого редактирования вы решаете, что делать с коммитами в вашей ветке. Всё просто, как палка: pick — оставить как есть, reword — поменять описание коммита, squash — слить воедино с предыдущим (процесс работает снизу вверх, то есть предыдущий — это который строчкой ниже), drop — вообще удалить, edit — и это самое интересное — остановиться и замереть. После того, как git встретит команду edit, он встанет в позицию, когда изменения в коммите уже добавлены в режим staged. Вы можете поменять всё, что угодно в этом коммите, добавить поверх него ещё несколько, и после этого скомандовать git rebase --continue, чтобы продолжить процесс rebase.

    Да, и кстати, вы можете поменять коммиты местами. Возможно, это создаст конфликты, но в целом процесс rebase редко обходится совсем уж без конфликтов. Как говорится, снявши голову, по волосам не плачут.

    Если вы запутались и кажется, что всё пропало, у вас есть кнопка аварийного катапультирования git rebase --abort, которая немедленно вернёт всё как было.

    Вы можете повторять rebase несколько раз, затрагивая только части истории, и оставляя остальные нетронутыми при помощи pick, придавая своей истории всё более и более законченный вид, как гончар кувшину. Хорошим тоном, как я уже написал выше, будет сделать так, что тесты в каждом коммите будут зелёными (для этого отлично помогает edit и на следующем проходе — squash).

    Ещё одна фигура высшего пилотажа, полезная в случае, если надо несколько изменений в одном и том же файле разложить по разным коммитам — git add --patch. Она бывает полезна и сама по себе, но в сочетании с директивой edit она позволит вам разделить один коммит на несколько, причём сделать это на уровне отдельных строк, чего не позволяет, если я не ошибаюсь, ни один GUI-клиент и ни одна IDE.

    Убедившись ещё раз, что всё в порядке, вы наконец можете со спокойной душой сделать то, с чего начался этот туториал: git push --force. Ой, то есть, разумеется, --force-with-lease!



    Поначалу вы, скорее всего, будете тратить на этот процесс (включая первоначальный rebase на master) час, а то и два, если фича реально развесистая. Но даже это намного лучше, чем ждать два дня, когда ревьювер заставит себя наконец взяться за ваш реквест, и ещё пару дней, пока он сквозь него продерётся. В будущем же вы, скорее всего, будете укладываться в 30-40 минут. Особенно помогают в этом продукты линейки IntelliJ со встроенным инструментом разрешения конфликтов (full disclosure: компания FunCorp оплачивает эти продукты своим сотрудникам).

    Последнее, от чего хочется предостеречь, — не переписывайте историю ветки в процессе код-ревью. Помните, что добросовестный ревьюер возможно клонирует ваш код к себе локально, чтобы иметь возможность смотреть на него через IDE и запускать тесты.

    Спасибо за внимание всем, кто дочитал до конца! Надеюсь, что статья будет полезна не только вам, но и коллегам, которым ваш код попадает на ревью. Если у вас есть клёвые хаки для git — делитесь ими в комментариях!
    FunCorp
    Разработка развлекательных сервисов

    Комментарии 29

      –24
      Одно из первых наставлений, которое молодой падаван получает вместе с доступом к git-репозиториям, звучит так: «никогда не ешь жёлтый снег делай git push -f».


      Я думаю, на этом месте можно заканчивать читать.
      1. У вас отвратительный процесс онбординга.
      2. У вас отсутствует документированный девелопмент-процесс
      3. Вы в штыки воспринимаете опыт, отличающийся от вашего.

      У нас в команде примерно каждый пользуется форс-пушем и rebase --interactive по многу раз за день. И тренинги про гит и наше воркфлоу — одни из первых строк в онбординг-плане.
        +8
        примерно каждый пользуется форс-пушем и rebase --interactive по многу раз за день

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

          –1
          Заминусовали, видимо, те, кто подумали, что мы в мастер форс-пушим постоянно что ли? Мастер де-факт protected и туда абсолютно никто не может зафорспушить даже если захочется.

          а чем это вызвано? или ваше «наше воркфлоу» к этому принуждает? просто интересно, т.к. всего пару-тройку раз требовалось использовать что одно, что другое...


          Не принуждает, это просто невероятно удобно =)

          Если вкратце, то исторически сначала команда работала в Геррите, где ревью per-commit. Соответственно, очень высокая девелоперская культура в команде, перед отправкой на ревью история причесывается. Сейчас уже почти три года в Гитлабе, но культура не ухудшилась.

          — Перед отправкой на ревью причесываешь локальную историю
          — Когда фиксишь фидбек по ревью, то делаешь fixup конкретных коммитов, так, что история остается чистой, соответственно, rebase + force push в свою ветку
          — Вливается PR/MR по стратегии rebase + ff-only. Т.е. история в мастере такая же чистая. Если в конкретном случае конкретный разработчик хочет влить MR сквошем — никто не запрещает, можно выбрать сквош и написать внятное коммит-сообщение.

          Побочные эффекты всего этого — нет никаких «мусорных» коммитов, история и локальная и в мастере всегда чистая, git log, git log --oneline, git log --graph — одни из главных помощников в работе, нет ухудшающих читаемость мерж-коммитов.

          И в том числе поэтому у меня текут глаза, когда я вижу в публичных проектах на гитхабе коммиты, написанные левой пяткой, а в особо запущенных случаях это не только в PR, но и в мастере оказывается. За кривые коммиты принято бить по рукам, это воспитывает :)
            +16

            Вы в штыки воспринимаете опыт, отличающийся от вашего

              +3

              В поддержку автора комментария хочу привести ссылку на статью, где я сам впервые прочитал про подобный процесс: Commit Often, Perfect Later, Publish Once: Git Best Practices.


              Там был эпичный абзац про изготовление сосисок, который запал мне в душу:


              The process of developing software, similar to the process of making sausage, is a messy messy business; all sorts of stuff happens in the process of developing software. Bugs are inserted into the code, uncovered, patched over. The end result may be a tasty program, but anyone looking at the process of how it was created (through inspection of the commits) may end up with a sour taste in their mouth. If you hide the sausage making, you can create a beautiful looking history where each step looks as delicious as the end-product.

              В целом, мне эти ценности близки, поэтому и я сам стараюсь по-возможности придерживаться подхода "часто комитить — причёсывать — публиковать".

                +5

                Мне этот абзац сначала понравился, а потом я вспомнил про нарциссические расстройства личности) Вот она, сила неправильных аналогий!


                Красивая (правильнее будет сказать — облагороженная) история сама по себе ценности не имеет. А вот удобство код-ревью и сохранение важной смысловой информации для тех инженеров, кто будет читать этот код через 5 и даже 10 лет — очень даже пересчитываются в деньги и время.

                0
                А, если не секрет, что вы такое делаете, что вам так часто надо обращаться к логу коммитов?
                git log, git log --oneline, git log --graph — одни из главных помощников в работе, нет ухудшающих читаемость мерж-коммитов.

            +3
            При rebase коммиты не просто переносятся, изменения из коммитов применяются (replay) к коду в новом месте. При такой формулировке легче понять, что и почему происходит.
            По памяти как минимум Sublime Merge умеет делать патч-коммиты. Вроде ещё каким-то клиенты были, но сейчас не вспомню.
              0

              Спасибо, хорошее уточнение. А Sublime merge умеет их делать на уровне строк? Потому что например Atlassian SourceTree умеет, но на уровне hunks, т.е. таких законченных кусков изменений.

                0

                PHPStorm тоже умеет коммитить отдельные куски изменений.
                В TortoiseGit тоже можно делать частичные коммиты, в том числе и на уровне строк, но там каждый раз нужны небольшие танцы с бубном (Restore after commit / Mark / Leave).

                  0

                  PHPStorm только сплошные куски может, вроде как. Не может 3 строки из 5 подряд идущих в диффе и уж точно не может редактировать то, что коммитится без редактирования рабочей копии. По крайней мере года два назад такое было

                    0

                    Ну да, но там это и не особо проблема. Отредактировал, закоммитил, нажал Ctrl+Z, еще раз закоммитил.

                  +2

                  Есть git-gui (пакет именно так и называется). Им можно стейжить как целые куски (hunk), так и построчно. Но это если отдельным инструментом. А вообще всё достаточно технично делается в JetBrains (вроде во всех их IDE оно одинаково) — если нужно распилить какой-то коммит — делаем интерактивный ребейс, начиная с предыдущего перед ним коммита (правый клик — interactively rebase from here). Ставим на нём точку edit, запускаем rebase. Потом кликаем в целевой коммит (который надо распилить) и выбираем compare with local. А дальше уже как фантазия пойдёт. Можно кусками, можно строчками, можно хоть отдельными символами. Можно вообще всё переписать (например, если в конечном результате "А" стало "Б", то в промежуточном можно "А" заменить на "Г", а следующим шагом уже "Г" заменить на конечное "Б".). Несомненный плюс такого подхода — каждое изменение можно протестировать отдельно. Мы имеем "предыдущее" состояние и просто накатываем на него "текущее" отдельными кусками. Это не "полукоммит", когда все изменения на самом деле уже в коде, а мы просто выбираем, что в этот коммит пойдёт половина строк, а в следующий — вторая половина, а именно полноценный коммит. Если мы решили взять в него ровно половину изменений — мы можем прогнать тесты и убедиться в работоспособности. И потом просто продолжить ребейс. Вторая половина изменений подтянется сама; если в промежуточном коммите были какие-то нетривиальные изменения (больше, чем просто выбор отдельных чанков или строчек) — надо будет по горячим следам поправить эти конфликты. И всё.

                  0

                  … и это очень удобно! После ребейза некоторые коммиты могут вообще схлопнуться из сотни строк в пару чанков, а то и вообще исчезнуть (хотя последнее часто вводит в ступор IDE (например, от JetBrains), которые пытаются контролировать процесс и вдруг не могут понять, куда внезапно исчез коммит).


                  Например, в ветке сделали переименование какой-нибудь сущности, вкоммитали. В это время в мастере провели ту же операцию. Последующий ребейс при применении изменений обнаружит, что коммит "уже применён" и просто его отбросит.

                  –5
                  Неплохая и информативная статья, спасибо! Но вот к декомпозиции задач нужно относиться более ответственно: видимо, ваш скрам-мастер просто не справляется со своей работой, и software planning проходит не так, как должно. Практически любую задачу можно разбить на «атомарные» подзадачи, при этом «убиваются» несколько «зайцев» сразу: небольшие, разумные коммиты, которые легко ревьюировать (или как правильно по-русски сказать, контролировать?), достигается хороший sprint velocity, легче планировать свое время, всегда можно заняться тем, к чему лежит душа в данный момент, начинающим же разработчикам так вообще «лафа», все разложено по полочкам, просто и понятно (потому им нужна помощь более опытных товарищей в декомпозиции и планировании спринта). Знающий scrum master и опытный software architect — и вам практически никогда не придется пользоваться git push --force.
                    +6

                    Может вы не знали, но не везде скрам. А там, где скрам — команда решает как задачи бить, а не скрам-мастер

                      +3
                      Декомпозиция сложных задач на атомарные — это не какая-то исключительная особенность скрама, а очень хорошая практика профессиональной разработки программного обеспечения. Вне зависимости, какой методологией вы пользуетесь, небольшие содержательные коммиты, которые легко проверить, это благо, и это отнюдь не мое личное мнение.
                      И я написал, что скрам-мастер (если это скрам) должен добиться, чтобы software planning состоял не из blah-blah-blah и шуточек, а из реальной работы, в том числе, и правильной декомпозиции. Многие программисты просто не умеют правильно это делать, потому (как я выше и написал), им порой нужна помощь.
                      0
                      Практически любую задачу можно разбить на «атомарные» подзадачи, при этом «убиваются» несколько «зайцев» сразу: небольшие, разумные коммиты

                      Это решение технической проблемы административными средствами.

                      +3

                      Проекту год с небольшим, постоянно пишут код от 1.5 до 2.5 чел. Ребейзим постоянно, мерджи 99% только FF, история прекрасна и чиста. Вероятно, всё-таки большое значение имеет размер команды — чем меньше, тем проще так делать.
                      Лично я пользуюсь как IDEA, так и TortoiseGit — часть действий мне в нём делать привычнее.


                      Из лайфхаков. Если история ветки богатая на MR с базовой веткой и ребейз сложен из-за конфликтов, можно сделать Soft Reset на базовую ветку и закоммитить текущее состояние как единственный засквошенный коммит.

                        +4

                        Хороший лайфхак!


                        Что до чистоты истории, то в перспективе девятилетней кодбазы, которую мы развиваем в iFunny, чистота не так важна, как понятность. Если фича сложная, то разбить её на несколько атомарных коммитов — гуманно по отношению к инженеру, который будет смотреть в этот код через 3-4 года. Squash-коммит приведёт его к тикету на разработку фичи, а почему было сделано именно такое изменение в коде — не факт что станет ясно.

                          –1
                          Согласен, но достигнув состояния из одного коммита, можно не останавливаться и далее разбить его на отдельные :)
                        +2
                        git commit --amend --no-edit
                        git rebase -i origin/master
                        git push -f


                        Ежедневный набор)

                        Чесно не могу представить, как можно запрещать такую простую вещь как force push, в мастер при нормальной политике так не влить, а с другими ветками проблем нет.

                        На крайняк всегда есть reflog, благо git никогда ничего сам не забывает.

                        Еще очень полезно после работы над большой фичей, если не хочется сквошить все в один коммит, сдлеать ресет на корень фичи и потом из полученного собрать отдельные коммиты, в которых будут локализованы атомарные изменения.

                        Сейчас еще пробуем жить с Rebase & Push при влитии ПР в основную ветку, на выходе линейная история. А это резко облегчает жизнь.
                          +2
                          потому что как ещё можно охарактеризовать реквест из 50 коммитов с почти 2000 изменённых строк? И как его, спрашивается, ревьюить?

                          По одному коммиту за раз, поняв общую сущность. А как ещё?
                          Если неоправданное дробление — да, жаловаться на него. Но таки бывает нужно иметь и 50 коммитов в цепочке, хоть и редко.


                          Ниже вы что-то подобное написали, но IMHO недостаточно явно.


                          Честно говоря, у меня ушло два дня просто на то, чтобы заставить себя приступить к этому ревью. И это нормальная реакция для инженера

                          Для красного словца — сойдёт. Если же всерьёз — предупредить, что требуется время; обычно в таких системах есть построчные замечания, но если нет — завести файлик и писать в него (а заодно пожаловаться на отсутствие критичного функционала). Вот это будет "нормальной реакцией для инженера".


                          Поэтому самым простым будет перед окончательной интеграцией в релиз схлопнуть все коммиты ветки в один при помощи git rebase.

                          Этот рецепт должен сопровождаться обязательным "только если все коммиты одного типа" (изменение функциональности, правка стиля, авторефакторинг, автоформатирование и т.п.) Если разных, за одно только предложение схлопнуть такие коммиты в один, если изменений больше чем на 20 строк — следует разжаловать в интерны с обязательным изучением всего IT с нуля, лучше на Algol-60 на бумаге. Исключение: если исходная ветка сохраняется и в схлопнутом коммите на неё есть ссылка — но всё равно смысл в таком появляется разве что в супертолстой базе.


                          Но есть способ облегчить жизнь товарищу.

                          Я честно не понимаю, при чём тут push -f из заголовка. Всё это можно было сделать и до первой публикации.


                          Если же речь про переделку после цикла замечаний — да, push -f для временных веток для pull requestʼов это нормально (хотя можно и сделать ветку типа my_request_v2).


                          Вернёмся наконец к git rebase --interactive.

                          Это сверхважный инструмент, но опять же к forced push имеет слабое отношение. И имело бы смысл создать статью именно по применению workflow с ним на основании цикла "выдал цепочку на ревью — получил замечания — переработал — goto 10".


                          Помните, что добросовестный ревьюер возможно клонирует ваш код к себе локально, чтобы иметь возможность смотреть на него через IDE и запускать тесты.

                          Если ведётся история в стиле reflog (а нормальные реализации ведут её и на сервере), то в течение некоторого времени (стандартно 90 дней) можно найти commit id от предыдущего push и вытащить по нему.


                          Ну а Gerrit, например, хранит такие вещи вечно (если явно не чистить).

                            +1

                            А как же git push +?

                              0

                              А что с ним? Я не знал про такой синтаксис, и, честно говоря, сходу не разобрался.

                                0

                                Аналог -f, и чуть более аккуратный по последствиям (-f без указания конкретного ref может обновить все ветки, а +master:master, например, только одну). Для тематики данной статьи разница несущественна, но для безопасности работы вариант через плюс чуть лучше.

                              +3
                              К счастью, эту проблему разработчики git тоже предусмотрели, введя команду сборки мусора git gc --prune.

                              Надеюсь этого никого не научит использовать git gc --prune, ведь даже после переписывания истории её можно восстановить через reflog – пару раз очень сильно выручало!


                              Про переписывание добавил бы мини-хак: можно добавлять изменения не только в последний коммит через -a, но и любой старый через git commit -a --fixup=<hash>. А вместе с этим можно добавить в глобальный .gitconfig настройку autosquash = true – теперь после запуска git rebase --interactive коммиты сами будут в нужном порядке и с нужными «флагами»

                                0

                                "-а" означает добавить все изменённые файлы в коммит. Работает только с теми файлами, которые уже есть в репо, и не работает с вновь созданными.

                                +1

                                Не вижу особых проблем с push -f. Это такой же инструмент, как и другие тулзы гита, и у него есть своя область применения, причём далеко не сказочно-экстремальные ситуации вроде "случайно впушил в паблик кошелёк с сотней биткойнов".


                                Ситуация один (капитан-очевидность) — собственный непубличный клон. Клон где-нибудь на хецнере/амазоне/яндекс-диске/роутере, который прописан в remotes как ssh://.... Кому вообще какое дело, что там лежит и как обновляется? Это как раз для коммитов вида feature_day1..feature_month5, чтоб потом их переформатировать и выкинуть.


                                Ситуация два: такая же ветка, но в публичном репозитории. Не для ревью, а просто как бэкап.


                                Ситуация три: mr/pr, созданный исключительно ради автоматизированных тестов CI. Накоммитал, напушил, дождался результатов (что все тесты прошлись) — и всё, на этом мавр сделал своё дело. Можно получившийся результат вообще не мержить, а напилить на коммиты прямо в мастер (если это допустимо).


                                Все ситуации имеют под собой основную идею: Я ХОЗЯИН этого репозитория/ветки/реквеста, и делаю, что хочу. Вы можете наблюдать за изменениями, но это всё ещё не public domain; код не обсуждается, предложения не принимаются. Дошло дело до ревью — вот с этого момента решения нужно принимать уже совместно с остальными участниками. И формировать какой-то совместный чеклист, типа "Вася собрался переписать последний коммит. Если кто его уже запуллил — по команде делаем git fetch, потом git reset --hard на новый remote/master"

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

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