Git
Chapters ▾ 2nd Edition

10.7 Git зсередини - Супроводження та відновлення даних

Супроводження та відновлення даних

Подеколи, вам може бути потрібно прибратися – зробити репозиторій компактнішим, почистити імпортований репозиторій, або відновити втрачену працю. Ця секція розгляне деякі з цих ситуацій.

Супроводження

Інколи, Git автоматично виконує команду під назвою “auto gc”. Переважно, ця команда не робить нічого. Втім, якщо вільних обʼєктів (обʼєктів не у файлі пакунку) забагато, чи забагато пакунків, то Git запускає git gc у повну силу. “gc” — це скорочення для збирання сміття (garbage collect), і ця команда робить різноманітні речі: збирає всі вільні обʼєкти та переносить їх до пакунків, обʼєднує пакунки до одного великого пакунку та вилучає обʼєкти, які стали недосяжними з будь-якого коміту й старші за декілька місяців

Ви можете виконати gc вручну таким чином:

$ git gc --auto

Знову, це зазвичай нічого не зробить. У вас має бути близько 7000 вільних обʼєктів або більш ніж 50 пакунків, щоб Git запустив справжню команду gc. Ви можете змінити ці обмеження за допомогою налаштувань gc.auto та gc.autopacklimit відповідно.

Також gc спакує ваші посилання до одного файлу. Припустімо, що ваше сховище містить наступні гілки та теґи:

$ find .git/refs -type f
.git/refs/heads/experiment
.git/refs/heads/master
.git/refs/tags/v1.0
.git/refs/tags/v1.1

Якщо ви виконаєте git gc, то ці файли зникнуть з директорії refs. Git перемістить їх заради ефективності до файлу під назвою .git/packed-refs, який виглядає так:

$ cat .git/packed-refs
# pack-refs with: peeled fully-peeled
cac0cab538b970a37ea1e769cbbde608743bc96d refs/heads/experiment
ab1afef80fac8e34258ff41fc1b867c702daa24b refs/heads/master
cac0cab538b970a37ea1e769cbbde608743bc96d refs/tags/v1.0
9585191f37f7b0fb9444f35a9bf50de191beadc2 refs/tags/v1.1
^1a410efbd13591db07496601ebc7a059dd55cfe9

Якщо ви оновите посилання, Git не буде редагувати цей файл, а натомість запише новий файл до refs/heads. Щоб отримати відповідний SHA-1 для даного посилання, Git шукає це посилання в директорії refs, а потім перевіряє файл packed-refs як запасний варіант. Втім, якщо ви не можете знайти посилання в директорії refs, то напевно воно у файлі packed-refs.

Зверніть увагу на останній рядок цього файлу, що починається з ^. Це означає, що теґ безпосередньо вище є анотованим, і цей рядок — коміт, на який указує цей теґ.

Відновлення даних

У якийсь момент свого життя з Git, ви можете випадково втратити коміт. Зазвичай, це трапляється через примусове видалення гілки, яка мала якусь працю, а потім виявляється, що гілка зрештою була потрібною; або ви примусово скинули (hard reset) гілку: таким чином загубили коміти, від яких вам щось було потрібно. Припускаючи що це трапилось, як ви можете повернути свої коміти?

Ось приклад, в якому гілку master вашого тестового сховища примусово скинуто до старішого коміту, а потім відновлено втрачені коміти. Спочатку, перегляньмо, в якому стані ви залишили репозиторій:

$ git log --pretty=oneline
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit

Тепер, перемістіть гілку master назад до середнього коміту:

$ git reset --hard 1a410efbd13591db07496601ebc7a059dd55cfe9
HEAD is now at 1a410ef third commit
$ git log --pretty=oneline
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit

У результаті ви втратили два останніх коміти – у вас немає жодної гілки, з якої ці коміти досяжні. Вам потрібно визначити SHA-1 останнього коміту, а потім додати гілку, яка на нього вказує. Найскладніше — знайти SHA-1 останнього коміту – адже навряд чи ви його запамʼятали, чи не так?

Часто, найшвидшим способом є використання інструменту під назвою git reflog. Під час вашої роботи Git тихенько записує де побував ваш HEAD, коли ви його змінюєте. Щоразу ви створюєте коміт або переключаєте гілки, журнал посилань (reflog) оновлюється. Журнал посилань також оновлюється командою git update-ref, що є ще однією причиною використовувати її замість простого запису значення SHA-1 до файлів посилань, що ми розглянули в Посилання Git. Ви можете бачити, де ви були востаннє, якщо виконаєте git reflog:

$ git reflog
1a410ef HEAD@{0}: reset: moving to 1a410ef
ab1afef HEAD@{1}: commit: modified repo.rb a bit
484a592 HEAD@{2}: commit: added repo.rb

Тут ви можете бачити два коміти, на які ми були переключались, проте тут про них надано небагато інформації. Щоб побачити цю ж інформацію в кориснішому вигляді, ми можемо виконати git log -g, що надасть вам звичайний вивід журналу для вашого журналу посилань.

$ git log -g
commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Reflog: HEAD@{0} (Scott Chacon <schacon@gmail.com>)
Reflog message: updating HEAD
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:22:37 2009 -0700

		third commit

commit ab1afef80fac8e34258ff41fc1b867c702daa24b
Reflog: HEAD@{1} (Scott Chacon <schacon@gmail.com>)
Reflog message: updating HEAD
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:15:24 2009 -0700

       modified repo.rb a bit

Виглядає ніби останній коміт і є втраченим, отже ви можете відновити його, якщо створите нову гілку для нього. Наприклад, ви можете розпочати гілку під назвою recover-branch для цього коміту (ab1afef):

$ git branch recover-branch ab1afef
$ git log --pretty=oneline recover-branch
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit

Файно – тепер у вас є гілка під назвою recover-branch, яка знаходиться там, де була гілка master, що робить перші два коміти знову досяжними. Далі, припустімо, що ваша втрата невідомо чому не була записана у журналі посилань – ви можете імітувати це, якщо вилучите recover-branch та журнал посилань. Тепер перші два коміти недосяжні будь-яким способом:

$ git branch -D recover-branch
$ rm -Rf .git/logs/

Через те, що дані журналу посилань зберігаються в директорії .git/logs, у вас фактично немає журналу посилань. Як можна тепер відновити коміт? Один засіб для цього — команда git fsck, яка перевіряє цілісність вашої бази даних. Якщо виконати її з опцією --full, то вона покаже вам всі обʼєкти, на які не вказує жоден інший обʼєкт:

$ git fsck --full
Checking object directories: 100% (256/256), done.
Checking objects: 100% (18/18), done.
dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4
dangling commit ab1afef80fac8e34258ff41fc1b867c702daa24b
dangling tree aea790b9a58f6cf6f2804eeac9f0abbe9631e4c9
dangling blob 7108f7ecb345ee9d0084193f147cdad4d2998293

У даному випадку, ви можете побачити втрачений коміт після рядка “dangling commit” (висячий коміт). Ви можете відновити його так само: додайте гілку, яка вказує на цей SHA-1.

Вилучення обʼєктів

Існує безліч чудових властивостей Git, проте одна з них може викликати проблеми — той факт, що команда git clone завантажує повну історію проекту, включно з кожною версією кожного файлу. Це нормально, якщо в ньому міститься вихідний код, оскільки Git дуже оптимізовано для ефективного стискання цих даних. Втім, якщо хтось колись в історії вашого проекту додав один величезний файл, кожне клонування завжди буде змушено завантажувати цей файл, навіть якщо його було вилучено з проекту наступного ж коміту. Через те, що він є досяжним в історії, він завжди буде там.

Це може бути величезною проблемою, якщо ви конвертуєте сховища Subversion або Perforce на Git. Через те, що ви не завантажуєте всю історію в цих системах, цей тип додавання призводить до декількох наслідків. Якщо ви імпортували з іншої системи, або іншим чином виявили, що ваше сховище набагато більше, ніж має бути, ось як ви можете знайти та вилучити великі обʼєкти.

Попереджаємо: цей метод знищує історію ваших комітів. Він переписує кожен обʼєкт комітів, починаючи з першого дерева, яке редагує для вилучення посилання на великий файл. Якщо ви робите це відразу після імпортування, перед тим, як хтось розпочав працювати над комітами, то все добре – інакше, ви маєте повідомити всіх співпрацівників, що вони мають перебазувати свою роботу на ваших нових комітах.

Задля демонстації, ви додасте великий файл до вашого тестового сховища, вилучите його в наступному коміті, знайдете його, та назавжди вилучите з репозиторія. Спершу, додайте великий файл до вашої історії:

$ curl https://www.kernel.org/pub/software/scm/git/git-2.1.0.tar.gz > git.tgz
$ git add git.tgz
$ git commit -m 'add git tarball'
[master 7b30847] add git tarball
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 git.tgz

Йой – ви не хотіли додавати величезний архів до вашого проекту. Краще позбутися його:

$ git rm git.tgz
rm 'git.tgz'
$ git commit -m 'oops - removed large tarball'
[master dadf725] oops - removed large tarball
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 git.tgz

Тепер, зробіть gc над вашою базою даних, та подивіться, скільки місця ви використовуєте:

$ git gc
Counting objects: 17, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (13/13), done.
Writing objects: 100% (17/17), done.
Total 17 (delta 1), reused 10 (delta 0)

Ви можете виконати команду count-objects, щоб швидко побачити, скільки місця ви використовуєте:

$ git count-objects -v
count: 7
size: 32
in-pack: 17
packs: 1
size-pack: 4868
prune-packable: 0
garbage: 0
size-garbage: 0

Елемент size-pack зазначає розмір ваших пакунків у кілобайтах, отже ви використовуєте майже 5 мегабайтів. Перед останнім комітом, ви використовували приблизно 2К – зрозуміло, що вилучення файлу в попередньому коміті не вилучило його з вашої історії. Щоразу, коли хтось клонує це сховище, йому доведеться клонувати всі 5Мб лише для того, щоб отримати цей крихітний проект, лише через те, що ви випадково додали великий файл. Позбудьмося його.

Спочатку його треба знайти. У цьому випадку, ви вже знаєте, що це за файл. Проте припустімо, що не знаєте; як вам визначити, який файл чи файли марнують стільки місця? Якщо виконати git gc, то всі обʼєкти потрапляють до пакунку; ви можете визначити великі обʼєкти за допомогою іншої кухонної команди під назвою git verify-pack, якщо упорядкуєте третє поле виводу, яке містить розмір файлу. Ви можете також пропустити результат через команду tail, адже вас цікавлять лише декілька найбільших файлів:

$ git verify-pack -v .git/objects/pack/pack-29…69.idx \
  | sort -k 3 -n \
  | tail -3
dadf7258d699da2c8d89b09ef6670edb7d5f91b4 commit 229 159 12
033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob   22044 5792 4977696
82c99a3e86bb1267b236a4b6eff7868d97489af1 blob   4975916 4976258 1438

Великий обʼєкт наприкінці: 5Мб. Щоб визначити, що це за файл, ви використаєте команду rev-list, яку ви трохи використовували в Відповідність повідомлення коміту певному формату. Якщо передати --objects до rev-list, то вона надасть список SHA-1 сум всіх комітів, а також блобів з іменами файлів, які з ними асоційовані. Ви можете використати це, щоб знайти імʼя вашого блобу:

$ git rev-list --objects --all | grep 82c99a3
82c99a3e86bb1267b236a4b6eff7868d97489af1 git.tgz

Тепер, вам треба вилучити цей файл з усіх дерев з вашого минулого. Ви легко можете бачити, які коміти змінювали цей файл:

$ git log --oneline --branches -- git.tgz
dadf725 oops - removed large tarball
7b30847 add git tarball

Ви маєте переписати всі коміти, починаючи з 7b30847, щоб повністю вилучити цей файл з вашої історії Git. Щоб це зробити, скористайтесь filter-branch, який ви використовували в Переписування історії:

$ git filter-branch --index-filter \
  'git rm --ignore-unmatch --cached git.tgz' -- 7b30847^..
Rewrite 7b30847d080183a1ab7d18fb202473b3096e9f34 (1/2)rm 'git.tgz'
Rewrite dadf7258d699da2c8d89b09ef6670edb7d5f91b4 (2/2)
Ref 'refs/heads/master' was rewritten

Опція --index-filter схожа на використану в Переписування історії опцію --tree-filter, тільки замість передавання команди, яка змінює файли на диску, ви натомість змінюєте щоразу свій індекс.

Замість вилучення окремого файлу за допомогою чогось на кшталт rm file, ви маєте вилучити його командою git rm --cached – ви мусите видалити його з індексу, не з диску. Варто зробити саме так через швидкість – адже тоді Git не має отримувати кожну ревізію на диск перед виконанням вашого фільтру, і процес може бути набагато, набагато швидшим. Ви можете досягнути того ж самого за допомогою --tree-filter, якщо бажаєте. Опція --ignore-unmatch команди git rm каже їй не вважати помилкою, якщо того, що ви вилучаєте не існує. Нарешті, ви просите filter-branch переписати вашу історію лише починаючи з коміту 7b30847, оскільки ви знаєте, де зʼявилася ця проблема. Інакше, він почне з початку історії, що вимагає невиправдано більше часу.

Ваша історія більше не містить посилання на цей файл. Втім, ваш журнал посилань та декілька посилань, які Git додав, коли ви виконали filter-branch під .git/refs/original досі вказують, отже ви маєте вилучити їх, а потім перепакувати базу даних. Вам треба позбутися будь-чого, що має вказівник на ті старі коміти перед перепакуванням:

$ rm -Rf .git/refs/original
$ rm -Rf .git/logs/
$ git gc
Counting objects: 15, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (11/11), done.
Writing objects: 100% (15/15), done.
Total 15 (delta 1), reused 12 (delta 0)

Подивімося, скільки місця ви заощадили.

$ git count-objects -v
count: 11
size: 4904
in-pack: 15
packs: 1
size-pack: 8
prune-packable: 0
garbage: 0
size-garbage: 0

Спаковане сховище зменшило розмір до 8К, що набагато краще, ніж 5Мб. Ви можете зрозуміти за значенням size, що ваш великий файл досі існує у вільних обʼєктах, отже він не зник; проте його не буде переправлено при надсиланні змін чи наступних клонуваннях, а лише це має значення. Якщо ви дійсно бажаєте, то можете вилучити файл цілковито, якщо виконаєте git prune з опцією --expire:

$ git prune --expire now
$ git count-objects -v
count: 0
size: 0
in-pack: 15
packs: 1
size-pack: 8
prune-packable: 0
garbage: 0
size-garbage: 0