Подеколи, вам може бути потрібно прибратися – зробити репозиторій компактнішим, почистити імпортований репозиторій, або відновити втрачену працю. Ця секція розгляне деякі з цих ситуацій.
Інколи, Git автоматично виконує команду під назвою auto gc''.
Переважно, ця команда не робить нічого.
Втім, якщо вільних обʼєктів (обʼєктів не у файлі пакунку) забагато, чи забагато пакунків, то Git запускає
gc'' — це скорочення для збирання сміття (garbage collect), і ця команда робить різноманітні речі: збирає всі вільні обʼєкти та переносить їх до пакунків, обʼєднує пакунки до одного великого пакунку та вилучає обʼєкти, які стали недосяжними з будь-якого коміту й старші за декілька місяцівgit gc
у повну силу.
Ви можете виконати 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 до файлів посилань, що ми розглянули в ch10-git-internals.asc.
Ви можете бачити, де ви були востаннє, якщо виконаєте 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 <[email protected]>)
Reflog message: updating HEAD
Author: Scott Chacon <[email protected]>
Date: Fri May 22 18:22:37 2009 -0700
third commit
commit ab1afef80fac8e34258ff41fc1b867c702daa24b
Reflog: HEAD@{1} (Scott Chacon <[email protected]>)
Reflog message: updating HEAD
Author: Scott Chacon <[email protected]>
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
, яку ви трохи використовували в ch08-customizing-git.asc.
Якщо передати --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
, який ви використовували в ch07-git-tools.asc:
$ 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
схожа на використану в ch07-git-tools.asc опцію --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