Всего через два месяца после версии 2.6 вышел Git 2.7 с новыми возможностями, исправлениями и улучшениями производительности. Что интересного он нам приготовил? Я расскажу о нескольких новинках, которые показались интересными команде Bitbucket.

Полноценный набор команд git worktree


Команда git worktree появилась в Git 2.5, она позволяет выгружать и одновременно работать со многими ветками репозитория в отдельных папках. Например, если нужно сделать срочную правку, но при этом не хочется трогать текущую рабочую копию, можно просто выгрузить нужную ветку в новую папку с помощью команды:
$ git worktree add -b hotfix/BB-1234 ../hotfix/BB-1234
Preparing ../hotfix/BB-1234 (identifier BB-1234)
HEAD is now at 886e0ba Merged in bedwards/BB-13430-api-merge-pr (pull request #7822)


Git 2.7 добавляет команду git worktree list, которая выводит список рабочих копий репозитория и веток, ассоциированных с ними:
$ git worktree list
/Users/kannonboy/src/bitbucket/bitbucket            37732bd [master]
/Users/kannonboy/src/bitbucket/staging              d5924bc [staging]
/Users/kannonboy/src/bitbucket/hotfix/BB-1234       37732bd [hotfix/BB-1234]

Улучшена поддержка многих рабочих копий командой git bisect. Ссылки, которые использовались bisect для «хороших» и «плохих» коммитов, переехали из .git/refs/bisect в .git/refs/worktrees/$worktree_name/refs/bisect, поэтому теперь стала возможной одновременная работа bisect в разных рабочих копиях репозитория.

Кроме того, начиная с Git 2.7 можно использовать git clone, указав в качестве аргумента такую отдельную рабочую копию, — при этом будет создан независимый git-репозиторий, а не ещё одна рабочая копия существующего.

Примечательно, что отдельные рабочие копии могут быть созданы не только для веток. Как и множество других команд, git worktree add можно вызвать с указателем на коммит, будь то его хеш или тег:
$ git worktree add ../git-2.4.7 ca00f80
Preparing ../git-2.4.7 (identifier git-2.4.7)
HEAD is now at ca00f80 Git 2.4.7

$ git worktree add ../git-v2.6.0 v2.6.0
Preparing ../git-v2.6.0 (identifier git-v2.6.0)
HEAD is now at be08dee Git 2.6

$ git worktree add ../git-v2.7.0 v2.7.0
Preparing ../git-v2.7.0 (identifier git-v2.7.0)
HEAD is now at 7548842 Git 2.7

$ git worktree list
/Users/kannonboy/src/git         7548842 [master]
/Users/kannonboy/src/git-2.4.7   ca00f80 (detached HEAD)
/Users/kannonboy/src/git-v2.6.0  be08dee (detached HEAD)
/Users/kannonboy/src/git-v2.7.0  7548842 (detached HEAD)


Несколько улучшений git stash


Если вы фанат git rebase, то, скорее всего, знакомы с опцией --autostash. Она автоматически сохраняет все локальные изменения во временное хранилище (stash) до выполнения rebase, а после его завершения применяет их снова.
$ git rebase master --autostash
Created autostash: 54f212a
HEAD is now at 8303dca It's a kludge, but put the tuple from the database in the cache.
First, rewinding head to replay your work on top of it...
Applied autostash.

Это удобно, поскольку можно делать rebase на «грязной» рабочей копии. Для ещё большего удобства существует параметр rebase.autostash, который делает описанное поведением по умолчанию. Применить его глобально можно с помощью команды:
$ git config --global rebase.autostash true

Этот параметр существует ещё с Git 1.8.4, но в Git 2.7 добавлена возможность отменить его с помощью опции --no-autostash. Скорее всего, эта опция добавлена для полноты, поскольку единственное, что она даёт при попытке выполнить rebase на «грязной» рабочей копии, — это соответствующее предупреждение:
$ git rebase master --no-autostash
Cannot rebase: You have unstaged changes.
Please commit or stash them.

Говоря о конфигурации, стоит также упомянуть о параметре stash.showPatch, который также появился в Git 2.7. При стандартных настройках команда git stash show выводит только краткую информацию о файлах во временном хранилище:
$ git stash show
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

Если же дополнительно указать опцию -p, вывод будет дополнен расширенным описанием изменений файлов:
diff --git a/package.json b/package.json
index c876b26..e21eeb3 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
     "mkdirp": "^0.5.0",
     "byline": "^4.2.1",
     "express": "~3.3.4",
-    "git-guilt": "^0.1.0",
+    "git-guilt": "^0.1.1",
     "jsonfile": "^2.0.0",
     "jugglingdb-sqlite3": "0.0.5",
     "jugglingdb-postgres": "~0.1.0",

Параметр stash.showPatch делает это поведением по умолчанию. Применить его глобально можно с помощью аналогичной команды:
$ git config --global stash.showPatch true

Как и в предыдущем случае, включённый параметр можно отменить и тем самым вернуться к старому краткому выводу, — с помощью опции --stat:
$ git stash show --stat
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

Будьте аккуратны: опция --no-patch не приводит к ошибке, однако она не отменяет stash.showPatch, как можно было бы ожидать.

Ускорение git filter-branch и индикатор прогресса


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

В Git 2.7 появился изящный индикатор прогресса, который отображает предполагаемое время до окончания выполнения filter-branch:


Помимо этого, в случае если filter-branch не изменяет объекты в индексе или деревья, то индекс вообще не читается при выполнении команды, что значительно увеличивает её производительность. Опция --commit-filter на анимации выше изменяет только автора каждого коммита и не затрагивает ассоциированные с ними объекты деревьев. Изменение первых 1000 коммитов Bitbucket Server заняло всего 38 секунд при использовании Git 2.7.0, в то время как аналогичная операция с Git 2.6.0 потребовала 64 секунды, то есть прирост скорости составляет целых 40%. Тесты производительности, появившиеся в Git 2.7 вместе с этими улучшениями, показывают ещё более впечатляющее ускорение — до 60%.

Улучшенное отрицание в .gitignore


Файлы .gitignore позволяют исключить из репозитория некоторые файлы, находящиеся в рабочей копии, т.е. они не будут добавляться в индекс. Шаблоны поддерживают флаг отрицания с помощью префикса !, чтобы было возможно отменить игнорирование определённого файла. Например, при таких шаблонах git будет игнорировать все файлы с расширением .json, кроме cat.json:
# .gitignore
*.json
!cat.json

Однако в Git 2.6 нельзя было применить отрицание к файлу, находящемуся в уже игнорируемой папке.
# .gitignore
/animals
!/animals/cat.json # <-- этот файл игнорируется в Git 2.6 и более ранних версиях

Начиная с Git 2.7, второй пример работает ровно так, как этого стоило ожидать: с помощью ! теперь можно отменить игнорирование файлов в папках, которые иначе были бы игнорированы.

Но и это ещё не всё!


Это только небольшая часть плюшек, которые появились в Git 2.7. Полный список изменений можно найти в заметках к релизу, а также в комментариях к коммитам в репозитории самого Git:
$ git log v2.6.0..v2.7.0



Автор оригинальной статьи — Тим Петтерсен, участвовал в разработке JIRA, FishEye/Crucible и Stash. С начала 2013 года он рассказывает о процессах разработки, git, непрерывной интеграции и поставке (continuous integration/deployment) и инструментах Atlassian для разработчиков, особенно о Bitbucket. Тим регулярно публикует заметки об этих и других вещах в Twitter под псевдонимом @kannonboy.

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


  1. Shablonarium
    06.02.2016 15:19

    Не совсем понятна разница между копией репозитория и независимым репозиторием создаваемым командой git clone с дополнительным аргументом. Если лрмать мозг, можно подумать, что она создает еще один удаленный репозиторий в источнике.


    1. detouched
      06.02.2016 16:06
      +25

      Разница в том, что при использовании git worktree репозиторий по-прежнему остаётся один, отсюда следующие преимущества:

      • git clone создаст полную копию репозитория, то есть все-все объекты будут выгружены в папку .git, в то время как git worktree выгрузит именно рабочую копию и один-единственный крохотный файл .git с указателем на основной репозиторий. Для больших репозиториев это имеет значение (к примеру, репозиторий одной известной IDE занимает порядка 4 Гб, а рабочая копия ветки, соответствующей одной из версий — всего 800 Мб).
      • Двумя одинаковыми, но независимыми репозиториями сложнее управлять. Если Вы сделали изменение в одной копии, её надо синхронизировать со второй, чтобы изменения стали в ней видны (и наоборот). Это лишние действия :) Да, иметь две одинаковые рабочие копии (то есть одной и той же ветки) с помощью git worktree запрещено — как раз чтобы не допустить рассинхронизации изменений в этой ветке в рамках одного репозитория. Тем не менее, если в одной рабочей копии сделать какие-то изменения, они тут же становятся видимыми в основном репозитории и во всех остальных его рабочих копиях, потому что хранятся все объекты git только в основной папке репозитория (папка .git есть только там).
      • Если делать git clone с удалённого репозитория, нужен доступ к нему (а если репозиторий большой, то нужно ещё время и/или хороший канал). Разумеется, можно клонировать с локального репозитория, но тогда у свежесозданного репозитория в качестве remote-ссылки будет этот локальный репозиторий. То есть, снова нужны дополнительные действия по настройке его до такого же состояния. Рабочая же копия, получаемая с помощью git worktree — это только рабочая копия: репозиторий где был, там и остался, так что настраивать ничего не нужно.


      Это не значит, что иметь два локальных репозитория с одного и того же remote — плохо. Но есть ряд случаев, когда путь с git worktree проще и удобнее.


      1. Shablonarium
        06.02.2016 16:28
        +3

        Ах вот оно что… Спасибо за комментарий.


    1. Dreyk
      06.02.2016 16:27
      +2

      В дополнение к вышесказанному, git worktree трекает все папки, которые им созданы, поэтому их можно увидеть в git worktree list


  1. tendium
    06.02.2016 20:22
    +3

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

    Вот так работало…

    /uploads/*
    !uploads/.gitkeep


    1. detouched
      07.02.2016 01:09
      +2

      В Вашем примере первая строка игнорирует не саму папку uploads, а все файлы/папки в ней.
      В 2.7 поддержали отмену игнорирования файла даже если какая-то из родительских папок игнорирована. То есть, исключить файл uploads/some_dir/file из приведённого ограничения Git 2.7 сможет, а 2.6 — нет.


  1. detouched
    07.02.2016 01:08

    Промахнулся веткой


  1. booomerang
    08.02.2016 01:25
    -1

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

    Да, как-то наткнулся на такую проблему, в итоге решил ее таким образом:
    В родительской папке выставил так, папка folder_name всё равно игнорировалась:
    *
    !folder_name

    Но внутри этой папки я создал новый файл .gitignore(folder_name/.gitignore) с таким содержимым и вроде, как сработало:
    !*