Розділ 7. Гросмейстерство Git

Тепер ви вже повинні вміти орієнтуватися в сторінках git help і розуміти майже все. Однак точний вибір команди, необхідної для вирішення конкретної проблеми, може бути виснажливим. Можливо, я збережу вам трохи часу: нижче наведені рецепти, які знадобилися мені в минулому.

Релізи вихідних кодів

У моїх проектах Git управляє в точності тими файлами, які я збираюся архівувати і пускати в реліз. Щоб створити тарбол з вихідними кодами, я виконую:

$ git archive --format=tar --prefix=proj-1.2.3/ HEAD

Комміт змін

У деяких проектах може бути трудомістким повідомляти Git про кожне додавання, видаленні та перейменування файлу. Замість цього ви можете виконати команди

$ git add .
$ git add -u

Git прогляне файли в поточному каталозі і сам подбає про деталі. Замість другої команди add, виконайте git commit -a, якщо ви збираєтеся відразу зробити комміт. Дивіться * git help ignore *, щоб дізнатися як вказати файли, які повинні ігноруватися.

Ви можете виконати все це одним махом:

$ git ls-files -d -m -o -z | xargs -0 git update-index --add --remove

Опції -z і -0 запобігають невірну обробку файлових імен, що містять спеціальні символи. Оскільки ця команда додає ігноровані файли, ви можливо захочете використовувати опції -x або -X.

Мій комміт занадто великий

Ви нехтували коммітамі занадто довго? Затято писали код і згадали про управління вихідними кодами тільки зараз? Внесли ряд незв'язаних змін, тому що це ваш стиль?

Немає причин для занепокоєння. Виконайте

$ git add -p

Для кожної зробленої вами правки Git покаже змінену ділянку коду і запитає, чи повинна ця зміна потрапити в наступний комміт. Відповідайте "y" (так) або "n" (ні). У вас є й інші варіанти, наприклад відкласти вибір; введіть "?" Щоб дізнатися більше.

Коли закінчите, виконайте

$ git commit

для внесення саме тих правок, що ви вибрали ('буферизованих' змін). Переконайтеся, що ви не вказали опцію -a, інакше Git закоммітить всі правки.

Що робити, якщо ви змінили безліч файлів в багатьох місцях? Перевірка кожної окремої зміни стає обтяжливою рутиною. У цьому випадку використовуйте git add -i. Її інтерфейс не такий простий, але більш гнучкий. У декілька натискань можна додати або прибрати з буфера кілька файлів одночасно, або переглянути і вибрати зміни лише в окремих файлах. Як варіант, запустіть git commit --interactive, яка автоматично зробить комміт коли ви закінчите.

Індекс — буферна зона Git

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

Наприклад, commit -a насправді двоетапний процес. Спочатку зліпок поточного стану кожного з відстежуваних файлів поміщається в індекс. Потім зліпок, що знаходиться в індексі, записується в історію. Комміт без опції -a виконує тільки другий крок, і має сенс тільки після виконання команд, що змінюють індекс, таких як git add.

Зазвичай ми можемо не звертати уваги на індекс і робити вигляд, що взаємодіємо безпосередньо з історією. Але в даному випадку ми хочемо більш тонкого контролю, тому управляємо індексом. Ми поміщаємо зліпок деяких (але не всіх) наших змін в індекс, після чого остаточно записуємо цей акуратно сформований зліпок.

Не втрачай "голови"

Тег HEAD подібний курсору, який зазвичай вказує на останній комміт, просуваючись з кожним новим коммітом. Деякі команди Git дозволяють переміщати цей курсор. Наприклад,

$ git reset HEAD~3

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

Але як повернутися назад у майбутнє? Адже попередні комміти про нього нічого не знають.

Якщо у вас є SHA1 вихідної "голови", то:

$ git reset 1b6d

Але припустимо, ви його не записували. Не турбуйтеся: для комнад такого роду Git зберігає оригінальну "голову" як тег під назвою ORIG_HEAD, і ви можете повернутися надійно і безпечно:

$ git reset ORIG_HEAD

Полювання за "головами"

Припустимо ORIG_HEAD недостатньо. Приміром, ви тільки що усвідомили, що допустили величезну помилку, і вам потрібно повернутися до давнього комміту в давно забутій гілці.

За замовчуванням Git зберігає комміти не менше двох тижнів, навіть якщо ви наказали знищити гілку, що їх містить. Проблема в знаходженні відповідного хешу. Ви можете проглянути всі значення хешів в .git/objects і методом проб та помилок знайти потрібний. Але є шлях значно легший.

Git записує кожен підрахований ним хеш комміта в .git/logs. У підкаталозі refs міститься повна історія активності на всіх гілках, а файл HEAD містить кожне значення хешу, яке коли-небудь приймав HEAD. Останній можна використовувати щоб ​​знайти хеши коммітів на випадково обрубаних гілках.

Команда reflog надає зручний інтерфейс роботи з цими журналами. Використовуйте

$ git reflog

Замість копіювання хешів з reflog, спробуйте

$ git checkout "@{10 minutes ago}"

Чи зробіть чекаут п'ятого з кінця з відвіданих коммітів за допомогою

$ git checkout "@{5}"

Дивіться розділ «Specifying Revisions» в git help rev-parse для додаткової інформації.

Ви можете захотіти подовжити відстрочку для коммітів, приречених на видалення. Наприклад,

$ git config gc.pruneexpire "30 days"

означає, що комміти, які видаляються, будуть остаточно зникати тільки після 30 днів і після запуску git gc.

Також ви можете захотіти відключити автоматичний виклик git gc:

$ git config gc.auto 0

У цьому випадку комміти будуть видалятися тільки коли ви будете запускати git gc вручну.

Git як основа

Дизайн Git, в істинному дусі UNIX, дозволяє легко використовувати його як низькорівневий компонент інших програм: графічних та веб-інтерфейсів; альтернативних інтерфейсів командного рядка; інструментів управління патчами; засобів імпорту або конвертації, і так далі. Багато команд Git насправді - скрипти, які стоять на плечах гігантів. Невеликим доопрацюванням ви можете переробити Git на свій смак.

Найпростіший трюк — використання аліасів Git для скорочення часто використовуваних команд:

$ git config --global alias.co checkout
$ git config --global --get-regexp alias       # відображає поточні аліаси
alias.co checkout
$ git co foo        # те ж саме, що і 'git checkout foo'

Інший приклад: можна виводити поточну гілку в запрошенні командного рядка або заголовку вікна терміналу. Запуск

$ git symbolic-ref HEAD

виводить назву поточної гілки. На практиці ви швидше за все захочете прибрати "refs/heads/" і повідомлення про помилки:

$ git symbolic-ref HEAD 2> /dev/null | cut -b 12-

Підкаталог contrib — це ціла скарбниця інструментів, побудованих на Git. З часом деякі з них можуть ставати офіційними командами. В Debian та Ubuntu цей каталог знаходиться у /usr/share/doc/git-core/contrib.

Один популярний інструмент з цього каталогу — workdir/git-new-workdir. Цей скрипт створює за допомогою символічних посилань новий робочий каталог, який має спільну історію з оригінальним сховищем:

$ git-new-workdir існуюче/сховище новий/каталог

Новий каталог і файли в ньому можна сприймати як клон, з тією різницею, що два дерева автоматично залишаються синхронізованими зважаючи на спільну історію. Немає необхідності в merge, push і pull.

Ризиковані трюки

Нинішній Git робить випадкове знищення даних дуже складним. Але якщо ви знаєте, що робите, ви можете обійти захист для розповсюджених команд.

Checkout: Наявність незакомміченних змін перериває виконання checkout. Щоб перейти до потрібного комміту, навіть знищивши свої зміни, використовуйте прапор змушування (force) -f:

$ git checkout -f HEAD^

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

Reset: скидання також переривається при наявності незакомміченних змін. Щоб змусити його спрацювати, запустіть

$ git reset --hard 1b6d

Branch: Видалення гілки припиниться, якщо воно призвело б до втрати змін. Для примусового видалення введіть

$ git branch -D мертва_гілка # замість -d

Аналогічно, спроба перезапису гілки шляхом переміщення буде перервана, якщо може призвести до втрати даних. Для примусового переміщення гілки введіть

$ git branch -M джерело ціль # замість -m

У відмінності від checkout і reset, ці дві команди дають відстрочку у видаленні даних. Зміни залишаються в каталозі.git і можуть бути повернуті відновленням потрібного хешу з .git/logs (дивіться вище розділ "Полювання за головами"). За замовчуванням вони будуть зберігатися принаймні два тижні.

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

$ git clean -f -d

Наступного разу ця прикра команда спрацює!

Запобігаємо поганим коммітам

Дурні помилки забруднюють мої сховища. Найжахливіше це проблема відсутніх файлів, викликана забутим git add.

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

Якби я тільки поставив захист від дурня, використовуючи хук, який би попереджав мене про ці проблеми:

$ cd .git/hooks
$ cp pre-commit.sample pre-commit # В старих версіях Git: chmod +x pre-commit

Тепер Git скасує комміт, якщо виявить зайві пробіли або невирішені конфлікти.

Для цього керівництва я в кінці кінців додав наступне в початок хука pre-commit, щоб захиститися від своєї неуважності:

if git ls-files -o | grep \.txt$; then echo ПЕРЕРВАНО! Невідслідковувані .txt файли. exit 1 fi

Хукі підтримуються кількома різними операціями Git, дивіться git help hooks. Ми використовували приклад хука post-update раніше, при обговоренні використання Git через http. Він запускався при кожному переміщенні голови. Простий скрипт post-update оновлює файли, які потрібні Git для зв'язку через засоби повідомлення такі як HTTP.