Кратко про системы контроля версий (2024)

Навроцкий Артем

Кратко про системы контроля версий (2024)

Навроцкий Артем

Общая информация

Определение из Wikipedia

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

Какие проблемы решаются?

Фиксация промежуточного результата

Как съесть слона? По кусочку!

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

Совместная работа и источник правды

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

Воспроизводимость сборки

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

Разделение прав доступа

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

Системы контроля версий

Snapshot-based

Patch-based

Базовый сценарий

Файл правит только один

sequenceDiagram
            actor A as Клиент
            participant B as Сервер
            A ->>+ B: Дай последнюю версию проекта!
            B -->>- A: Перекидывает архив с проектом
            A ->>+ B: Я начинаю править файл foo.py!
            B -->> A: Помечает файл заблокированным и отдаёт последнюю версию
            A ->> B: Я заливаю изменения!
            B -->>- A: Сохраняет у себя файл и снимает блокировку
        

Файл правит только один

Системы контроля версий

Perforce, Visual SourceSafe

Плюсы

Минусы

Конфликты решаются потом

sequenceDiagram
            actor A as Клиент
            participant B as Сервер
            A ->>+ B: Дай последнюю версию проекта!
            B -->>- A: Перекидывает архив с проектом
            A ->> A: Правит файл foo.py!
            A ->>+ B: Я заливаю изменения!
            B -->>- A: Принимает изменения
            A ->>+ B: Дай изменения проекта!
            B -->>- A: Отправляет изменения от коллег
        

Конфликты решаются потом

Системы контроля версий

CVS, Subversion

Плюсы

Минусы

Распределённая система

Общий алгоритм

  1. Забираем локально копию репозитория
  2. Начинаем ветку от любой версии
  3. Правим локально файлы и фиксируем результат
  4. Забираем свежие изменения
  5. Сливаем изменения с эталонной веткой
  6. Отправляем ветку на удалённую копию

Распределённая система

Системы контроля версий

Git, Mercurial

Плюсы

Минусы

Subversion vs Git

Subversion

Git

Как устроен Git?

Собираем Git репозиторий на Python

Любая достаточно развитая технология неотличима от магии.

Третий закон Артура Кларка

Git имеет очень простую внутреннюю структуру.

Для иллюстрации соберём небольшой Git-репозиторий используя только стандартную библиотеку Python.

Минимальный репозиторий

#!/usr/bin/env python3
# -*- coding: utf8 -*-
import os.path


def create(dir: str):
    os.makedirs(os.path.join(dir, ".git/refs"), 0o755, True)
    os.makedirs(os.path.join(dir, ".git/objects"), 0o755, True)
    head = os.path.join(dir, ".git/HEAD")
    if not os.path.exists(head):
        with open(head, "wt") as f:
            f.write("ref: refs/heads/master")

Минимальный репозиторий должен содежрать директории refs/, objects/ и файл HEAD.

Объекты репозитория

object.py:
# -*- coding: utf8 -*-
import hashlib
import os.path
import zlib


def write(dir: str, kind: str, data: bytes):
    object = b"%s %d\0%s" % (kind.encode("utf-8"), len(data), data)
    hash = hashlib.sha1(object).hexdigest()
    path = os.path.join(dir, ".git/objects/%s/%s" %
                        (hash[:2], hash[2:]))
    os.makedirs(os.path.dirname(path), 0o755, True)
    with open(path, "wb") as f:
        f.write(zlib.compress(object, zlib.Z_BEST_COMPRESSION))
    print(hash, kind)
    return hash

Объекты можно сложить в директорию objects/. Имя объекта — его SHA-1 хэш (160 бит).

Объекты репозитория

Есть всего 4-ре вида объектов в данных репозитории:

blob
Содержимое файла
tree
Содержимое каталога
commit
Коммит
tag
Метка конкретного коммита

Каждый объект хранится в виде: тип <SP> размер <NUL> данные

Помимо этих объектов есть еще ссылки на коммиты (ветки).

Blob-объекты репозитория

blob.py:
#!/usr/bin/env python3
# -*- coding: utf8 -*-
import object


def create(dir: str, data: bytes):
    return object.write(dir, "blob", data)

Blob-объект просто содержит данные файла.

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

Tree-объекты репозитория

tree.py:
#!/usr/bin/env python3
# -*- coding: utf8 -*-
import object


# create([
#   (0o100755, "bar.sh",  "74afc74214c6dd572ff137f5018a96407f20b92e"), # blob
#   (0o100644, "foo.txt", "964880834baa20ccfd5865121f60c9ea9c62f5ff"), # blob
#   (0o040000, "subdir",  "840613015b2fe6287427884150e5ebd55c1b8574"), # tree
#   (0o160000, "submod",  "aa2d6217394ba004578be6a83fcd33aa8db22b20"), # commit
# ])
def create(dir: str, tree):
    data = b""
    for (attr, name, hash) in tree:
        data += b"%o %s\0%s" % (attr, name.encode("utf-8"), bytes.fromhex(hash))
    return object.write(dir, "tree", data)

Tree-объекты содержат список объектов внутри директории.

Commit-объекты репозитория

commit.py:
#!/usr/bin/env python3
# -*- coding: utf8 -*-
import object


def create(dir: str, tree: str, parents, author: str, message: str):
    data = b""
    data += b"tree %s\n" % (tree.encode("utf-8"),)
    for parent in parents:
        data += b"parent %s\n" % (parent.encode("utf-8"),)
    data += b"author %s\n" % (author.encode("utf-8"))
    data += b"committer %s\n" % (author.encode("utf-8"))
    data += b"\n%s\n" % (message.encode("utf-8"))
    return object.write(dir, "commit", data)

Commit-объект содержит автора, сообщение, ссылку на дерево и родительские коммиты.

Ветки репозитория

branch.py:
#!/usr/bin/env python3
# -*- coding: utf8 -*-
import os.path


def create(dir: str, name: str, commit: str):
    path = os.path.join(dir, ".git/refs/heads", name)
    os.makedirs(os.path.dirname(path), 0o755, True)
    with open(path, "wb") as f:
        f.write(b"%s\n" % commit.encode("utf-8"))
    print(commit, "<==", name)

Это просто файл с именем ветки, который содержит хэш коммита. То есть это просто именованная ссылка на конкретный коммит.

Создаём репозиторий

main.py:
#!/usr/bin/env python3
# -*- coding: utf8 -*-

import tree, blob, repo, commit, branch

dir = "."
repo.create(dir)
foo = blob.create(dir, b"Git repo from scratch\n")
barv1 = blob.create(dir, b"Lorem\nippsum\ndolor\nsit\namet\n")
barv2 = blob.create(dir, b"Lorem\nipsum\ndolor\nsit\namet\n")

t1 = tree.create(dir, [
    (0o100644, "bar.txt", barv1),
    (0o100644, "foo.txt", foo),
])
c1 = commit.create(dir, t1, [], "Artem <bozaro@yandex.ru> 1696268685 +0300", "Commit 1")

t2 = tree.create(dir, [
    (0o040000, "bar", tree.create(dir, [
        (0o100644, "bar.txt", barv2),
    ])),
    (0o100644, "foo.txt", foo),
])
c2 = commit.create(dir, t2, [c1], "Artem <bozaro@yandex.ru> 1696662204 +0300", "Minor fix")
c3 = commit.create(dir, t2, [c1, c2], "Artem <bozaro@yandex.ru> 1696662427 +0300", "Merge")
branch.create(dir, "demo", c3)

Выполняем скрипт

./main.py
2b0086133aeb100d73eecaee115ab18df6c8985a blob
90a13b791d9a37e20b01e01eb4c6e77695f6a243 blob
7b335cc9bca0e70782ba26a376e33242e5154199 blob
6a484aeaa36216c81cbca06f89c13f5e923fead9 tree
3a78335e22d824bc4896e23803b089e6bb8fbafd commit
6783d45f5287538b95e4518ab92388a16f8b8387 tree
031eccc2fe7cd55cf47212c320db7fdb1800464f tree
52688b886931c02e4c3a2527c09f1c426720b526 commit
e90d9e7d93bef10d08ecba3c1ca0dbd4277a7121 commit
e90d9e7d93bef10d08ecba3c1ca0dbd4277a7121 <== demo
    

Проверяем


git log demo --graph --stat

*   commit e90d9e7d93bef10d08ecba3c1ca0dbd4277a7121 (demo)
|\  Merge: 3a78335 52688b8
| | Author: Artem <bozaro@yandex.ru>
| | Date:   Sat Oct 7 10:07:07 2023 +0300
| |
| |     Merge
| |
| * commit 52688b886931c02e4c3a2527c09f1c426720b526
|/  Author: Artem <bozaro@yandex.ru>
|   Date:   Sat Oct 7 10:03:24 2023 +0300
|
|       Minor fix
|
|    bar.txt => bar/bar.txt | 2 +-
|    1 file changed, 1 insertion(+), 1 deletion(-)
|
* commit 3a78335e22d824bc4896e23803b089e6bb8fbafd
  Author: Artem <bozaro@yandex.ru>
  Date:   Mon Oct 2 20:44:45 2023 +0300

      Commit 1

   bar.txt | 5 +++++
   foo.txt | 1 +
   2 files changed, 6 insertions(+)
    

То же самое средствами Git

repo.sh:
#!/bin/bash -ex
git init -b demo .
git config --local user.email bozaro@yandex.ru
git config --local user.name Artem

echo -en "Git repo from scratch\n" > foo.txt
echo -en "Lorem\nippsum\ndolor\nsit\namet\n" > bar.txt
git add foo.txt bar.txt
export GIT_COMMITTER_DATE="Mon Oct 2 20:44:45 2023 +0300"
git commit --date "$GIT_COMMITTER_DATE" -m "Commit 1"

git checkout -b fix
mkdir -p bar
mv -f bar.txt bar/bar.txt
sed -i s/ippsum/ipsum/g bar/bar.txt
git add bar.txt bar/bar.txt
export GIT_COMMITTER_DATE="Sat Oct 7 10:03:24 2023 +0300"
git commit --date "$GIT_COMMITTER_DATE" -m "Minor fix"

git checkout demo
export GIT_COMMITTER_DATE="Sat Oct 7 10:07:07 2023 +0300"
export GIT_AUTHOR_DATE="${GIT_COMMITTER_DATE}"
git merge -m "Merge" --no-ff fix

git log demo --graph --stat

Git это направленный ациклический граф

66-way merge: "Christ, that's not an octopus, that's a Cthulhu merge"

Merge remote-tracking branches 'asoc/topic/ad1836', 'asoc/topic/ad193x', 'asoc/topic/adav80x', 'asoc/topic/adsp', 'asoc/topic/ak4641', 'asoc/topic/ak4642', 'asoc/topic/arizona', 'asoc/topic/atmel', 'asoc/topic/au1x', 'asoc/topic/axi', 'asoc/topic/bcm2835', 'asoc/topic/blackfin', 'asoc/topic/cs4271', 'asoc/topic/cs42l52', 'asoc/topic/da7210', 'asoc/topic/davinci', 'asoc/topic/ep93xx', 'asoc/topic/fsl', 'asoc/topic/fsl-mxs', 'asoc/topic/generic', 'asoc/topic/hdmi', 'asoc/topic/jack', 'asoc/topic/jz4740', 'asoc/topic/max98090', 'asoc/topic/mxs', 'asoc/topic/omap', 'asoc/topic/pxa', 'asoc/topic/rcar', 'asoc/topic/s6000', 'asoc/topic/sai', 'asoc/topic/samsung', 'asoc/topic/sgtl5000', 'asoc/topic/spear', 'asoc/topic/ssm2518', 'asoc/topic/ssm2602', 'asoc/topic/tegra', 'asoc/topic/tlv320aic3x', 'asoc/topic/twl6040', 'asoc/topic/txx9', 'asoc/topic/uda1380', 'asoc/topic/width', 'asoc/topic/wm8510', 'asoc/topic/wm8523', 'asoc/topic/wm8580', 'asoc/topic/wm8711', 'asoc/topic/wm8728', 'asoc/topic/wm8731', 'asoc/topic/wm8741', 'asoc/topic/wm8750', 'asoc/topic/wm8753', 'asoc/topic/wm8776', 'asoc/topic/wm8804', 'asoc/topic/wm8900', 'asoc/topic/wm8901', 'asoc/topic/wm8940', 'asoc/topic/wm8962', 'asoc/topic/wm8974', 'asoc/topic/wm8985', 'asoc/topic/wm8988', 'asoc/topic/wm8990', 'asoc/topic/wm8991', 'asoc/topic/wm8994', 'asoc/topic/wm8995', 'asoc/topic/wm9081' and 'asoc/topic/x86' into asoc-next

Следствия внутренней структуры

Автор, коммитер, дата — всё условно

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

В частности, пользователя отправившего коммит в GitHub можно узнать колько через API GitHub-а. Средстави Git его узнать нельзя.

Один из частых способов решения проблемы: запрет заливки коммитов с другим коммитером на уровне серверных hook-ов.

Коммит нельзя поменять задним числом

А как работает команда git notes?

git:(demo)  git log -n1
commit e90d9e7d93bef10d08ecba3c1ca0dbd4277a7121 (HEAD -> demo)
Merge: 3a78335 52688b8
Author: Artem <bozaro@yandex.ru>
Date:   Sat Oct 7 10:07:07 2023 +0300

    Merge
git:(demo)  git notes add -m 'Add commit info'
git:(demo)  git log -n1
commit e90d9e7d93bef10d08ecba3c1ca0dbd4277a7121 (HEAD -> demo)
Merge: 3a78335 52688b8
Author: Artem <bozaro@yandex.ru>
Date:   Sat Oct 7 10:07:07 2023 +0300

    Merge

Notes:
    Add commit info

Локально хранится вся история

В репозитории могут храниться большие файлы и размер репозитория может быть проблемой.

Git Large File Storage (LFS)

Расширение Git'а, предназначенное для версионирования больших файлов. Git LFS заменяет большие файлы (аудио, видео, наборы данных или графические файлы) текстовыми указателями внутри Git'а, в то время как само содержимое этих файлов сохраняется на удалённом сервере, таком как GitHub.com.

Большинство серверов Git (например, GitLab, GitHub, BitBucket, Gitea) поддерживают Git LFS.

Команды Git

Начало работы

git config

Крайне рекомендуется начать с указания имени пользователя :)

git config --global user.email "bozaro@yandex.ru"
git config --global user.name "Artem"

git init

Команда git init позволяет создать новый пустой репозиторий.

git clone

Команда вида git clone git@github.com:bozaro/presentations.git позволяет создать локальную рабочую копию удалённого репозитория.

Кратенько о .gitignore

Простой пример

.idea/
.build/
**/~*
*.pyc
.DS_Store
    

Исключения

# exclude everything except directory foo/bar
/*
!/foo
/foo/*
!/foo/bar

Базовый сценарий Git

git add

git add foo.txt
git add "**/*.txt"

Добавление/удаление файла в индекс Git

git commit

git commit -m "Commit message"
git commit --amend -m "Commit message"

Созание коммита из текущего состояния индекса.

git push

git push --set-upstream origin

Отправка локальной истории текущей ветки на сервер.

Просмотр истории

git log

git log origin/master..master
git log origin/master...master

Просмотр истории коммитов.

git annotate

Просмотр информации о том, в каком коммите какая строка файла была изменена в последний раз.

Просмотр истории

git show

git show HEAD
git show HEAD~2:README.md

Просмотр изменений коммита.

Получение изменений с сервера

git pull

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

git fetch

Просто загружает изменения с сервера.

git status

Отображает текущее состояние рабочей копии.


On branch master
Your branch is up to date with 'origin/master'.
    

Ветвление

git checkout

git checkout master

Переключение рабочей копии на ветку master.

git checkout -b feature-123

Создание новой ветки feature-123 от текущей головы рабочей копии.

git merge

git checkout master
git merge feature-123

Переключение на ветку master и вливание в неё ветки feature-123.

Подобрать вишенку

git cherry-pick

git cherry-pick fcb2604

Создание нового коммита на текущей ветке по образу и подобию коммита fcb2604

Правила хорошего тона

Переписывание истории

git rebase

%%{init: { 'gitGraph': {'rotateCommitLabel': false}} }%%
gitGraph
        commit id:"A"
        commit id:"B"
        branch "topic*"
        commit id:"C"
        commit id:"D"
        checkout main
        commit id:"E"
        commit id:"F"
    
git rebase main topic
git rebase main
%%{init: { 'gitGraph': {'rotateCommitLabel': false}} }%%
gitGraph
        commit id:"A"
        commit id:"B"
        commit id:"E"
        commit id:"F"
        branch "topic*"
        commit id:"C'"
        commit id:"D'"
    

Переписывание истории

git rebase

%%{init: { 'gitGraph': {'rotateCommitLabel': false}} }%%
gitGraph
        commit id:"A"
        commit id:"B"
        branch topicA
        commit id:"C"
        commit id:"D"
        branch topicB
        commit id:"E"
        commit id:"F"
    
git rebase --onto main topicA topicB
%%{init: { 'gitGraph': {'rotateCommitLabel': false}} }%%
gitGraph
        commit id:"A"
        commit id:"B"
        branch topicA
        commit id:"C"
        commit id:"D"
        checkout main
        branch topicB
        commit id:"E'"
        commit id:"F'"
    

Переписывание истории

git rebase -i

# Rebase 91e416e..61baca6 onto 91e416e (11 commands)
#
# 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 [-C | -c]  = like "squash" but keep only the previous
#                    commit's log message, unless -C is used, in which case
#                    keep only this commit's message; -c is same as -C but
#                    opens the editor
# x, exec  = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop  = remove commit
# l, label 

Git autosquash

Пример использования

$ git add .
$ git commit --fixup HEAD
$ git log -n2 --oneline
6f3bc46 (HEAD -> foo) fixup! Commit 1
3a78335 Commit 1
$ git rebase -i HEAD^^ --autosquash

Включение на постоянной основе

git config --global rebase.autosquash true

Чистка

git clean

Удаляет файлы, про которые не знает Git.

-d
Удалять директории.
-f, --force
Удалять неизвестные директории без лишних вопросов.
-x
Удалять игнорируемые файлы.

git reset --hard

Вернуть содержимое всех файлов в соответствие с указанным коммитом.

Работа с временными изменениями

git stash

Сохранение временных изменений в стеке.

git stash apply

Применение временных изменений.

git drop

Выкидывание временных изменений из стека.

git stash pop

Применение временных изменений и выкидывание их из стека.

Git LFS

git lfs

$ git lfs install
$ git lfs track "*.tar.gz"
$ git add .gitattributes
$ git commit -m "track *.tar.gz files using Git LFS"

.gitattributes

*.tar.gz filter=lfs diff=lfs merge=lfs -text

Git bisect

git bisect bad

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

Git revert

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

Не очевидно, что "откатить" git revert при помощи повторного git merge нельзя. Его надо откатывать через git revert коммита с ревертом.

%%{init: { 'gitGraph': {'rotateCommitLabel': true}} }%%
gitGraph
        commit id:"A"
        commit id:"B"
        branch topic
        commit id:"C"
        commit id:"D"
        checkout main
        merge topic id:"E"
        commit id:"F"
        commit id:"revert E" type: REVERSE
        commit id:"G"
        commit id:"revert revert E" type: REVERSE
    

Ааа!!! Я запутался!!!

git reflog

Git reflog показывает последние коммиты, на которых была голова рабочей копии.

Особенности операционных систем

Окончания файлов (.gitattributes)

text=auto
Вариант по умолчанию. Используется EOL для текущей OS
text eol=crlf
Git будет всегда преобразовывать окончания строк в CRLF при извлечении, даже в OSX или Linux.
text eol=lf
Git будет всегда преобразовывать окончания строк в LF, даже в Windows.
binary
Файлы не являются текстом и изменять их не следует. Параметр binary также является псевдонимом для -text -diff.

Регистрозависимость

В Linux обычно файловая система является регистрозависимой.

В Windows и OSX файловая система является регистронезависимой.

Executable bit

Флаг исполняемого файла специфичен только для POSIX-файловых систем. В Windows его нет.

Кодировка и имена файлов

Статья про нормализацию Unicode: habr.com/ru/articles/45489

Normalization Form C (NFC)
По-умолчанию используется в Windows и Linux
Normalization Form D (NFD)
По-умолчанию используется в OSX
SourceNormalization Form D (NFD)Normalization Form C (NFC)
Å
00C5
A
0041
˚
030A
Å
00C5
ô
00F4
o
006F
ˆ
0302
ô
00F4
й
0439
и
0438
˘
0306
й
0439

Из Subversion в Git

git svn

Утилита, позволяющая локально держать Git-репозиторий и из него коммитить в Subversion.

SubGit

Официальный сайт: subgit.com

Сервис, который держит в синхронизированном состоянии как Subversion, так и Git вариант репозитория.

git-as-svn

Статья на Хабр: habr.com/ru/companies/vk/articles/241095

Сервис, реализующий Subversion-сервис поверх существующего Git-репозитория.

В отличие от SubGit, не использует для работы отдельную копию Subversion-репозитория. Все данные только в Git.

Домашняя работа

Домашняя работа

Пройти вкладку «Основы» на сайте learngitbranching.js.org.

Спасибо за внимание!