2.2Рабочая копия
Разберёшь, какие команды меняют index и working tree, а какие сохраняют незаконченные правки. Научишься находить правило ignore и задавать одинаковую нормализацию строк для всей команды.
- Зачем checkout разделили на
switchиrestoreи откудаrestoreберёт версию файла. - Что сохраняет
git stash, из каких коммитов состоит запись и почему после конфликта она остаётся в списке. - Почему правило в
.gitignoreне срабатывает или задевает лишнее и как найти строку, которая скрыла файл. - Откуда в диффе «изменены все строки» и как
.gitattributesлечит это у всей команды.
checkout: две команды под одним именем
С именем ветки git checkout переключает HEAD и переписывает индекс с рабочей копией, а
если мешает незакоммиченная правка, отказывает (глава 1.3). С путём он копирует файл из индекса
поверх рабочей копии и ничего не спрашивает. Что делать, git выбирает по аргументу. У Ани в клоне
поправлен handler.go, в проекте есть каталог docs, а на сервере ветка
docs:
$ git status --short
M handler.go
$ git checkout docs # каталог или ветка?
fatal: 'docs' could be both a local file and a tracking branch.
Please use -- (and optionally --no-guess) to disambiguate
$ git checkout feture # опечатка в имени ветки
error: pathspec 'feture' did not match any file(s) known to git
$ git checkout .
Updated 1 path from the index
$ git status --short
Опечатка в имени ветки превратилась в путь, а точка, то есть все файлы каталога, молча выбросила
правку. В git 2.23 (2019) из checkout выделили две узкие команды: git switch работает
только с ветками, git restore только с файлами. До 2.51 обе числились
экспериментальными. checkout не объявлен устаревшим, но подсказки git status предлагают
уже restore. Чужой аргумент новые команды не примут:
$ git switch docs
M handler.go
branch 'docs' set up to track 'origin/docs'.
Switched to a new branch 'docs'
$ git switch feture
fatal: invalid reference: feture
$ git switch v0.1.0
fatal: a branch is expected, got tag 'v0.1.0'
hint: If you want to detach HEAD at the commit, try again with the --detach option.
$ git restore main
error: pathspec 'main' did not match any file(s) known to git
На тег или коммит switch без --detach не встанет, и в detached HEAD (глава 1.2)
случайно не попасть. Старые формы и новые:
| Было | Стало | Что происходит |
|---|---|---|
git checkout -b fix | git switch -c fix | создать ветку и перейти |
git checkout v0.1.0 | git switch --detach v0.1.0 | detached HEAD на теге |
git checkout --orphan site | git switch --orphan site | ветка без истории; switch, в отличие от checkout, очищает индекс и диск |
git checkout -- main.go | git restore main.go | файл из индекса на диск |
git checkout docs -- api.md | git restore -s docs -SW api.md | файл из другой ветки в индекс и на диск |
git reset -- main.go | git restore --staged main.go | убрать правку из индекса |
restore: откуда и куда
Куда писать, задают флаги: -W (--worktree, по умолчанию) пишет в рабочую
копию, -S (--staged) в индекс, оба сразу пишут в оба места. Откуда брать,
задаёт -s (--source): коммит, ветка или тег. Без него рабочая копия
восстанавливается из индекса, а индекс из HEAD.
| Команда | Откуда | Куда |
|---|---|---|
git restore f | индекс | рабочая копия |
git restore --staged f | HEAD | индекс |
git restore -SW f | HEAD | индекс и рабочая копия |
git restore -s HEAD~2 f | коммит HEAD~2 | рабочая копия |
У handler.go три версии: в HEAD, в индексе с третьим заказом и на диске с дописанным
четвёртым.
$ git status --short
MM handler.go
$ git restore handler.go # диск ← индекс: четвёртый заказ пропал
$ git status --short
M handler.go
$ git restore --staged handler.go # индекс ← HEAD: правка осталась на диске
$ git status --short
M handler.go
С --source restore пишет только на диск, пока не попросишь --staged, а
git checkout коммит -- путь пишет сразу в индекс и на диск. В истории Ани три коммита:
«Сервис заказов» с портом 8080, «Порт 8081» и «Описание API».
$ git restore --source=HEAD~2 main.go
$ git status --short
M main.go
$ git restore main.go
$ git checkout HEAD~2 -- main.go
$ git status --short
M main.go
Второе отличие видно на файлах, которых в источнике нет. checkout работает в режиме overlay
(«наложение»): кладёт файлы коммита поверх текущих и ничего не удаляет. restore приводит пути к
источнику в точности. В HEAD~1 ещё нет docs/api.md:
$ git checkout HEAD~1 -- .
$ git status --short
$ git restore --source=HEAD~1 .
$ git status --short
D docs/api.md
Переключают режим флаги --no-overlay у checkout и --overlay у restore.
Правку, которую ни разу не добавляли в индекс, git не хранит нигде, кроме самого файла. После
git restore или git checkout . её нет ни в объектах, ни в reflog, и
git fsck (глава 5.2) её не найдёт. Сомневаешься, сначала git stash. А
git restore -p показывает фрагменты по одному и про каждый спрашивает
Discard this hunk from worktree.
stash: отложить незаконченное
У Ани в индексе третий заказ в handler.go, в main.go вызов ещё не написанной
обёртки withLog, рядом новый cache.go и собранный server из
.gitignore. Просят срочно поправить main. stash
сохраняет незакоммиченные правки в отдельную запись и возвращает индекс и рабочую копию к HEAD:
$ git status --short --ignored
M handler.go
M main.go
?? cache.go
!! server
$ git stash
Saved working directory and index state WIP on main: 7b653aa Описание API
$ git status --short --ignored
?? cache.go
!! server
$ git stash list
stash@{0}: WIP on main: 7b653aa Описание API
$ git stash pop -q # срочное сделано, возвращаем правки
$ git status --short
M handler.go
M main.go
?? cache.go
Два сюрприза. Без флагов stash берёт только отслеживаемые файлы: cache.go остался на
диске и уехал бы на любую ветку. И handler.go после pop больше не в
индексе: снимок индекса в записи есть, но без --index все правки выкладываются на диск.
$ git add handler.go
$ git stash -u
Saved working directory and index state WIP on main: 7b653aa Описание API
$ git status --short --ignored
!! server
$ git stash pop -q --index
$ git status --short --ignored
M handler.go
M main.go
?? cache.go
!! server
| Вызов | Правки отслеживаемых | Неотслеживаемые | Игнорируемые |
|---|---|---|---|
git stash | в запись | остаются на диске | остаются |
git stash -u | в запись | в запись, с диска удаляются | остаются |
git stash -a | в запись | в запись | в запись, с диска удаляются |
git stash --keep-index | в запись, индекс остаётся на месте | остаются | остаются |
git stash --staged (2.35) | в запись только индекс | остаются | остаются |
--keep-index нужен, чтобы проверить будущий коммит без остальных правок.
go vet падает на недописанном withLog, а третий заказ от него не зависит:
$ go vet ./...
# example.com/shop
# [example.com/shop]
vet: ./main.go:9:29: undefined: withLog
$ git stash --keep-index -u
Saved working directory and index state WIP on main: 7b653aa Описание API
$ git status --short
M handler.go
$ go vet ./... && git commit -qm "Третий заказ"
$ git stash pop -q
$ git status --short
M main.go
?? cache.go
На диске остался ровно будущий коммит, он проверен и создан, а pop вернул остальное.
Такой цикл есть в документации git stash. Но будь отложенная правка соседней с
закоммиченной, pop кончился бы конфликтом, как в разделе ниже.
Как устроена запись stash
$ git stash -u
Saved working directory and index state WIP on main: 7b653aa Описание API
$ git log --graph --oneline stash@{0}
*-. bd2d11e WIP on main: 7b653aa Описание API
|\ \
| | * 16ec9ab untracked files on main: 7b653aa Описание API
| * 6b52fe1 index on main: 7b653aa Описание API
|/
* 7b653aa Описание API
* e64310b Порт 8081
* c1ed37a Сервис заказов
$ git cat-file -p stash@{0}
tree 1c999bf0231ae9a8b809a9c3f91c733a2d685e4f
parent 7b653aa7115b9f51d13bb9901c84c99f6e7a1653
parent 6b52fe1370a8233b368ba31bfb6e9339b3578ce6
parent 16ec9abc9e3d4260515514de38becb7dd5f8ef64
author Аня <anya@example.com> 1788256800 +0300
committer Аня <anya@example.com> 1788256800 +0300
WIP on main: 7b653aa Описание API
$ git ls-tree stash@{0}^3
100644 blob 2ce54467ebfe2864ad9435e8611d0715c1659cf5 cache.go
Запись собрана из обычных коммитов. refs/stash указывает на bd2d11e
(в документации W) с тремя родителями, а его дерево хранит отслеживаемые файлы в том виде, в каком
они лежали на диске. Первый родитель, HEAD в момент stash, служит базой при возврате. Второй,
6b52fe1 (I), хранит снимок индекса, по нему работает pop --index. Третий,
16ec9ab (U), появляется только с -u или -a: в его дереве одни
неотслеживаемые файлы, родителей у него нет.
git stash -u.
Справа то, что записала команда: коммит индекса, коммит неотслеживаемых файлов без родителя и
коммит рабочей копии с тремя родителями, на который указывает refs/stash.
Список записей хранится в reflog ссылки refs/stash, отсюда имена
stash@{1} в синтаксисе reflog из главы 1.2. Спрячем ещё правку в
docs/api.md:
$ git stash -m "черновик POST"
Saved working directory and index state On main: черновик POST
$ git reflog show --format='%h %gd %gs' refs/stash
5af9976 stash@{0} On main: черновик POST
bd2d11e stash@{1} WIP on main: 7b653aa Описание API
Stash локален: refs/stash не ветка, push её не отправляет, clone не приносит. С git
2.51 записи переносят так: git stash export --to-ref refs/stashes/anya собирает их в
цепочку коммитов, её пушат, а на другой машине загружают через git stash import.
Запись git stash drop убирает только из reflog, коммиты остаются: по хешу из строки
Dropped refs/stash@{0} (bd2d11e…) команда git stash apply --index bd2d11e
вернёт правки, пока их не убрала сборка мусора (глава 5.2). А возврат записи устроен как слияние, и
у слияния бывают конфликты.
Когда pop оставляет запись
pop делает apply и drop, но drop выполняет, только если apply
прошёл чисто. Пока запись лежала, в main появился коммит с заказом B-7 в той же строке:
$ git log --oneline -1
1dbdad2 Заказ B-7
$ git stash pop # сводка status из вывода убрана
Auto-merging handler.go
CONFLICT (content): Merge conflict in handler.go
The stash entry is kept in case you need it again.
$ git status --short
UU handler.go
M main.go
?? cache.go
$ git stash list
stash@{0}: WIP on main: 7b653aa Описание API
$ sed -n 9,13p handler.go
<<<<<<< Updated upstream
orders := []string{"A-1", "B-7"}
=======
orders := []string{"A-1", "A-2", "A-3"}
>>>>>>> Stashed changes
apply провёл трёхстороннее слияние (глава 3.1) с базой 7b653aa: с одной стороны текущий
HEAD, с другой дерево W. Маркеры подписаны Updated upstream (ветка) и
Stashed changes (запись). Выброси git запись сейчас, и неудачный разбор конфликта
оставил бы тебя без целой копии правок.
Заметь M main.go: чисто применённая правка оказалась в индексе, хотя
пряталась недобавленной. Так бывает при любом слиянии с конфликтом (глава 3.2). Оставляем в
handler.go оба заказа, чистим индекс и удаляем запись сами:
$ git restore --staged .
$ git status --short
M handler.go
M main.go
?? cache.go
$ git stash drop
Dropped refs/stash@{0} (bd2d11e5b19f1ee7db809633fde7e98e8681d865)
Разбирать конфликт не обязательно. git stash branch создаёт ветку от первого родителя
записи, где правки и делались, применяет их вместе с индексом и при успехе удаляет запись. База
совпадает, конфликту взяться неоткуда:
$ git stash branch orders-a3 # сводка status из вывода убрана
Switched to a new branch 'orders-a3'
Dropped refs/stash@{0} (bd2d11e5b19f1ee7db809633fde7e98e8681d865)
$ git status --short
M handler.go
M main.go
?? cache.go
$ git log --oneline -1
7b653aa Описание API
.gitignore: как читаются шаблоны
Шаблон без слеша совпадает с именем на любой глубине. Слеш в начале или в середине привязывает его
к каталогу, где лежит .gitignore, слеш в конце оставляет только каталоги. Звёздочка не
проходит через /, а ** проходит.
| Шаблон | Что игнорирует |
|---|---|
server | server на любой глубине, в том числе каталог cmd/server/ |
/server | только server рядом с .gitignore |
*.test | бинарники go test -c вроде shop.test в любом каталоге |
logs/ | каталоги logs, файл с таким именем не задевает |
docs/*.md | docs/api.md, но не docs/v1/api.md |
docs/**/*.md | оба: /**/ совпадает с любым числом каталогов, и с нулём тоже |
!logs/.gitkeep | отменяет игнор, если сам каталог logs не исключён |
Первая строка таблицы подводит Go-проекты чаще остальных. Бинарник когда-то собирали в корне и
внесли в игнор без слеша, а теперь Аня переносит main.go в cmd/server/:
$ cat .gitignore
server
$ mkdir -p cmd/server
$ mv main.go cmd/server/main.go
$ git add .
$ git status --short
D main.go
$ git check-ignore -v cmd/server/main.go
.gitignore:1:server cmd/server/main.go
$ echo /server > .gitignore
$ git add .
$ git status --short
M .gitignore
R main.go -> cmd/server/main.go
git add . молча пропустил каталог, и в коммит ушло бы одно удаление.
git check-ignore -v называет файл правил, номер строки и сработавший шаблон. На
! попадаются тоже часто: в исключённый каталог git не заглядывает, и отрицание для
файла внутри не действует. Исключать надо содержимое каталога:
$ printf '/server\nlogs/\n!logs/.gitkeep\n' > .gitignore
$ git check-ignore -v logs/.gitkeep
.gitignore:2:logs/ logs/.gitkeep
$ printf '/server\nlogs/*\n!logs/.gitkeep\n' > .gitignore
$ git check-ignore -v logs/.gitkeep logs/app.log
.gitignore:3:!logs/.gitkeep logs/.gitkeep
.gitignore:2:logs/* logs/app.log
$ git status --short --ignored logs
?? logs/
!! logs/app.log
Совпавший шаблон с ! значит, что файл виден. status --ignored помечает
скрытое через !!, так удобно проверять, не спрятано ли лишнее.
Где ещё живут правила игнора
Источники по старшинству: командная строка, .gitignore в каталоге файла и выше (нижний
перекрывает верхний), .git/info/exclude, файл из core.excludesFile. Внутри
уровня решает последний совпавший шаблон. .gitignore коммитится и действует у всех.
В .git/info/exclude держат личное для одного репозитория, он никуда не уезжает. А
core.excludesFile общий для всех твоих репозиториев, по умолчанию это
$XDG_CONFIG_HOME/git/ignore или ~/.config/git/ignore: место для
.DS_Store и .idea/.
$ git config get core.excludesFile # не задан: действует ~/.config/git/ignore
$ mkdir -p ~/.config/git
$ printf '.DS_Store\n.idea/\n*.log\n' > ~/.config/git/ignore
$ echo 'notes.md' >> .git/info/exclude
$ echo '!audit.log' >> .gitignore
$ touch notes.md debug.log audit.log
$ git status --short
M .gitignore
?? audit.log
$ git check-ignore -v notes.md audit.log
.git/info/exclude:7:notes.md notes.md
.gitignore:2:!audit.log audit.log
debug.log скрыл глобальный *.log, а audit.log вернула строка в
старшем .gitignore. Для глобального файла check-ignore -v печатает полный
путь. Правило в exclude на строке 7, потому что первые шесть git записал комментариями
при создании репозитория.
На отслеживаемые файлы игнор не действует: git видит их всегда, check-ignore о них
молчит, а снять с учёта может только git rm --cached (глава 1.3). По той же причине
файл, добавленный через git add -f, дальше отслеживается как обычный. Игнорируемое git
считает расходным: switch перезапишет его молча (глава 1.3), stash -u не
сохранит, git clean -fdX удалит, а git clean -ndX заранее покажет, что именно.
.gitattributes: концы строк
Редакторы на Windows заканчивают строку байтами CR LF, на Linux и macOS одним LF. Без правил git
кладёт в блоб байты как есть. У Бори на Windows core.autocrlf выключен, и редактор
сохранил handler.go с CRLF после правки одной строки:
$ git diff --stat
handler.go | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
$ git diff --stat --ignore-cr-at-eol
handler.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
$ git ls-files --eol handler.go
i/lf w/crlf attr/ handler.go
Колонки ls-files --eol: i/ в индексе, w/ на диске,
attr/ действующие атрибуты. После коммита Бори у Ани в репозитории
i/crlf, в истории правка на все 11 строк, и blame приписывает Боре весь файл.
Обычно советуют core.autocrlf=true. С ним git переводит CRLF в LF при
add и LF в CRLF при записи на диск, и установщик Git for Windows ставит его по
умолчанию. Но это настройка машины, в репозиторий она не попадает. Клон с ней (преобразование от
системы не зависит, пример воспроизводится и на macOS):
$ git clone -q -c core.autocrlf=true shop.git win
$ cd win
$ git ls-files --eol handler.go main.go
i/lf w/crlf attr/ handler.go
i/lf w/crlf attr/ main.go
$ gofmt -l .
handler.go
main.go
$ git status --short
$ git config core.autocrlf false
$ git status --short
$ touch handler.go # сменилось только время изменения
$ git status --short
M handler.go
gofmt пишет только LF и считает каждый файл с CRLF неотформатированным. Выключил настройку, и git молчит: время изменения и размер файлов прежние, содержимое он не перечитывает. Файл всплывает изменённым целиком, когда его тронут. Сразу после clone изменёнными покажутся все: индекс записан в ту же секунду, что и файлы, поэтому git сверяет их содержимое. А тот, кто настройку не включал, так и коммитит CRLF.
Лечат это атрибуты. .gitattributes коммитится, и правила одинаковы в каждом клоне.
Шаблоны в нём пишутся как в .gitignore, но каталог целиком задают как api/**:
слеш в конце на файлы внутри здесь не действует. text=auto велит git самому
отличать текст от двоичного и хранить текст с LF. eol=lf задаёт концы строк на диске и
перекрывает core.autocrlf; автоопределение он не отключает с git 2.10. Файлы, уже
закоммиченные с CRLF, git при этом не трогает, их переписывает
git add --renormalize . (git 2.16):
$ echo '* text=auto eol=lf' > .gitattributes
$ git add .gitattributes
$ git add --renormalize .
$ git status --short
A .gitattributes
M handler.go
$ git commit -qm "Хранить концы строк как LF"
$ git ls-files --eol handler.go
i/lf w/crlf attr/text=auto eol=lf handler.go
На диске остался старый файл. status чист, ведь после преобразования содержимое
совпадает с индексом, и git restore его не перепишет. Так же в клоне с
autocrlf после pull: пришедшие файлы получат LF, прочие останутся с CRLF.
На чистой рабочей копии все файлы перепишут git rm -r -q --cached . и
git restore --staged --worktree ., после чего gofmt -l молчит.
core.autocrlf помогает только тому, кто его включил, а правило в
.gitattributes действует в каждом клоне.
В корневом .gitattributes проекта Go одна строка * -text: файлы хранятся
побайтно. Комментарий над ней объясняет выбор: результат одинаков в любом окружении, от
контрибьюторов на Windows ждут редактор с LF, а случайный CRLF ловят проверки gofmt в
git-codereview. Обычному проекту без таких проверок проще * text=auto eol=lf.
.gitattributes для Go: diff=golang и сгенерированный код
В строке @@ после номеров строк стоит заголовок ханка. По умолчанию git берёт ближайшую
выше строку, которая начинается с буквы, _ или $. В Go это подводит на
многострочном SQL в обратных кавычках. С git 2.17 есть встроенный драйвер golang: он
ищет строки с func и объявления type … struct или interface.
$ git diff | grep '^@@'
@@ -25,7 +25,7 @@ WHERE user_id = $1`, userID)
$ echo '*.go diff=golang' > .gitattributes
$ git diff | grep '^@@'
@@ -25,7 +25,7 @@ func (s *Store) ListOrders(ctx context.Context, userID int64) ([]string, error)
$ git diff --word-diff | grep '\[-'
ids = append(ids, [-id-]{+"order-"+id+})
Драйвер знает и токены Go: без него --word-diff режет по пробелам и выдаёт
[-id)-]{+"order-"+id)+}. На него же опирается git log -L :ListOrders:store.go
(глава 2.3): без драйвера функция обрывается на строке SELECT id, и git следит за двумя
строками вместо двадцати.
Атрибуты читают и серверы. С linguist-generated GitHub прячет файл в диффе, пока его не
развернуть, и не считает в статистике языков. GitLab так же сворачивает файлы с
gitlab-generated. Go-файл со строкой // Code generated … DO NOT EDIT.
(соглашение из go help generate) GitHub узнаёт сам, GitLab только по словам «Code
generated by». Остальному сгенерированному, например спецификации OpenAPI, нужен атрибут. Что действует на путь, покажет git check-attr -a путь.
Отрицания ! здесь запрещены, вместо них ставят минус: -linguist-generated.
# концы строк: в репозитории и на диске LF
* text=auto eol=lf
*.bat text eol=crlf
*.png binary
# заголовки ханков, --word-diff и log -L для Go
*.go diff=golang
# сгенерированное: свернуть в ревью на GitHub и GitLab
api/openapi.gen.yaml linguist-generated gitlab-generated
binary встроенный макрос для -diff -merge -text: такой файл git не сравнивает
построчно, не сливает и не преобразует. Конфликты в go.sum и сгенерированном коде
разобраны в главе 3.2.
Вопросы
4git restore файл, копия из
индекса на диск. Добавленные снимает git restore --staged файл, копия из HEAD в индекс,
диск не трогается. Файл из прошлого достают с --source. Необратимо всё, что
перезаписывает рабочую копию: недобавленная правка нигде, кроме диска, не хранилась.Какая команда
| Что хочешь | Команда | Статус до → после |
|---|---|---|
| выбросить недобавленное | git restore f | MM → M |
| снять с индекса, оставив на диске | git restore --staged f | M → M |
| вернуть файл к HEAD целиком | git restore -SW f | MM → чисто |
| взять файл из старого коммита | git restore -s HEAD~2 f | чисто → M, с -SW → M |
Новый файл после --staged снова неотслеживаемый, с диска он не пропадает:
$ git add store.go
$ git status --short
A store.go
$ git restore --staged store.go
$ git status --short
?? store.go
Старые формы и поиск версии
То же делают git checkout -- f, git checkout коммит -- f и
git reset -- f, но checkout коммит -- f пишет сразу в индекс и на диск, а
для каталога не удаляет файлы, которых в коммите нет. Коммит ищут через
git log --oneline -- f (глава 2.3), посмотреть версию без записи на диск можно так:
git show HEAD~2:main.go. Удалённый коммитом файл вернёт
git restore -s коммит -SW путь; без -SW он появится неотслеживаемым.
Что не вернуть
git restore f, git restore -SW f и git checkout . затирают
файлы без подтверждения. Если затёртая версия ни разу не проходила через git add,
объекта с ней нет, и ни reflog, ни fsck не помогут. Если проходила, в базе остался
блоб без ссылок, и его можно выловить (глава 5.2).
git stash возвращает файлы к HEAD так же, как git restore -SW ., но
сохраняет правки коммитами. Не понадобились, удаляешь запись git stash drop, и даже
тогда коммиты живут до сборки мусора.
-u, игнорируемые только с -a. pop выполняет
apply и drop, но drop пропускает, если apply закончился конфликтом или
ошибкой: запись остаётся единственной целой копией правок.Что попадает в запись
Коммит W, на который указывает refs/stash, хранит отслеживаемые файлы с диска. Его
второй родитель I хранит индекс, третий U появляется с -u или -a и
хранит неотслеживаемые файлы. Индекс сохраняется всегда, но pop без
--index выкладывает все правки на диск, и индекс под будущий коммит придётся
собирать заново.
Почему запись остаётся
apply проводит трёхстороннее слияние с базой в первом родителе W. Если ветка за это время
поменяла те же строки, будет конфликт, и потерять запись в этот момент значило бы потерять правки.
Запись остаётся и при отказе: своя правка в файле, который трогает stash, сорвёт слияние, и
pop правки не применит. С -u тут ловушка: неотслеживаемые файлы git
выкладывает после попытки слияния, даже сорвавшейся.
$ git stash pop # сводка status из вывода убрана
error: Your local changes to the following files would be overwritten by merge:
handler.go
Please commit your changes or stash them before you merge.
Aborting
The stash entry is kept in case you need it again.
$ git status --short
M handler.go
?? cache.go
$ git restore handler.go
$ git stash pop # сводка status из вывода убрана
cache.go already exists, no checkout
error: could not restore untracked files from stash
The stash entry is kept in case you need it again.
Второй pop применил правки handler.go и main.go, но из-за
уже лежащего cache.go вернул ошибку и запись оставил. Сверь
git stash show -p --include-untracked с диском и удали запись сам.
Как довести до конца
- Разреши конфликт и выполни
git restore --staged .: после конфликта чисто применённые правки лежат в индексе, хотя прятались недобавленными. - Проверь результат и удали запись:
git stash drop. - Не хочешь разбирать конфликт:
git stash branch имясоздаст ветку от коммита, где правки делались, применит их с индексом и удалит запись.
Новый файл остаётся на диске и едет на любую ветку. Если других правок нет,
git stash отвечает No local changes to save и выходит с кодом 0, так
что скрипт примет это за успех. Для новых файлов нужен git stash -u.
.gitignore действует только на неотслеживаемые файлы:
файл из индекса git видит, пока его не убрать через git rm --cached. Если новый файл
пропал, его задело правило шире задуманного. Какое, скажет git check-ignore -v путь:
файл правил, строка и шаблон.Файл виден, хотя он в игноре
Файл попал в репозиторий раньше правила. Для отслеживаемых путей check-ignore
молчит, а с --no-index показывает, какая строка сработала бы:
$ echo 'config.local.yaml' >> .gitignore
$ echo 'debug: true' >> config.local.yaml
$ git status --short
M .gitignore
M config.local.yaml
$ git check-ignore -v config.local.yaml; echo "rc=$?"
rc=1
$ git check-ignore -v --no-index config.local.yaml
.gitignore:2:config.local.yaml config.local.yaml
Лечится git rm --cached и коммитом, последствия для коллег разобраны в главе 1.3.
Новый файл не попадает в коммит
| Правило | Что задело | Как надо |
|---|---|---|
server | каталог cmd/server/ со всем кодом | /server |
logs/ и !logs/.gitkeep | отрицание внутри исключённого каталога не работает | logs/* и !logs/.gitkeep |
*.log в глобальном файле | у тебя файл скрыт, у коллег виден | правило проекта держать в .gitignore |
Источники правил по старшинству: командная строка, .gitignore,
.git/info/exclude, core.excludesFile. Два последних коллеги не видят, и
check-ignore -v сразу покажет, из какого файла пришло правило. Всё скрытое перечислит
git status --short --ignored.
Подсказка The following paths are ignored by one of your .gitignore files появляется,
только если назвать игнорируемый путь явно. git add . пропускает такие файлы молча.
После переноса каталогов смотри git status: строка D там, где ждёшь
R, значит, что новое место файла скрыто правилом.
core.autocrlf не решает проблему: это настройка машины, а не
репозитория. Решает .gitattributes с * text=auto eol=lf и коммит после
git add --renormalize ..Как убедиться
git diff --stat --ignore-cr-at-eol покажет настоящий размер правки, а
git ls-files --eol место CRLF: i/crlf значит, что он уже в репозитории,
i/lf w/crlf значит, что только на диске.
Почему не core.autocrlf
- Установщик Git for Windows ставит
true, но хватит одного участника с другим значением, и CRLF снова в истории. - С
trueна диске CRLF, иgofmt -l .перечисляет все Go-файлы. - После смены значения в готовом клоне файлы с CRLF всплывают изменёнными по одному, когда их трогают.
Как починить
$ echo '* text=auto eol=lf' > .gitattributes
$ git add .gitattributes
$ git add --renormalize .
$ git commit -qm "Хранить концы строк как LF"
# у каждого, на чистой рабочей копии:
$ git rm -r -q --cached .
$ git restore --staged --worktree .
eol=lf перекрывает core.autocrlf, и настройка на машинах больше ни на что
не влияет. Файлам, которым нужен CRLF, пишут своё правило, например
*.bat text eol=crlf. Коммит нормализации меняет каждую строку затронутых файлов, и
blame припишет их ему. Его хеш кладут в .git-blame-ignore-revs: GitHub читает файл
сам, а локальному git blame нужна настройка blame.ignoreRevsFile (глава 2.3).
git restore --staged --worktree . после rm --cached пишет каждый файл из
HEAD, и незакоммиченные правки пропадут, поэтому сначала коммит или git stash.
Одного git restore . мало: по данным stat файлы не менялись, и git их
не тронет.
Что стоит запомнить
- switch меняет текущую ветку, restore меняет содержимое index или working tree.
- Stash сохраняет состояние правок отдельными объектами и может потребовать разрешить конфликт при применении.
- .gitignore не перестаёт отслеживать уже добавленный в Git файл.
Частые ошибки
- Не применяй restore из HEAD поверх нужной незакоммиченной правки.
- Правила .gitattributes могут нормализовать содержимое по-разному в index и working tree.
Самопроверка
Сначала реши задачу самостоятельно, затем открой ответ и сравни ход рассуждений.
Чтение кода
1. Найди правило, скрывшее файл
Почему Git игнорирует config/local.yaml и какой файл содержит совпавшее правило?
$ git check-ignore -v config/local.yaml
.gitignore:12:config/*.yaml config/local.yaml
Ответ и разбор
Файл совпал с шаблоном config/*.yaml в строке 12 корневого .gitignore. Проверь, относится ли шаблон только к локальной конфигурации; если он слишком широк, уточни путь или исключение.
Код-ревью
2. Не рассчитывай, что ignore снимет tracked файл
Пароль уже попал в commit. Разработчик добавил файл в .gitignore и считает проблему закрытой. Что ещё нужно сделать?
echo config.yaml >> .gitignore
Ответ и разбор
Ignore действует на неотслеживаемые файлы и не удаляет объект из истории. Убери файл из текущего index, отзови опубликованный секрет и, если нужно удалить его из истории, проведи отдельную согласованную перепись истории с обновлением клонов.
Применение
3. Разбери stash после конфликта
После git stash pop возник конфликт в файле. Почему запись может остаться в git stash list и когда безопасно её удалить?
Ответ и разбор
Git не удаляет stash, пока применение не завершилось успешно: иначе конфликт оставил бы незавершённые изменения без сохранённой копии. Разреши конфликт, проверь рабочее дерево и убедись, что нужные правки восстановлены; затем удали только эту запись через git stash drop.
Применение
4. Настрой окончания строк
На Windows одни и те же файлы постоянно показывают diff на каждую строку. Как закрепить нормализацию Go-файлов для всей команды?
Ответ и разбор
Добавь в репозиторий .gitattributes правило вроде *.go text eol=lf, затем нормализуй уже отслеживаемые файлы отдельным контролируемым commit. Проверь diff до и после на чистом checkout. Локальная настройка core.autocrlf у каждого разработчика не задаёт единую политику репозитория.