Всем привет! Меня зовут Маша, и я Golang Backend Developer в компании Ozon. В этой статье я хотела бы поговорить о теме, так или иначе объединяющую все сферы нашего любимого мира IT. А именно — VCS Git.

Без системы контроля версий сейчас невозможно представить ни один проект. Это оплот любой кодовой базы и мощнейший инструмент, с помощью которого эту базу можно изменять и отслеживать. Однако нередко чистотой истории изменений пренебрегают, полагаясь на старое доброе «И так сойдёт!», абсолютно игнорируя при этом сложность понимания и поддержки такой истории в будущем.

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

Внимание! Данная статья актуальна для проектов с Git Feature Branch Workflow, где при мердже feature branch в master не используется squash коммитов.

Проблема «грязных» историй

Вот бывает, смотришь ты историю коммитов проекта — и всё, казалось бы, хорошо и понятно, но вдруг натыкаешься на такое: 

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

Как же можно было оставить историю Git в таком состоянии? Так много коммитов, у всех из них одинаковые бессмысленные названия, а в коде, скорее всего, изменений на одну строку. Те, кто будут смотреть эту историю, вряд ли смогут понять, что тут происходило. Ведь все коммиты можно было объединить в один, содержащий итоговые правки и дать ему осмысленное название – иными словами, сохранить историю Git в чистоте.

К сожалению, пример выше — это не плод моего воображения, а довольно частая практика. Особенно страшно видеть подобное в исполнении опытных разработчиков. Грязная история часто остаётся, когда приходится раз за разом исправлять что-то небольшое, или экспериментировать с кодом до тех пор, пока он не заработает в тестовом окружении.

Но ведь это так просто — не делать подобных ошибок! Давайте разберёмся, что же такое Clean Git History, в чём её преимущества, как можно её добиться, а также на практике выйдем за рамки пресловутых commit/push и научимся сохранять историю Git в чистоте.

Концепт Clean Git History

Clean Git History, или чистая история Git — это такая история изменений проекта, которая соответствует трём ключевым критериям:

1. Рефлективность

Описание каждого коммита истории должно отражать сущность его изменений.

2. Атомарность

Каждый коммит должен быть атомарен и логически завершён. У него не должно быть зависимостей от других коммитов настолько, насколько это возможно.

3. Прозрачность

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

Основные правила ведения чистой истории

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

1. Давать коммитам осмысленные названия

Описания коммитов всегда должны четко и кратко отражать суть всего, что было вами сделано за одну итерацию. Даже если изменения были незначительными, это не повод оставлять Git message без внимания. Если же, наоборот, хочется описать все детали, стоит использовать первую строку для краткого резюме, а подробную информацию дать уже на последующих.

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

2. Следить за количеством коммитов

Не стоит создавать десятки коммитов со схожими правками, однако не нужно и пытаться уместить в один изменения на тысячи строк. Коммитов должно быть ровно столько, сколько логически завершённых блоков было в вашей работе, — не больше и не меньше.

3. Разделять логику коммитов

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

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

Таким образом, отдельный коммит следует использовать при:

  • написании бизнес-логики;

  • создании тестов;

  • рефакторинге кода;

  • генерации кода (например, из файлов proto и т. д.).

Как прийти к Clean Git History

Итак, мы имеем некую историю коммитов (уже существующих или создающихся в данный момент) и хотим её изменить таким образом, чтобы она соответствовала критериям Git Clean History.

Давайте теперь на практике посмотрим, какими способами это можно сделать.

Перед началом

Для наглядности я создала небольшой проект и сделала в нём пять коммитов с соответствующими названиями.

$ git log --oneline --graph --decorate --all

* 3887aa2 (HEAD -> feature-branch) commit 5
* 884fe33 commit 4
* eb38304 commit 3
* 9c2daaf commit 2
* fb7ab41 (main) commit 1

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

Основные команды я буду показывать через CLI, а также дам к ним документацию для дальнейшего изучения. Если вам сложно или непривычно работать с Git в CLI, в интернете можно легко найти информацию о реализации каждого из описанных ниже приёмов в вашей любимой IDE.

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

С большой силой приходит большая ответственность

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

Если вы ещё не пользовались приведёнными командами, прежде чем делать это с продовой кодовой базой, определенно стоит потренироваться в их выполнении на тестовом репозитории — до полного понимания всех принципов. 

Основные приёмы

Команды и приёмы для ведения чистой истории Git здесь представлены в готовом к использованию виде. Но вы, безусловно, всегда вольны изменить их на свой вкус.

Force push

Документация: 

Флаг --force — костяк всех приёмов для поддержания чистоты истории Git. Его мы используем с командой push, когда хотим перезаписать историю Git в удалённом репозитории и сделать её такой же, как локальная.

Вместо --force можно пользоваться более безопасным флагом --force-with-lease. Он работает так же, однако не даст вам перезаписать удалённую историю, если кто-то из коллег добавил туда новые коммиты.

$ git push --force-with-lease origin feature-branch

Применение:

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

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

Внимание! Перед пушем с флагом обязательно проверьте ветку, в которую вы пушите. Ни в коем случае это не должна быть master!

Почему?

Основная задача ветки master — гарантировать, что в ней находится 100% рабочий проверенный код. Благодаря этому её всегда можно использовать как референс. Соответственно, все изменения, вносимые в эту ветку, должны проходить тщательное ревью и тестирование. --force push же внесёт изменения без проверок, в результате чего, во-первых, есть шанс сломать проект, а во-вторых, ваши коллеги, не знающие об изменениях, продолжат работать со старой версией репозитория.

Amend

Документация: 

Это, наверное, один из самых используемых инструментов среди приведённых мной. Данный флаг позволяет исправить последний сделанный коммит, а также изменить его commit message (правки могут ограничиваться лишь этим). Ваши старые изменения останутся, если не будут затронуты новыми. В противном случае они перезапишутся.

 git commit --amend

Пример:

# индексируем изменённые файлы
$ git add .

# коммитим новые изменения, заодно меняя commit message
$ git commit --amend -m "better commit message"

[feature-branch 7aa55d8] better commit message
 Date: Sun Apr 30 18:58:15 2023 +0300
 1 file changed, 1 insertion(+), 1 deletion(-)
 
# проверяем наши коммиты и видим изменённый commit message последнего коммита
$ git log --oneline --graph --decorate --all

* 7aa55d8 (HEAD -> feature-branch) better commit message
* 97f9c9f commit 4
* eb38304 commit 3
* 9c2daaf commit 2
* fb7ab41 (main) commit 1

Применение:

Использовать --amend можно в самых разных ситуациях: когда нужно изменить commit message или код последнего коммита, избежать добавления грязных (пустых) коммитов при тестировании или изменения одного-двух символов в коде.

Reset

Документация: 

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

$ git reset [<commit>]

Чтобы избежать конфликтов, используйте reset с флагом --hard. Благодаря этому все незакоммиченные локальные изменения пропадут и конфликтов, соответственно, не будет. Однако стоит заранее позаботиться о незакоммиченном коде (см. раздел «Дополнительные приёмы»).

$ git reset --hard [<commit>]

Пример:

# хотим отбросить два последних коммита
$ git reset --hard HEAD~2

# видим, какой коммит теперь ведущий
HEAD is now at eb38304 commit 3

# убеждаемся, что в истории стало на два коммита меньше
$ git log --oneline --graph --decorate --all

* eb38304 (HEAD -> feature-branch) commit 3
* 9c2daaf commit 2
* fb7ab41 (main) commit 1

Применение:

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

Revert

Документация: 

Эта команда позволяет отменить изменения одного определённого коммита посредством создания нового с обратными изменениями. 

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

$ git revert [<commit>]

Вам будет предложено выбрать название для нового коммита, но можно оставить стандартное: Revert “[<reverted commit message>]”.

Пример:

# хотим отменить изменения коммита eb38304
$ git revert eb38304

# видим, что в истории появился новый коммит с обратными изменениями
$ git log --oneline --graph --decorate --all
* dd742c3 (HEAD -> feature-branch) Revert “commit 3” 
* 3887aa2 commit 5
* 884fe33 commit 4
* eb38304 commit 3
* 9c2daaf commit 2
* fb7ab41 (main) commit 1

Применение:

Представим, что вы решили удалить ненужный код в одном из коммитов, однако спустя время оказалось, что код всё-таки нужен и стоит его вернуть. Тут вам идеально подойдет команда revert.

Interactive rebase

Документация: 

Interactive rebase — это самое мощное оружие в нашем арсенале. С его помощью можно к каждому из коммитов применить определённое действие, так или иначе направленное на изменение истории.

Чтобы начать процесс, нужно выполнить соответствующую команду с флагом --interactive и указать коммит, с которого мы хотим начать процесс rebase. Для изменения будут доступны коммиты, следующие за указанным.

$ git rebase --interactive [<commit>]

Пример:

# например, начнём процесс rebase с четвёртого коммита от ведущего
$ git rebase --interactive HEAD~4
# следующая команда аналогична предыдущей
$ git rebase --interactive fb7ab419 # fb7ab419 — хеш первого коммита (commit 1) 

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

Также там будет памятка по использованию команды interactive rebase с указанием того, какие действия можно совершать в отношении коммитов. Сами же действия выполняются последовательно сверху вниз.

# файл может выглядеть так:
pick 9c2daaf commit 2
pick eb38304 commit 3
pick 97f9c9f commit 4
pick 8614442 commit 5
# дальше идёт памятка 

Мы видим, как и ожидали, что коммиты расположены в хронологическом порядке. Но что это за слово “pick” слева от каждого из них? pick — это как раз одно из тех действий, которые мы можем совершать в отношении коммитов. Для каждого действия есть определённая команда. Чтобы её выполнить, нужно указать её название слева от коммита. После того как мы выбрали желаемые действия в отношении коммитов, файл нужно сохранить — и процесс rebase начнётся.

Теперь рассмотрим каждое действие подробно.

Pick

Использовать данный коммит. 

Если для коммита указано действие pick, значит, он останется без изменений. Именно поэтому в самом начале процесса rebase у всех коммитов стоит “p”.

p, pick [<commit>]

Reword

Использовать данный коммит, но поменять его commit message.

По-простому — переименовать коммит. Само новое сообщение к коммиту на этом этапе нигде писать не нужно. После того как вы сохраните файл, у вас спросят, как вы хотите назвать коммит.

r, reword [<commit>]

Squash & Fixup

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

С помощью этих команд можно объединять сразу несколько коммитов, указав команды для каждого из них.

squash и fixup делают одно и то же, но с небольшим отличием: 

  • при squash commit messages всех соединённых коммитов будут объединены с описанием коммита, в который попадут все изменения; 

  • при fixup у коммитов, подвергшихся слиянию, будут удалены commit messages. Commit message останется только у самого верхнего коммита, в который попадут все изменения.

s, squash [<commit>]
f, fixup [-C | -c] [<commit>]

Edit

Использовать данный коммит, но остановиться для действия amend.

Эта команда позволяет во время процесса rebase остановиться на выбранном коммите и произвести над ним уже знакомую нам операцию amend.

e, edit [<commit>]

Все окошки процесса rebase закроются — и файлы перейдут в состояние выбранного коммита. После этого необходимо внести изменения в код и всего лишь проиндексировать их. Создавать новый коммит с флагом --amend не нужно — это произойдёт автоматически.

git add .

После этого следует продолжить процесс rebase.

git rebase --continue

Drop

Удалить коммит. 

Также это можно сделать, просто удалив всю строку коммита.

d, drop [<commit>]

Reorder

Изменить порядок коммитов. 

Для этого нет специальной команды, так как сделать это можно, меняя строки коммитов местами.

Пример

Начнём interactive rebase, указав хеш коммита, идущего перед тем, начиная с которого мы будем менять историю.

$ git rebase --interactive fb7ab419 # fb7ab419 — хеш первого коммита (commit 1)

Откроется файл со всеми коммитами, начиная со второго (commit 2), поскольку он идёт после указанного первого (commit 1). 

Представим, что в результате interactive rebase мы решили совершить над коммитами следующие действия: 

r 9c2daaf commit 2 # reword
p eb38304 commit 3 # pick
s 97f9c9f commit 4 # squash с предыдущим
d 8614442 commit 5 # drop

После сохранения файла нас попросят указать новое commit message для коммита с командой r. Редактируем файл и сохраняем:

better commit 2 message
# Please enter the commit message for your changes. Lines starting
# статус rebase ...

Ниже в комментариях файла с новым commit message будет указан статус процесса rebase: какие команды будут выполнены далее, сколько их осталось, какие изменения коснутся кода и т. д.

После будет предложено изменить commit message у коммита с командой s, а также коммита выше (с которым объединится указанный коммит).

# This is a combination of 2 commits.
# This is the 1st commit message:

edited commit 3

# This is the commit message #2:

edited squashed commit 4

# Please enter the commit message for your changes. Lines starting
# статус rebase ...

Если бы мы выбрали fixup вместо squash, нам бы предложили изменить commit message только у самого верхнего коммита, так как все объединяющиеся коммиты свои сообщения потеряли бы.

После успешного завершения процесса rebase мы увидим сообщение о его результатах:

[detached HEAD 15433c0] better commit 2 message
 Date: Sun Apr 30 15:46:20 2023 +0300
 1 file changed, 2 insertions(+)
[detached HEAD 578ddda] edited commit 3
 Date: Sun Apr 30 15:46:27 2023 +0300
 1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/feature-branch.

Можно убедиться, что история теперь выглядит так, как мы и задумывали. А именно: первый коммит изменения не затронули, у второго изменился commit message, четвёртый был объединён с третьим с сохранением commit messages у обоих, а пятый был отброшен:

$ git log --graph --decorate --all
* commit 578dddaf08cba7707decf79446942b4cd962c766 (HEAD -> feature-branch)
| Author: John Doe <john_doe@gmail.com>
| Date:   Sun Apr 30 15:46:27 2023 +0300
|
|   edited commit 3
|
|   edited squashed commit 4
|
* commit 15433c09b140710f9b8347fcb1b48284c9f80cd6
| Author: John Doe <john_doe@gmail.com>
| Date:   Sun Apr 30 15:46:20 2023 +0300
|
|   better commit 2 message
|
* commit fb7ab419cb936c099def554f07131c34a8044ce9 (main)
| Author: John Doe <john_doe@gmail.com>
| Date:   Sun Apr 30 15:28:44 2023 +0300
|
|   commit 1

Дополнительные приёмы

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

Stash

Документация: 

Бывает, работаешь над новым функционалом, — и вдруг срочно нужно исправить что-то в другой ветке. В такой ситуации (и во многих других) на помощь придёт команда stash.

Она позволяет убирать изменённые файлы в специальное место внутри директории .git на временное хранение. Таким образом, файлы репозитория примут состояние последнего коммита. Позже, когда понадобится, вы так же легко сможете применить к коду сохранённые ранее изменения.

Работает stash по принципу стека. Поэтому добавляться и применяться её элементы будут в соответствующем порядке (LIFO).

Создание элемента stash:

# создадим элемент stash
$ git stash 

# создадим элемент stash с сообщением (это считается хорошей практикой) — прямо как commit message 
$ git stash save "some changes message"

Просмотр элементов stash:

# посмотрим, какие изменения лежат в stash-стеке
$ git stash list

stash@{0}: On feature-branch: some changes message
stash@{1}: On feature-branch: refactoring in process
stash@{2}: On feature-branch: almost implemented new feature

Применение элемента stash:

# применим изменения верхнего элемента stash
$ git stash pop

# применение изменения определённого элемента stash (индекс можно посмотреть в git stash list)
$ git stash pop stash@{2}

# создание патча на основе изменения элемента stash (про файлы patch см. ниже)
$ git stash show -p stash@{2} > my_feature.patch

Patch

Документация: 

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

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

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

Создание файла patch:

# создадим файл patch из локальных изменений
$ git diff > my_feature.patch

# создадим файл patch из изменений коммита
$ git format-patch [<commit>]

Применение файла patch:

# применим файл patch
$ git apply my_feature.patch

# применим файл patch одновременно с созданием коммита на основе изменений кода
# (будет работать только с файлом patch, созданным с помощью команды format-patch)
# флаг --signoff в истории Git указывает, кто применил patch
$ git am --signoff < my_feature.patch

Выводы

В завершение, хотелось бы еще раз кратко резюмировать, почему же так важно придерживаться чистой истории Git:

  • история изменений проекта будет понятна и логична;

  • в ней будет легко ориентироваться и при необходимости так же легко изменять;

  • в случае неполадок будет легче откатиться;

  • и в конце концов, история коммитов заслуживает права быть чистой ничуть не меньше, чем того заслуживает сам код!

Надеюсь, теперь наши истории Git станут немножечко чище, а мир — немножечко лучше :) Спасибо за внимание!


Все приведённые в статье примеры историй Git являются вымышленными, однако, к сожалению, основанными на реальных событиях.

Полезные ресурсы

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


  1. saboteur_kiev
    15.08.2023 13:50
    +16

    Простите, но вы предлагаете неверный выход из известной ситуации.

    "Для наглядности я создала небольшой проект и сделала в нём пять коммитов с соответствующими названиями."

    Вот тут главная ошибка. НИКТО не должен коммитить в мастер. В него должны делать пулл реквесты.

    Мне даже сложно найти сейчас проекты, где используется чистый гит, а не гит с любой code-review система, настолько Code-review сейчас распространен.

    Все эти bitbucket, github, gitlab и так далее - все предлагают пользоваться Pull Request, который перед merge в мастер как раз и будет грамотно оформлен, просмотрен, сжат (merge squeeze), проверен на конфликты и добавлен в основную ветку единым коммитом с правильным коммит мессадж.

    Почитайте также git workflow, и посмотрите несколько вариантов. Это и есть правильный путь.

    А внутри своей feature ветки, разработчик вполне имеет право делать хоть десять коммитов с непонятным сообщением. Он человек, он может допускать опечатки, он должен иметь возможность проверить какие-то мелочи, если проект создан так, что билд и автотесты нельзя выполнить на своей локальной машине, а где-то в CI.


    1. kjushka
      15.08.2023 13:50
      +8

      Начну с того, что методологии Git Workflow относятся к организации рабочего процесса внутри гита, зачастую даже к организации ветвления, но никак не к наименованию и количеству коммитов. Это во-первых

      Во-вторых, ревью хоть и требует качественной проверки, но человеческий фактор никто не отменял, тут как ни крути. Тем более автор отмечает

      Внимание! Перед пушем с флагом обязательно проверьте ветку, в которую вы пушите. Ни в коем случае это не должна быть master!

      Да и в целом в статье не говорилось ни слова о том, что коммиты нужно сразу писать в мастер. Однако, как-то же коммиты попадают в мастер :)

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

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


      1. LittleRunaway Автор
        15.08.2023 13:50
        +1

        Привет!
        Спасибо большое за комментарий!
        Да, вы как раз и написали обо всём, что я хотела сказать :)

        И отличное замечание о небольших проектах. Действительно, так как методологий git workflow бывает много, одна из них как раз и предполагает пуш сразу в мастер. Поэтому это всё скорее не про то, как правильно и неправильно, а про то, от чего будет больше пользы в определенном проекте и с чем самим разработчикам удобнее работать.


    1. LittleRunaway Автор
      15.08.2023 13:50
      +2

      Привет!
      Спасибо большое за комментарий!

      Да, полностью с вами согласна, при описанном вами подходе, когда feature branch мерджится в master со сквошем всех коммитов, данная статья будет скорее неактуальна.

      Однако git workflows бывают ведь разные и каждая команда вольна выбирать свой. Это дело вкусовщины и устоев команды. Например, у нас тот самый GitFlow Git workflow, о котором вы говорите, единственное отличие - мы не сквошим коммиты при мердже в master. Поэтому нам так важно, чтобы история коммитов, которая потом окажется в мастере, была чистой.

      Я так же поддерживаю запрет коммитов напрямую в мастер (в обсуждаемых воркфлоу), поэтому сделала ремарку об этом в блоке про запрет пушей сразу в мастер. Там же и расписала, что весь код, оказывающейся в мастере, должен пройти ревью и тестирование - это как раз про pull (merge) реквесты.

      А разработчик тоже, конечно, имеет права работать со своей веткой как ему нравится и удобно. Это отлично работает, если потом все его коммиты все равно сожмуться в один, как вы и описали. Но если в команде не принято мерджить в мастер со сквошем коммитов, то тут уже, после всех экспериментов разработчика, стоит привести историю коммитов ветки в аккуратный вид. Именно о том, как это проще сделать я и написала.

      Это хорошее замечание, я добавлю заметку о том, для какого git workflow подойдет данная статья, сейчас это действительно не очень очевидно.
      Если вдруг я что-то упустила или не верно поняла - пишите, я буду только рада улучшить материал :)

      Большое спасибо!


    1. ryanl
      15.08.2023 13:50

      Ну OSS pet-проекты можно поначалу и в мастер настреливать, по 150 коммитов за 2 недели)


      1. domix32
        15.08.2023 13:50

        Не в одном же PR, право слово.


        1. Andrey_Solomatin
          15.08.2023 13:50

          PR он про ревью. Если не ревью то зачем это ограничение?

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


    1. event1
      15.08.2023 13:50

      Вот тут главная ошибка. НИКТО не должен коммитить в мастер. В него должны делать пулл реквесты.

      Я работаю в Gerrit и коммичу в мастер. Каждый коммит проходит ревью конечно. Почему нельзя использовать такой процесс остаётся загадкой. Ему даже модное имя дали — trunk based development


      1. domix32
        15.08.2023 13:50
        +3

        То есть у вас master это dev, а релизы в отдельной ветке живут? В таком случае у вас релизная ветка должна быть защищённой и никто в неё не должен коммитить напрямую.

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


      1. Andrey_Solomatin
        15.08.2023 13:50

        Если вы делает ревью, то вы не коммитете в мастер. `git push origin HEAD:refs/for/master` создаёт на каждый ваш пуш новую ветку.

        Откройте любое ревью здесь и нажмите DOWNLOAD, вы увидите что там под капотом ветки. https://gerrithub.io/c/redhat-openstack/infrared/

        При ревью вы вообще не мерджите, вы ставите флаг что можно мерджить и Геррит делате это сам, когда все остальные условия выполнены. (родительские ветки вмерджены, топики готовы).


  1. YChebotaev
    15.08.2023 13:50

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


    1. sergiodev
      15.08.2023 13:50
      +6

      Ага, потом открываешь git blame в IDE напротив какой-то строчки, и там везде сообщения вроде "fix bug", и не понятно вообще к какой задаче оно относилось и с какой целью было сделано.


      1. nickolaym
        15.08.2023 13:50
        +2

        блейм будет показывать на мержкоммит
        а в мержкоммите будет ссылка на пулреквест
        а в пулреквесте хорошие люди пишут общее пояснение и дают ссылку на тикет


        1. storoj
          15.08.2023 13:50
          +4

          ... нажимаешь ссылку – а там access denied, или ссылка мёртвая, или уже всё мёртвое – и таск-трекер, и репозиторий.

          одной ссылкой сыт не будешь


  1. ritorichesky_echpochmak
    15.08.2023 13:50
    +1

    Помимо --force-with-lease есть ещё --force-if-includes - недостаточно хорошо задокументированная и освещённая но очень нужная фича

    гитхуки могут локально давать по рукам при попытке коммитать напрямую в мастер или писать безинформативное fix в коммите


  1. sa2304
    15.08.2023 13:50
    -2

    Спасибо за полезную статью.

    НИКТО не должен коммитить в мастер.

    Поддерживаю - в настройках проекта нужно запрещать коммиты в master.

    За push --force в origin предлагаю расстреливать на месте :) Пушим в репозиторий только когда все локально причесали и на 200% уверены.

    А внутри своей feature ветки, разработчик вполне имеет право делать хоть десять коммитов с непонятным сообщением

    Только до тех пор, пока это его локальная ветка. Репозиторий - не винегрет, а код единого проекта. Дисциплина должна быть как в стиле кода, так и в истории изменений.

    Код пишут, чтобы читать. Историю Git пишут, чтобы читать. Через год в вашу фичу полезет другой и проклянет вашу лень.

    Читайте ссылки по запросу git commit message best practices и чистой вам Git-истории :)

    8 правил, которые пригодятся при описании Git-коммитов

    Отличная идея - добавлять номер задачи в начало сообщения: облегчает чтение, когда веток много. prepare-commit-msg hook автоматически достанет номер задачи из имени ветки и добавит к сообщению, а commit-msg hook умеет бить по рукам за плохие сообщения.

    Отрабатываем Git hooks на автоматизации commit message


    1. nickolaym
      15.08.2023 13:50
      +5

      push -f - это прямой результат действительно совместной работы.

      Если вы наделали изменений, и ваш коллега параллельно наделал изменений, которые могут повлиять на ваши, - то у вас есть два путя:

      1. сделать спагетти из неизменяемой истории: мержкоммит свежего мастера в вашу ветку, героическую борьбу с массовыми конфликтами и обратно мержкоммит ваших трудов в мастер, и пусть затем люди читают всё это и думают - вот здесь вы исправляли баг по существу, или же исправляли конфликты, или исправляли последствия кривых исправлений конфликтов?

      2. сделать ребейз своей ветки на свежий мастер, исправить конфликты (в каждом вашем коммите лично вам будет очевидно, какие правки как накатывать), push -f ветки в ориджин, и уже предлагать пулреквест с мержкоммитом.

      В итоге, мержкоммиты есть только в мастере и черипики этих мержкоммитов из мастера в релиз-кандидаты, и больше нигде и никак.


      1. sa2304
        15.08.2023 13:50

        Мы с коллегой работали так: завели ветку dev - в нее мержили те feature, что запланированы на следующий релиз. В случае конфликтов мы садились у одного монитора и разрешали конфликты локальным merge, а после коммитили.

        Проектировщики самолетов прячут рычаг катапультирования в кабине пилота так, чтобы пилот случайно его не зацепил. Флаг --force - тот же рычаг катапультирования - дергать без нужды не стоит :)

        Команды на удаление или перезапись - всегда риск. Удаленный репозиторий у вас один, берегите его.


        1. aspect04tenor
          15.08.2023 13:50

          Удалённый репозиторий, как правило, трудно сломать, особенно, если правильно настроены права доступа. Максимум можно сломать свою ветку, в которую вы коммитите. Не понимаю такого страха перед форсом, если вы знаете что делаете, и, особенно, если удалённый репозиторий правильно настроен, то никаких проблем с его использованием быть не должно. Поправьте меня, если я ошибаюсь.

          П.С. И «форс пуш в ориджин» выше — имели в виду в мастер? Что плохого чтобы пушить всё что угодно в свою ветку? А если своих веток нет (нельзя создать), значит, и пушнуть не должно получиться. Значит, форкнуть и создать PR со своего репозитория. Спасибо что хоть туда разрешили форспушить:)


    1. Dolios
      15.08.2023 13:50
      +3

      За push --force в origin предлагаю расстреливать на месте :)

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

      Пушим в репозиторий только когда все локально причесали и на 200% уверены.

      1. У вас винты на локальной машине никогда не умирали?

      2. У вас с веткой всегда 1 разработчик только работает?

      3. Как оттестировать промежуточные результаты, если пушить нельзя, а тесты запускаются на сервере?

      Только до тех пор, пока это его локальная ветка. Репозиторий - не винегрет, а код единого проекта. Дисциплина должна быть как в стиле кода, так и в истории изменений.

      После мержа фичи в мастер ветка удаляется, зачем она нужна дальше?


      1. sa2304
        15.08.2023 13:50

        если правильно настроены права доступа ... если вы знаете что делаете ... если удалённый репозиторий правильно настроен

        Увы, закон Мерфи все еще работает :) Хорошо не всё и не всегда.

        У вас винты на локальной машине никогда не умирали?

        Моим винтам умирать строго воспрещается :) А если серьезно, то у нас пушили по несколько раз в день, но предельно осторожно.

        У вас с веткой всегда 1 разработчик только работает?

        Да, одна ветка - одна фича.

        Как оттестировать промежуточные результаты, если пушить нельзя, а тесты запускаются на сервере?

        Мы тесты локально запускали перед коммитом.

        Вашу мысль я понял, просто у нас разной величины проекты, команды и раздача полномочий к репозиторию. Мы работали без rebase, мержили локально.


        1. Dolios
          15.08.2023 13:50

          Да, одна ветка - одна фича.

          Фича одна, а разработчиков несколько. Например, фронт и бэк пилят разные люди и одно без другого вливать нельзя.

          Мы работали без rebase

          Ну, вы просто по другой схеме работали. У нее есть свои преимущества и недостатки, как и у рибейза.

          Мы тесты локально запускали перед коммитом

          Это работает только если тесты идут быстро. Если они идут десятки минут/часы, локально запускать их довольно накладно.


  1. sergiodev
    15.08.2023 13:50

    Я бы ещё упомянул изобилие бесполезных мёрдж-коммитов, когда мёрджат ветку с одним коммитом в master вместо того, чтобы просто черри-пикнуть один коммит. По-моему такое оправдано разве что при наличии какой-то полезной информации в названии ветки или комментарии у самого мёрдж-коммита, иначе просто засоряет историю (хотя можно конечно исключить мёрдж-коммиты при просмотре лога через какой-то флаг).

    Насчёт patch - мне кажется, что проще создать отдельную локальную ветку, чем мучаться с патч-файлами, т. к. их можно случайно закоммитить, если оставить в репозитории.


    1. house2008
      15.08.2023 13:50
      +1

      На мерж реквест в CI настроен прогон тестов (юнит/юай/интеграционные/снапшот). То есть, чтобы в мастер (или develop) закоммитить мелкую праву в стринге (например поменяли "hello world" на "Hello world") то должен пройти каскад всех тестов на созданном МР, в данном случае если поменялась просто строка, то скорее всего упадут снапшот тесты если строка отображалась юзеру и тесты тоже нужно будет поправить))


      1. domix32
        15.08.2023 13:50

        То есть оно делает мерж с мастером, гоняет тесты, и если что-то пошло не так ревертит этот коммит и пушит эти два коммита в мастер или как? Звучит как минимум не очень адекватно.


        1. house2008
          15.08.2023 13:50
          +1

          Там, как я понимаю, проще (конкретно как это сделано я не знаю). Локально на CI мержится в мастер и запускаются задачи, в моем случае несколько наборов различных тестов. Если все (обязательно) прошли успешно, то на CI на мерж реквесте загорается кнопка "замерить в мастер" и только после нажатия на нее произойдет реальный мерж. Но у нас на кнопку "Замержить в мастер" еще висит условие - чтобы она стала активной определенные ревьюверы должны это подтвердить, иначе кнопка будет выключена. Итого, чтобы мерж реквест прошел в мастер нужно чтобы обязательно прошли все тесты и это было подтверждено как минимум 1-2 ревьюверами.


          1. domix32
            15.08.2023 13:50
            +1

            Ну то есть от rebase версии буквально ничем не отличается кроме наличия мерж коммита в мастере по итогу.


            1. house2008
              15.08.2023 13:50
              +1

              Да, всё верно)
              Но я больше отвечал на

              изобилие бесполезных мёрдж-коммитов, когда мёрджат ветку с одним коммитом в master вместо того, чтобы просто черри-пикнуть один коммит.

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


    1. ritorichesky_echpochmak
      15.08.2023 13:50

      Вы, наверное, ищете как мержить с fast-forward, чтобы не плодить чери-пики. Но тогда придётся регулярно перед пушем делать git fetch и git rebase origin/master


      1. sergiodev
        15.08.2023 13:50
        +1

        Не, я скорее сетую на тех разработчиков, которые не могут осилить такую сложную комбинацию команд ))


        1. Andrey_Solomatin
          15.08.2023 13:50

          Жизнь бывает чуть сложнее.

          Нужно пушнуть код. Получить аппрув. Должны пройти тесты и прочие проверки на CI.

          Если в процессе этого, кто-то пушнул свои изменения, всё по новой. Не для всех проектов такое подойдёт.

          А для проектов которым норм на GitHub есть запрет для мерджей если выша ветка на обновлена до мастера.


      1. domix32
        15.08.2023 13:50

        git pull --rebase?


        1. house2008
          15.08.2023 13:50

          есть еще проще, в Idea сделать pull и она сама предложить ребэйзнуть ))


          1. domix32
            15.08.2023 13:50

            Не все ж с Idea. Да и не все в gui обитают.


  1. nickolaym
    15.08.2023 13:50

    Патчи хороши, если какой-то файл надо закоммитить по кусочкам.
    Например, внёс в файл пять правок, но логично было бы сделать их тремя коммитами (сгруппировать с правками других файлов, например).

    Вот только руками редактировать патчфайл - это мрачный труд.
    Но уважающие себя графические клиенты (и/или компоненты IDE) умеют формировать патч построчно.
    Конечно, если правки нахлестнулись, то придётся страдать и как-то выкручиваться из ситуации.
    (Либо плюнуть на красивую историю и жахнуть всё одним коммитом).


    1. storoj
      15.08.2023 13:50
      +2

      git add -p тоже может стейджить построчно


    1. ritorichesky_echpochmak
      15.08.2023 13:50

      Как минимум GitExtensions позволяет ложить в stash изменения по частям без боли и путаницы


    1. domix32
      15.08.2023 13:50

      gitui или lazygit если вы из терминала выходить не хотите. Ну или например git-cola, если вам гуи нравятся больше. Выбираем файл с изменениями и жмакаем s на интересующих вас строчках - вуаля, она ушла на этап стеджинга.


  1. storoj
    15.08.2023 13:50
    +1

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


    1. Andrey_Solomatin
      15.08.2023 13:50

      Я вижу только один коммент с минусом, там сразу много пунктов и не со всеми стоит соглащаться. Кажется ваш комментарий потерял актуальность.


  1. domix32
    15.08.2023 13:50
    +1

    Ещё надо было про --fixup коммиты и --autosquash ребейз рассказать.

    $ git commit --fixup e1c231c568
    $ git log --oneline 4
    276371ccba fixup! Add blabla to foobar
    aa7371c3b1 Do some bar staff
    da73c1b8b8 Do some qux staff
    e1c231c568 Add blabla to foobar
    $ git rebase --interactive --autosquash HEAD~4
    
    pick e1c231c568 Add blabla to foobar
    fixup 276371ccba fixup! Add blabla to foobar
    pick da73c1b8b8 Do some qux staff
    pick aa7371c3b1 Do some bar staff


    1. Andrey_Solomatin
      15.08.2023 13:50

      Спасибо, что напомнили, я про это и забыл.

      Сейчас работаю с Герритом, а там приходится менять старые коммиты.


  1. Andrey_Solomatin
    15.08.2023 13:50

    При работе в GitHub можно запретить пушить в мастер, а мердже разрешить через Pull Request только чере Squash & Merge. То есть не важно, что там у разработчика, в мастере будет красивенько.

    Там же сможно сдалать обязательную проверку на наличие тикета в сообщенит. Я обычно добавляю проверку сообщений (https://github.com/jorisroovers/gitlint
    ) в хуки от https://pre-commit.com/.

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

    Например делать `git push --force` в ветку, где работает вся команда плохо, но остановило ли меня это? Нет, я это сделал не умышленно.
    Повторил ли кто-то мой подвиг? Нет, включили автоматическую защиту.