Навроцкий Артем
Как съесть слона? По кусочку!
Система управления версиями позволяет хранить несколько версий одного и того же документа, при необходимости возвращаться к более ранним версиям, определять, кто и когда сделал то или иное изменение, и многое другое.
На системе управления версиями всегда есть самая свежая версия кода, которая может использоваться как эталон для разработчиков. Так же на ней можно запускать автоматизированные сборки.
Можно получить сборку от более старой версии исходного кода и, к примеру, проверить: воспроизводится ли на ней недавно обнаруженная проблема.
На стороне системы управления версиями можно принимать решения по разрешению и запрету тех или иных действий.

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

Git, Mercurial
Любая достаточно развитая технология неотличима от магии.
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.
# -*- 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-ре вида объектов в данных репозитории:
Каждый объект хранится в виде: тип <SP> размер <NUL> данные
Помимо этих объектов есть еще ссылки на коммиты (ветки).
#!/usr/bin/env python3
# -*- coding: utf8 -*-
import object
def create(dir: str, data: bytes):
return object.write(dir, "blob", data)
Blob-объект просто содержит данные файла.
Этот тип объектов не может ссылаться ни на какие другие.
#!/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-объекты содержат список объектов внутри директории.
#!/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-объект содержит автора, сообщение, ссылку на дерево и родительские коммиты.
#!/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)
Это просто файл с именем ветки, который содержит хэш коммита. То есть это просто именованная ссылка на конкретный коммит.
#!/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(+)
#!/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
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'а, предназначенное для версионирования больших файлов. Git LFS заменяет большие файлы (аудио, видео, наборы данных или графические файлы) текстовыми указателями внутри Git'а, в то время как само содержимое этих файлов сохраняется на удалённом сервере, таком как GitHub.com.
Большинство серверов Git (например, GitLab, GitHub, BitBucket, Gitea) поддерживают Git LFS.
Крайне рекомендуется начать с указания имени пользователя :)
git config --global user.email "bozaro@yandex.ru"
git config --global user.name "Artem"Команда git init позволяет создать новый пустой репозиторий.
Команда вида git clone git@github.com:bozaro/presentations.git позволяет создать локальную рабочую
копию удалённого репозитория.
.idea/
.build/
**/~*
*.pyc
.DS_Store
# exclude everything except directory foo/bar
/*
!/foo
/foo/*
!/foo/bar
git add foo.txt
git add "**/*.txt"Добавление/удаление файла в индекс Git
git commit -m "Commit message"
git commit --amend -m "Commit message"Созание коммита из текущего состояния индекса.
git push --set-upstream originОтправка локальной истории текущей ветки на сервер.
git log origin/master..master
git log origin/master...masterПросмотр истории коммитов.
Просмотр информации о том, в каком коммите какая строка файла была изменена в последний раз.
git show HEAD
git show HEAD~2:README.mdПросмотр изменений коммита.
Загружает изменения с сервера и делает merge полученной головы текущей ветки в текущую локальную ветку.
Просто загружает изменения с сервера.
Отображает текущее состояние рабочей копии.
On branch master
Your branch is up to date with 'origin/master'.
git checkout masterПереключение рабочей копии на ветку master.
git checkout -b feature-123Создание новой ветки feature-123 от текущей головы рабочей копии.
git checkout master
git merge feature-123Переключение на ветку master и вливание в неё ветки feature-123.
git cherry-pick fcb2604Создание нового коммита на текущей ветке по образу и подобию коммита fcb2604
%%{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 topicgit 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'"
%%{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'"
# 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 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.
Вернуть содержимое всех файлов в соответствие с указанным коммитом.
Сохранение временных изменений в стеке.
Применение временных изменений.
Выкидывание временных изменений из стека.
Применение временных изменений и выкидывание их из стека.
$ git lfs install
$ git lfs track "*.tar.gz"
$ git add .gitattributes
$ git commit -m "track *.tar.gz files using Git LFS"*.tar.gz filter=lfs diff=lfs merge=lfs -text
Чрезвычайно полезная утилита для поиска коммита в котором впервые проявился баг или проблема с помощью автоматического бинарного поиска.
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 показывает последние коммиты, на которых была голова рабочей копии.
В Linux обычно файловая система является регистрозависимой.
В Windows и OSX файловая система является регистронезависимой.
Флаг исполняемого файла специфичен только для POSIX-файловых систем. В Windows его нет.
Статья про нормализацию Unicode: habr.com/ru/articles/45489
| Source | Normalization Form D (NFD) | Normalization Form C (NFC) |
|---|---|---|
Å 00C5 | A 0041 ◌ ˚030A | Å 00C5 |
ô 00F4 | o 006F ◌ ˆ0302 | ô 00F4 |
й 0439 | и 0438 ◌ ˘0306 | й 0439 |
Утилита, позволяющая локально держать Git-репозиторий и из него коммитить в Subversion.

Официальный сайт: subgit.com
Сервис, который держит в синхронизированном состоянии как Subversion, так и Git вариант репозитория.

Статья на Хабр: habr.com/ru/companies/vk/articles/241095
Сервис, реализующий Subversion-сервис поверх существующего Git-репозитория.
В отличие от SubGit, не использует для работы отдельную копию Subversion-репозитория. Все данные только в Git.
Пройти вкладку «Основы» на сайте learngitbranching.js.org.
