Skip to content

Latest commit

 

History

History
1002 lines (782 loc) · 65.4 KB

Git.md

File metadata and controls

1002 lines (782 loc) · 65.4 KB

Основные понятия Git

Как работает Git

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

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

Файл Версия I Версия II Версия III
A.txt delta I delta II
B.txt delta I delta II

В таблице выше delta — список изменений файла, прочерк — отсутствие данных об изменениях.

Git рассматривает данные не как таблицы изменений конкретных файлов, а как поток снимков (stream of snapshots).

Каждый снимок (shapshot) означает сохранение определённого состояния проекта. Система запоминает, как выглядит каждый файл проекта на момент сохранения (делает снимок) и сохраняет ссылку на этот снимок. Если файл не изменился, то Git не запоминает его вновь, а создаёт ссылку на идентичную версию файла из предшествующего снимка.

Файл Версия I Версия II Версия III
A.txt A1 A2 A2
B.txt B1 B1 B2

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

Благодаря такому подходу Git чем-то похож на файловую систему.

Состояния файлов

Git делит все файлы на отслеживаемые и неотслеживаемые.

Отслеживаемые (tracked) файлы — файлы, которые были в последнем снимке состояния проекта, неотслеживаемые (untracked) файлы — все остальные.

Также имеются категория игнорируемых (ignoring) файлов. Изменения этих файлов Git игнорирует.

Чтобы сделать файл (папку) игнорируемым, необходимо его добавить в файл .gitignore, который должен лежать в проекте (обычно в корневой папке).

Пример содержимого .gitignore.

node_modules/
logs/
dist/
.env

Состояния отслеживаемых файлов

  • Неизменённый (Unmodified). Файл не изменён.
  • Изменённый (Modified). Файл изменён локально.
  • Подготовленный (Staged). Файл изменён локально и помечен для включения в следующий коммит.
  • Зафиксированный (Committed). Версия файла сохранена в истории Git.

Три области Git

  • Рабочая директория (Working Directory). Файлы распаковываются из сжатой базы данных репозитория и располагаются на локальном диске для чтения и записи. Здесь происходит работа с ними до тех пор, а затем они переходят
  • Область подготовленных файлов (Staging Area). Здесь содержатся сведения о подготовленных файлах и их изменениях, которые должны попасть в следующий коммит. Эта область является частью папки .git.
  • Папка .git (.git directory, Repository). Здесь хранятся метаданные (данные о данных) и Git-объекты текущего проекта, в том числе история коммитов.

Коммит и индекс

Коммит (Commit) — отметка (точка, этап) в истории Git. К этой точке можно вернуться. Вся история проекта состоит из цепочки связанных друг с другом коммитов. Каждый коммит имеет родительский коммит и зависит от него.

Близкими по смыслу коммиту понятиями являются версия (version) и снимок (snapshot).

Уникальным идентификатором коммита является его хеш.

commit 54e2f882f077cb0f4a1ca0600eada25cc96c7e5a
commit ec1b065a072c545ca2849e0c05b60c520bdc39e4

Индекс (Index) — снимок следующего намеченного коммита.

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

/* добавить изменённый файл */
git add <filename>

/* добавить папку с изменёнными файлами */
git add <directory>

/* добавить все изменённые файлы */
git add .

Чтобы добавить файлы из области подготовленных файлов в коммит, используется команда commit.

git commit

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

git commit -m "Commit message here"

Ветвление

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

Ветка (Branch) — независимая линия разработки проекта.

Ветка состоит из последовательности коммитов.

Базовый коммит ветки (Base commit) — коммит, с которого началась (была создана) ветка.

Будем обозначать латинискими буквами хеши коммитов: A, B, C и т.д.

/* Ветвление */
feature              |                  F —— G
                     |                /
develop              |          C —— D —— E
	             |         /
master (основная)    |   A —— B

На изображении выше базовым коммитом для ветки master является коммит A, для ветки BC, для featureD.

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

От основной ветки создаются новые ветки, от них тоже при необходимости создаются ветки.

Базовым коммитом новой ветки становится последний на момент создания коммит текущей ветки.

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

Для управления ветками используется команда branch.

/* создание новой ветки */
git branch <branch_name>

/* список всех веток */
git branch -a

/* переименование ветки */
git branch -m <old_name> <new_name>

/* удаление ветки */
git branch -D <branch_name>

Для переключения между ветками используется команда checkout.

/* переключение на ветку branch_name */
git checkout <branch_name>

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

HEAD и верхушка ветки

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

Это позволяет Git хранить ветку не как последовательность коммитов, а просто как ссылку на последний её коммит.

Верхушка ветки (Branch tip, Branch head) — последний коммит ветки. У каждой ветки есть одна вершутка.

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

Когда мы создаём новую ветку, её базовым коммитом становится тот, на который ссылается HEAD.

Если открыть файл HEAD в папке .git, то можно увидеть, что он содержит ссылку на текущую ветку.

cat .git/HEAD
/* ref: refs/heads/master */

checkout develop
cat .git/HEAD
/* ref: refs/heads/develop */

Если открыть папку refs/heads, то там можно видеть, что Git хранит для каждой ветки отдельный файл, в котором хранится хеш последнего коммита ветки.

cat ./git/refs/heads/master
/* 1ed2bf4eebbcd0515a638b48550a7eb81c7c01e5 */

Основные команды Git

merge

Команда merge позволяет слить (смержить) истории двух веток в одну, то есть из двух последовательностей коммитов создать одну.

Целевой ветка (target branch) сливается в текущую ветку (current branch), которую также называют рабочей.

git merge <target_branch>

При слиянии изменения затрагивают только текущую ветку: целевая ветка остаётся без изменения.

Как работает merge

Git просматривает цепочку коммитов обеих веток, пытаясь найти их общий коммит. Обычно такой коммит имеется (как минимум, начальный коммит).

Довольно редко бывают случаи, когда между двумя ветками нет ни одного общего коммита. По умолчанию Git отказывается сливать такие ветки в одну, но можно использовать флаг --allow-unrelated-histories, который позволит это сделать.

Если общий коммит между двумя ветками найден, то возможны два случая

  • Только в одной ветке были новые коммиты с момента общего коммита. В таком случае одна ветка является продолжением другой. Используется fast-forward merge.
  • Обе ветки имеют новые коммиты с момента общего коммита. Это означает, что они развивались параллельно, независимо от друга. Используется true merge.

Fast-forward merge

Fast-forward merge (перемотка вперёд) используется, если одна ветка является продолжением другой.

Возможны два случая: целевая ветка длиннее или короче текущей.

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

/* до слияния веток develop и master целевая ветка develop была
впереди на 2 коммита C и D */
develop          |          C —— D
	         |         /
master (current) |   A —— B
git checkout master
git merge develop
/* после слияния верхушки веток указывают на один и тот же коммит D */
develop          |          C —— D
	         |         /
master (current) |   A —— B —— C —— D

Если отменить последний коммит в ветке master, то в ней отменится только коммит D.

/* отмена последнего коммита при помощи git reset HEAD~ */
develop          |          C —— D
	         |         /
master (current) |   A —— B —— C

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

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

git checkout develop
git merge master

Fast-forward merge не будет применён при использовании флага --no-ff, применится true merge.

git merge --no-ff branch_name

True merge

Если текущая и целевая ветки развивались независимо друг от друга в течение какого-то времени, тогда каждая из них имеет новые коммиты с момента общего коммита веток и использовать fast-merge не получится. В этом случае применяется true merge (3-way merge), которые использует 3-х сторонний алгоритм (3-way algorithm). Алгоритм так называется, поскольку учитываются 3 стороны (состояние до изменений, изменения целевой ветки, изменения текущей ветки).

При применении true merge создаётся особый тип коммита, имеющий сразу два родительских коммита, — merge commit. В нём содержатся новые изменения, которые были в целевой ветки, но не были в текущей.

/* до слияния веток с момента общего коммита B целевая ветка develop 
имеет 2 коммита C и D, текущая ветка master имеет один коммит E */
develop          |          C —— D
	         |         /
master (current) |   A —— B —— E
git checkout master
git merge develop
/* после слияния в текущей ветке master появился merge commit H,
содержащий в себе все изменения коммитов C, D, E */
develop          |           C —— D
	         |         /       \
master (current) |   A —— B —— E —— H

Как и в случае fast-forward merge, изменения true merge затрагивают только текущую ветку, поэтому merge commit создаётся только в текущей ветке (коммит H появился только в master).

Аналогичный результат бы получился, если бы ветка develop была текущей, а master — целевой. Разница лишь в том, что в этом случае коммит H хранился бы только в develop.

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

/* отмена последнего коммита при помощи git reset HEAD~ */
develop          |          C —— D
	         |         /
master (current) |   A —— B —— E

Конфликты при true merge и их разрешение

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

Для отображения конфликтов по умолчанию использует 2 версии файла и следующий синтаксис.

  • <<<<<<< — изменения коммита текущей ветки.
  • >>>>>>> — изменения коммита целевой ветки.
  • ======= — разделяющая полоса.
<<<<<<<
/* изменения в текущей ветке */
=======
/* изменения в целевой ветке */
>>>>>>>

Можно настроить команду merge таким образом, чтобы также показывалось и состояние до изменений. За это отвечает свойство merge.conflictStyle со значением diff3. Синтаксис: |||||||.

<<<<<<<
/* изменения в текущей ветке */
|||||||
/* до изменений */
=======
/* изменения в целевой ветке */
>>>>>>>

Чтобы разрешить конфликт, следует выбрать необходимые изменения и убрать всё лишнее, затем эти изменения добавляются в merge commit.

Пример конфликта в одном из файлов (в коммите текущей ветки используется const, в коммите целевой ветки — let).

<<<<<<< HEAD (Current Change)
let name = 'Notes';
=======
const name = 'Notes';
>>>>>>> develop (Incoming Change)

В этом примере можно оставить следующую строку и добавить её в merge commit.

let name = 'Notes';

rebase

Как работает rebase

Базовый коммит ветки (Base commit) — коммит, с которого начинается (создана) ветка.

Перебазирование (Rebasing) — перемещение последовательности коммитов к новому базовому коммиту.

git rebase <base>

Перебазирование переписывает историю текущей ветки.

Когда мы указываем ветку в качестве <base> для команды rebase, берётся последний коммит этой ветки.

/* до перебазирования целевая ветка develop по сравнению с master
имеет новый коммиты C и D, базовым для неё является коммит B */
develop (current) |          C —— D
	          |         /
master            |   A —— B —— E
git checkout develop
git rebase master
/* после перебазирования базовым коммитом для develop стал коммит E
(последний в master), C* и D* — копии коммитов C и D с новыми хешами */
develop (current) |                C* —— D*
	          |               /
master            |   A —— B —— E

Если бы перебазировался не develop, а master, то изменились бы все коммиты ветки master с момента их общего коммита с develop (в данном случае это только коммит E).

git checkout master
git rebase develop
/* после перебазирования базовым коммитом для master остался коммит A
(поскольку начало историй обеих веток совпадают), E* — копия
коммита E с новым хешем */
develop          |          C —— D
	         |         /
master (current) |   A —— B —— C —— D —— E*

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

При перебазировании возможны такие же конфликты, как и при true merge. Решаются они аналогично.

rebase vs merge

Самое основное отличие заключается в том, что rebase перезаписывает историю, а merge только дополняет её.

Fast-forward просто копирует коммиты из одной ветки в конец ветки.

True merge создаёт новый merge commit, содержащий все необходимые текущей ветке изменения целевой ветки (с необходимыми правками, если были конфликты).

Недостаток fast-forward merge: не может работать с ветками, которые развивались параллельно.

Недостаток true merge: merge commits загрязняют историю приложения.

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

Если rebase затронет коммиты, которые не были созданы в текущей ветке, то при слиянии с другой веткой Git увидит две разные истории и создаст большой merge commit, который использует обе версии. Появится дублирование одних и тех же изменений.

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

Другие возможности rebase и интерактивный режим

Помимо перебазирования команда rebase имеет интерактивный режим, который позволяет полностью переписать историю определённого числа коммитов. Для перехода в интерактивный режим используется флаг -i. Для выбора N последнимх коммитов используется указатель HEAD~N.

git checkout develop
git rebase -i HEAD~2 /* изменение истории двух последних коммитов текущей ветки */

В этом случае будет открыт текстовый редактор со следующим содержимым.

pick C Message for the commit C
pick D Message fof the commit D

# Rebase B..D onto B
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  d, drop <commit> = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

До символов # показывается список всех выбранных для редактирования коммитов. По умолчанию к каждому из них принимается команда pick (оставить как есть), но её можно заменить текстом на одну из следующих:

  • reword - изменение сообщения коммита (окно для изменения сообщения появится после).
  • squash - совместить коммит с предыдущим (далее надо будет набрать общий текст для нового скомбинированного коммита).
  • fixup - совместить коммит с предыдущим (с отменой сообщения коммита).
  • drop - удалить коммит и его изменения из истории (это можно также сделать, удалив строку с коммитом из списка).
  • Также можно менять строки в списке местами, тогда соответствующие им коммиты также поменяются местами в истории.

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

Если нужно перезаписать сообщение последнего коммита, то можно вместо rebase использовать команду commit с флагом --ammend:

git commit --amend -m "Updated last commit message"

pull и fetch

Команда git fetch используется для того, чтобы получить информацию о последних изменениях на удалённой ветке (origin/). Таким образом можно узнать, были ли изменения вообще.

git fetch
/*
From github.com:YourName/RepositoryName
   2eefe71..ac391ab  develop   -> origin/develop
   fca7c62..c31477c  feature/1 -> origin/feature/1
 * [new branch]      feature/2 -> origin/feature/2
 * [new branch]      feature/3 -> origin/feature/3
*/

Выше можно видеть, что ветки develop и feature/1 отличаются от своих одноимённых удалённых веток хешем последних коммитов, а ветки feature/2 и feature/3 новые и их нет локально.

Команда git fetch помимо информации об изменениях скачивает и сами изменения, но их не слияние с локальной веткой не происходит. Это можно проверить командой git diff.

git diff origin/develop
/*
diff --git a/Git.md b/Git.md
index abe21d5..e207862 100644
+++ b/client/Dockerfile
- Hello
+ Hello, Notes!
*/

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

Команда git pull также, как и git fetch получает изменения и информацию о них, но дополнительно сливает изменения в локальную ветку.

По сути, git pull объединяет в себе две команды: git fetch и git merge.

Откат изменений с reset, checkout, revert, restore

Обычно HEAD указывает на верхушку ветки (англ. branch tip).

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

Каждая из команд reset, checkout, revert, restore позволяют откатывать (англ. undo) изменения, но делают это по-своему.

checkout

Базовое использование команда checkout просто перемещает указатель HEAD, что позволяет переключиться в любую точку истории проекта и начать работать оттуда.

/* до checkout HEAD ветки совпадал с верхушкой (tip) 
ветки master и указывал на коммит E */
                     (HEAD,
	             master)        
A ——— B ——— С ——— D ——— E
git checkout B
/* после checkout */
  (detached
     HEAD)           (master)
A ——— B ——— С ——— D ——— E

Если ввести git branch, то он выдаст ветку (HEAD detached at B).

Если ввести git log, то коммит B в текущей ветке будет последним.

/* git log */
    (HEAD)
A ——— B

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

Откат определённых файлов при помощи checkout

Git позволяет откатывать отдельные файлы до определённого коммита при помощи продвинутого использования команды checkout.

Откат конкретного файла до определённого коммита по хешу коммита <commit_hash>.

git checkout <commit_hash> -- <path_to_file>
git checkout 1e4d903 -- client/package-lock.json
git checkout 37fc11b -- server/package-lock.json

Откат файла до предыдущего коммита (хеш опускается).

git checkout -- <path_to_file>
git checkout -- client/package-lock.json

Откат файла к родительскому коммиту конкретного коммита <commit_hash> при помощи ~1:

git checkout <commit_hash>~1 -- <path_to_file>

revert

Команда revert выбирает коммит и создаёт новый коммит, который откатывате изменения выбранного коммита.

/* до revert */
	        (HEAD,
	        master)        
A ——— B ——— С ——— D
git revert C
/* после revert создаётся коммит E, отменяющий все изменения коммита D,
и таким образом возвращающий состояние проекта на момент коммита B */
	              (HEAD,
	              master)        
A ——— B ——— С ——— D ——— E

Если при помощи revert откатывается не последний коммит (например, C), то могут появиться конфликты (поскольку коммит D зависеть от некоторых изменений коммита C).

Команда revert является безопасным вариантом для отката изменений в публичном репозитории, поскольку коммиты не удаляются, а создаются — история не перезаписывается.

reset

Команда git reset сбрасывает все изменения и историю до определённого коммита.

/* до reset */
	        (HEAD,
	        master)        
A ——— B ——— С ——— D
git reset B
/* после reset */
   (HEAD,
   master)        
A ——— B

Примеры

git reset HEAD~1 /* переставляет указатель HEAD на один коммит */
git reset HEAD~2 /* переставляет указатель HEAD на два коммита */

Команда git reset имеет несколько режимов:

  • git reset --soft - перемещает HEAD к определённому коммиту, сохраняет все локальные staged изменения с последних коммитов
  • git reset --mixed (используется по умолчанию) - перемещает HEAD к определённому коммиту, сохраняет локальные staged изменения и переводит их в unstaged
  • git reset --hard - перемещает HEAD к определённому коммиту и стирает все изменения локальные staged и unstaged изменения

Таким образом команда git reset --hard полностью, безвозвратно удаляет как все закоммиченные, так и незакоммиченные изменения (получается чистое удаление). В то же время команды git reset --soft и git reset --mixed сохраняют локальные (незакоммиченные) изменения, но --soft оставляет их в staging, а --mixed - не оставляет.

Например,

		(HEAD,
	        master)        
A ——— B ——— С ——— D

Все три команды git reset --soft B, git reset --mixed B (git reset B), git reset --hard B локально изменят историю ветки master на

   (HEAD,
   master)        
A ——— B

Но при этом:

  • После git reset --soft B изменения коммитов C и D появятся во вкладке staged changes (staging area).
  • После git reset --mixed B изменения коммитов C и D появятся во вкладке unstaged changes (working area).
  • После git reset --mixed B изменения коммитов C и D исчезнут, working tree is clean.

Отмена команды git reset --<mode>:

  • Для --soft: git commit.
  • Для --mixed: git add, затем git commit.
  • Для --hard: изменения не хранятся ни в одной ветке, так что вернуть их не так просто. Закоммиченные ранее и откаченные теперь изменения можно найти при помощи команды git reflog, незакоммиченные изменения пропали навсегда и их уже не вернуть.

restore

**Команда git restore восстановит (вернёт) состояние файла <file_name> к предыдущему (последнему) коммиту, эффективно откатывая любые изменения с момента последнего комита.

git restore <file_name>

Откатить все файлы в проекте:

git restore .

reflog

Версионирование и тэги

О версионировании и тэгах

Версионирование (англ. version control, source control) - это процесс отслежвания изменений в коде программного обеспечения (англ. software code) и управления ими.

Когда продукт готов к использованию, происходит его выпуск - релиз (англ. release), то есть продукт становится доступен пользователям (например, в виде приложения в App Store или вебсайта). Со временем продукт улушается, а значит происходят новые релизы - и пользователи получают доступ к новым функциям приложения. Таким образом, при каждом релизе появляется новая версия приложения (англ. version).

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

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

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

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

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

Чаще всего название тэга имеет формат vX.X.X.

Семантическое версионирование, формат версий

Семантический формат номерации версии: <X>.<Y>.<Z>, где <X>, <Y>, <Z> - целые неотрицательные числа.

При переходе на новую версию в приложении увеличивается одно из чисел <X>, <Y>, <Z> (один из счётчиков).

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

Мажорная версия

Счётчик <X> называют мажорной версией (англ. major version, MAJOR) приложения. Приложение переходит на новую мажорную версию (то есть счётчик <X> увеличивается на 1, Y и Z сбрасываются до 0), если его изменения обратно не совместимы с предыдущиями версиями.

Например, вы используете в своём приложении функцию f из пакета help версии 1.2.0. Обновляя пакет help до версии 2.0.0 вы обнаруживаете, что функция f ломает приложение, поскольку она была удалена из пакета help, была переименована в функцию g или просто поменяла тип возвращаемого значения. Такие изменения называют ломающими изменениями (англ. breaking changes). Таким образом, обновляя пакет до новой мажорной версии, вы ставите под угрозу работоспособность своего приложения. Код вашего приложения придётся изменять, если в мажорной версии были затронуты использующиеся в вашем приложении фукнции.

Минорная версия

Счётчик <Y> называют минорной версией (англ. minor version, MINOR). Приложение переходит на новую минорную версию (то есть счётчик <Y> увеличивается на 1, X остаётся прежним, Z сбрасывается), если была добавлена новая функциональность в приложение, а старая функциональность не была затронута. В этом случае обратная совместимость сохраняется.

Например, вы используете в своём приложении функцию f из пакета help версии 1.2.1. Обновляя пакет help до версии 1.3.0 вы обнаруживаете, что функция f не ломает приложение, поскольку единственным изменением в пакете было добавление новой функции g. Таким образом, обновляя пакет до новой минорной версии, вы не должны менять код своего приложения.

Патч-версия

Счётчик <Z> называют патч-версией (англ. patch version, PATCH). Приложение переходит на новую патч-версию (то есть только счётчик <Z> увеличивается на 1, X и Y остаются прежними), если были в прилажение были внесены сделаны обратно совместимые исправления, то есть новых функций не было добавлено, а старые функции хоть и были затронуты в коде, но возвращают тот же результат. Таким образом, обратная совместимость сохраняется.

Например, вы используете в своём приложении функцию f из пакета help версии 1.2.0. Обновляя пакет help до версии 1.2.1 вы обнаруживаете, что всё работает как прежде. Какие исправление могут войти в патч? Оптимизация скорости работы функции, удаление console-логов, исправление типографических ошибок, ошибок линтера, переименование внутренней переменной, написание тестов, документации и так далее - всё то, что не влияет на параметры экспортируемых из пакета функций, их названия и их возвращаемые значения.

Создание тэга

Обязательным при создании тэга является лишь указание названия для тэга.

git tag -a <version_name>

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

Для того, чтобы указать коммит при создании тэга, необходимо передать хэш коммита:

git tag -a <version_name> <commit_hash>

Например,

git tag -a v1.1.0 d51bbf173a527bef67a61fa57824763ffa22e302

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

Для создания тэга с описаниеем используется команда:

$ git tag -a <version_name> -m <version_description>
$ git tag -a v1.1.0 -m "My version 1.1.0 (Feb, 11 2022)"

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

$ git push origin v1.1.0

Отображение информации о версии

Для просмотра списка существующих тэгов (версий) используется команда:

git tag
/*
v1.0.0
v1.0.1
v1.1.0
*/

Для отображения информации об определённой версии используется команда git show:

$ git show <tagname>
$ git show v1.1.0
/* 
tag v1.1.0
Tagger: Max Starling <17.max.starling@gmail.com>
Date:   Fri Feb 22 17:33:49 2022 +0300

My version 1.1.0 (Feb, 11 2022)

commit d51bbf173a527bef67a61fa57824763ffa22e302
Author: Max Starling <17.max.starling@gmail.com>
Date:   Fri Feb 22 17:28:32 2022 +0300

    Merge branch 'development'
*/

Переключение между версиями

Git позволяет переключаться между версиями так же, как и между ветками:

$ git checkout <tagname>
$ git checkout v1.2.0

Удаление тэга

Для удаления тэга используется команда

$ git tag -d <tagname>

Удаляется только тэг, коммит остаётся.

Полный флоу релиза и создания версии

  1. Переключаемся на ветку production
$ git checkout production
  1. Делаем мерж из development ветки в production. Возможно, будет создан merge commit в ветке production:
$ git merge development
  1. Смотрим список существующих версий
$ git tag
v1.0.0
v1.0.1
  1. Создаём новый тэг соответствующий номеру версии в ветке production.
$ git tag -a v1.1.0 -m "My version 1.1.0 (Feb, 11 2022)"
  1. Проверяем, что тэг повесился на самый последний коммит в ветке production
$ git log --oneline
/*
d51bbf17 (HEAD -> master, tag: v1.1.0, origin/master) Merge branch 'development'
ccd4b31f Add dark mode
...
*/
  1. Пушим тэг в удалённый репозиторий (remote origin)
$ git push origin v1.1.0
  1. Заходим на GitHub, находим тэг во вкладке Releases/Tags.

Дополнительно можно сделать Release Notes в Github:

  1. Заходим в GitHub во вкладку Releases.
  2. Нажимаем Draft a new release.
  3. Выбираем тэг из списка, пишем название релиза и описание (а лучше нажать на Auto-generate release notes) image
  4. Нажимаем Publish release
  5. Смотрим результат image

Полезные возможности Git

Отмена последнего коммита

git reset HEAD~

Перенос коммитов из одной локальной ветки в другую

Например, перенос N коммитов из локальной ветки A и локальную ветку B.

git checkout B
git merge A
git checkout A
git reset --hard HEAD~N # удаление N коммитов из ветки A

Смена CRLF на LF одновременно для всех файлов в проекте

Если вы работаете на Windows, то скорее всего после скачивания Git-репозитория у вас будет проблема с тем, что при открытии каждого файла в проекте стоит CRLF. При этом чаще всего удалённый сервер использует LF (характерный для Linux и Mac систем). Это может приводить к ошибкам линтера, а значит и к ошибкам в CI & CD пайплайнах. Более того, если линтер стоит локально, то и у вас проект, вероятно, проект не будет запускаться, пока вы не измените каждый открытый файл на LF.

Следующий набор команд позволит решить проблему. Но сперва убедитесь, что нет незакоммиченных файлов - проще говоря, убедитель, что дерево изменений на данный момент пусто, иначе все изменения пропадут после команды git reset --hard.

git config core.autocrlf false 
git rm --cached -r . 
git reset --hard

Git flow

Фича (Feature) — новая функциональность.

Разработка новой фичи

  • Разработка новых фич начинается с ветки develop, от которой создаётся новая ветка feature/name, где name - название фичи.
  • Разработчик переключается на новую ветку и начинает работать с ней.
  • После завершения фичи создаётся Pull Request.
  • Ветка feature/name сливается с (merge into) веткой develop.
  • Ветка feature/name удаляется.
  • Разработчик переключается обратно на ветку develop.

Релиз в production

  • От ветки develop создаётся ветка release/vX.X.X.
  • Ветка release/vX.X.X при необходимости помечается тэгом vX.X.X.
  • Разрешены мелкие исправления (minor bug fixes) в ветке release/vX.X.X.
  • Ветка release/vX.X.X сливается с веткой master.
  • Ветка release/vX.X.X сливается обратно (back-merge) с веткой develop.
  • Ветка release/vX.X.X удаляется.
  • Разработчик переключается обратно на ветку develop.

Hotfix в production

  • Если в production найден серьёзный баг, который нужно быстро исправить, от ветки master создаётся ветка hotfix/name, в которой делаются необходимые исправления.
  • Ветка hotfix/name сливается с веткой master.
  • Ветка hotfix/name сливается с веткой develop.
  • Ветка hotfix/name удаляется.

SSH Github & Gitlab

  • Открыть Bash.

Генерация ключей

  • Сгенерировать ключ для Github при помощи ssh-keygen -t rsa -C "email" -f ~/.ssh/id_rsa_github, где нужно заменить email на свой. id_rsa_github - название файла, где будет лежать приватный ключ. Команда также автоматически генерирует публичный ключ id_rsa_github.pub в той же папке.

  • Ввести ключевую фразу и повторить её.

  • Сгенерировать ключ для Gitlab при помощи ssh-keygen -t rsa -C "email" -f ~/.ssh/id_rsa_gitlab, где нужно заменить email на свой.

  • Ввести ключевую фразу и повторить её.

Cоздание ключа на Github

  • Скопировать публичный ключ ~/.ssh/id_rsa_github.pub для Github. Например, вывести его на экран при помощи cat ~/.ssh/id_rsa_github.pub и скопировать через Ctrl + C.
  • Открыть на Github Settings > SSH keys и нажать на добавление нового ключа.
  • Вставить скопированный ключ, придумать название для него и сохранить.

Cоздание ключа на Gitlab

  • Скопировать публичный ключ ~/.ssh/id_rsa_gitlab для Gitlab.
  • Открыть на Gitlab Settings > SSH keys.
  • Вставить скопированный ключ, придумать название для него и сохранить.

Добавление SSH-ключа в SSH-agent

  • Добавить SSH-ключ для GitHub в SSH-agent ssh-add ~/.ssh/id_rsa_github. Если агент не запущен, то нужно сперва его запустить eval $(ssh-agent -s) или ssh-agent bash.
  • Добавить SSH-ключ для GitLab в SSH-agent ssh-add ~/.ssh/id_rsa_gitlab.
  • Проверить, что ключи добавлены через ssh-add -L.

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

Псевдоним (Alias) — аббревиатура, позволяющая избежать написания длинной последовательности команд.

  • Создадим файл .bashrc в корневой папке (в Windows: `C:/Users//) и поместим там следующее.
alias sa="eval `ssh-agent -s` ssh-add ~/.ssh/id_rsa_gitlab"

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

Конфигурация SSH

  • Создать файл touch ~/.ssh/config.
  • Вставить туда следующее
# config for github
Host github.com
   HostName github.com
   User git
   IdentityFile ~/.ssh/id_rsa_github
# config for gitlab
Host gitlab.com
   HostName gitlab.com
   User git
   IdentityFile ~/.ssh/id_rsa_gitlab
  • Сохранить изменения.

Проверка работоспособности SSH

  • Попробовать сделать git pull через SSH.
  • Дополнительная проверка для Github: ssh -T git@github.com (режим отладки: ssh -T git@github.com).
  • Дополнительная проверка для Gitlab: ssh -T git@gitlab.com (режим отладки: ssh -T git@gitlab.com).

Ошибка "Host key verification failed"

Если при выполнении команды ssh -T git@github.com возникает ошибка "Host key verification failed" как на скриншоте ниже image Тогда следует добавить хост github.com в список известных хостов known_hosts следующей командой:

ssh-keyscan -t rsa github.com >> ~/.ssh/known_hosts

И проблема должна решиться.
image

Git Config

Вывод конфига

git config -l

Имя пользователя и почта

/* имя пользователя */
git config --global user.name "Your Username"
/* электронная почта */
git config --global user.email "your@email"

Git Bash

Командная оболочка (Shell) — терминальное приложение (terminal app), используемое для взаимодействия с ОС посредством письменных команд.

Bash (Bourne again shell, "Born again shell", "возрождённый" shell) — усовершенствованная версия ранней командной оболочки Bourne shell, исполняющей файлы формата .sh в UNIX. Bash является командной оболочкой по умолчанию для Linux и macOS.

Git Bash — пакет, устанавливающий Bash, некоторые базовые утилиты и Git на Windows.

Убить запущенный процесс

Чтобы на определённом порте убить запущенный процесс, нужно узнать его PID (Process Identifier).

netstat -ano | findstr :PORT /* например, :3000 */
/* Скопировать PID из последнего стобца результата и вставить в команду ниже */
tskill PID