Git
Chapters ▾ 2nd Edition

2.2 Основи Git - Запис змін до репозиторія

Запис змін до репозиторія

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

Пам’ятайте, що кожен файл вашої робочої директорії може бути в одному з двох станів: контрольований (tracked) чи неконтрольований (untracked). Контрольовані файли — це файли, що були в останньому знімку. Вони можуть бути не зміненими, зміненими або індексованими. Якщо стисло, контрольовані файли — це файли, про які Git щось знає.

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

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

Цикл життя статусу ваших файлів.
Figure 8. Цикл життя статусу ваших файлів.

Перевірка статусу ваших файлів

Щоб дізнатись, в якому стані ваші файли, варто скористатись командою git status. Якщо ви виконаєте цю команду відразу після клонування, ви маєте побачити таке:

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working directory clean

Це означає, що ваша робоча директорія чиста — іншими словами, жоден з контрольованих файлів не змінено. Git також не бачить неконтрольованих файлів, інакше він би їх тут вказав. Нарешті, ця команда показує вам, в якій ви зараз гілці та інформує вас про те, що вона не розбіглася з такою ж гілкою на сервері. Поки що, ця гілка завжди буде “master”, така гілка створюється автоматично. Це нас не обходить у цьому розділі. Галуження в git розповідає про гілки та посилання докладно.

Припустімо, ви додали новий файл до вашого проекту, простий файл README. Якщо файл раніше не існував, і ви виконаєте git status, ви побачите ваш неконтрольований файл так:

$ echo 'My Project' > README
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Untracked files:
  (use "git add <file>..." to include in what will be committed)

    README

nothing added to commit but untracked files present (use "git add" to track)

Ви можете бачити, що ваш новий файл README неконтрольований, адже він під заголовком “Untracked files” у статусі. Неконтрольований (untracked) означає, що Git бачить файл, якого нема у попередньому знімку (коміті). Git не почне включати його до ваших комітів доки ви явно не скажете йому це зробити. Так зроблено щоб ви випадково не почали включати генеровані бінарні файли чи інші файли, які ви не збирались включати. Ви все ж таки хочете почати включати README, отже почнімо контролювати файл.

Контролювання нових файлів

Щоб почати контролювати новий файл, вам треба використати команду git add. Почати контролювати файл README можна так:

$ git add README

Якщо ви знову виконаєте команду status, ви побачите, що ваш файл README тепер контролюється та готовий до включення до коміту:

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   README

Ви можете зрозуміти, що цей файл доданий, бо він під заголовком “Changes to be committed”. Якщо ви створите коміт зміни зараз, версія файлу на момент коли ви виконали git add буде збережена в знімку в історії. Ви можете пригадати, що коли ви виконали git init раніше, ви потім виконали git add <файли> — це було зроблено щоб розпочати контролювати файли у вашій директорії. Команда git add приймає шлях файлу або директорії. Якщо це директорія, команда додає усі файли в цій директорії рекурсивно.

Індексування змінених файлів

Змінімо файл, що вже контролюється. Якщо ви зміните файл CONTRIBUTING.md, що вже контролюється, та потім виконаєте команду git status знову, ви отримаєте щось на кшталт:

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   README

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   CONTRIBUTING.md

Файл CONTRIBUTING.md з’явився під секцією названою “Changes not staged for commit” — це означає, що контрольований файл був редагований у робочій директорії проте його не індексували. Щоб проіндексувати його, виконайте команду git add. git add багатоцільова команда — її слід використовувати щоб почати контролювати нові файли, щоб додавати файли, та для інших речей, наприклад позначання конфліктних файлів як розв’язаних. Про неї краще думати “Додай саме цей зміст до наступного коміту” а не “додай цей файл до проекту”. Виконаймо git add зараз для індексації файлу CONTRIBUTING.md, а потім знову виконаємо git status:

$ git add CONTRIBUTING.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   README
    modified:   CONTRIBUTING.md

Обидва файли індексовані та будуть включені до наступного коміту. Припустімо, що саме зараз ви пригадали маленьку зміну, яку ви хочете зробити в CONTRIBUTING.md до того, як зробити коміт з ним. Ви знову його відкриваєте та редагуєте, і ви готові зробити коміт. Втім, виконаймо git status ще раз:

$ vim CONTRIBUTING.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   README
    modified:   CONTRIBUTING.md

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   CONTRIBUTING.md

Якого біса? Тепер CONTRIBUTING.md є як в індексованих, так і в неіндексованих. Як таке можливо? Виявляється, що Git індексує файл саме таким, яким він був, коли ви виконали команду git add. Якщо ви зараз створите коміт, в історії збережеться версія CONTRIBUTING.md, яка була коли ви востаннє викликали git add, а не поточна версія файлу з вашої робочої директорії, коли ви виконаєте git commit. Якщо ви зміните файл після того, як виконаєте git add, вам треба знову виконати git add щоб проіндексувати останню версію файлу:

$ git add CONTRIBUTING.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   README
    modified:   CONTRIBUTING.md

Короткий статус

Хоча вивід git status доволі вичерпний, він також дещо довгий. Git також пропонує опцію короткого перегляду статусу, щоб ви могли побачити свої зміни в більш компактному вигляді. Якщо ви виконаєте git status -s або git status --short, ви отримаєте набагато простіший вивід:

$ git status -s
 M README
MM Rakefile
A  lib/git.rb
M  lib/simplegit.rb
?? LICENSE.txt

Нові неконтрольовані файли позначаються ??, нові індексовані файли позначаються A, змінені файли позначаються M тощо. Результат має дві колонки – ліва містить статус індексу, а права містить статус робочої теки. Наприклад у цьому виводі, файл README змінений у робочій директорії, проте не індексований, а файл lib/simplegit.rb змінений та індексований. Rakefile був змінений, індексований та знову змінений, тому є зміни в обох колонках.

Ігнорування файлів

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

$ cat .gitignore
*.[oa]
*~

Перший рядок каже Git ігнорувати файли, що закінчуються на “.o” або “.a” — об’єктні та архівні файли, що можуть бути продуктами компіляції вашого коду. Другий рядок каже Git ігнорувати всі файли, що їхні назви закінчуються на тильду (~), яка використовується багатьма текстовими редакторами (такими як Emacs) щоб позначати тимчасові файли. Ви також можете додати директорії log, tmp та pid, автоматично згенеровану документацію, тощо. Заповнення файлу .gitignore вашого нового сховища до початку праці зазвичай гарна думка, адже це допоможе вам випадково не додати файли, які ви не хочете додавати до репозиторія Git.

Правила для взірців, які ви можете додати до файлу .gitignore:

  • Порожні рядки та рядки, що починаються з #, ігноруються.

  • Стандартні ґлоб взірці працюють, і будуть застосовані для всього робочого дерева рекурсивно..

  • Ви можете почати взірець з прямої похилої риски (/) щоб уникнути рекурсії.

  • Ви можете завершити взірець похилою рискою (/) щоб позначити директорію.

  • Ви можете відкинути взірець, якщо почнете його зі знаку оклику (!).

Ґлоб (glob) взірці – це ніби спрощені регулярні вирази, що їх використовують оболонки. Зірочка (*) відповідає нулю або більше символам. [абв] відповідає будь-якому з символів всередині квадратних дужок (у цьому випадку а, б або в). Знак питання (?) відповідає одному символу. Квадратні дужки з символами, що розділені дефісом ([0-9]) відповідають будь-якому символу між ними (у даному випадку від 0 до 9). Ви можете використовувати дві зірочки щоб позначити вкладені директорії: a/**/z відповідає a/z, a/b/z, a/b/c/z тощо.

Ось ще один приклад файлу .gitignore:

# ігнорувати всі файли .a
*.a

# Проте відстежувати lib.a, хоч ми й ігноруємо .a файли вище
!lib.a

# Ігнорувати файл TODO тільки в поточній теці, не в інших теках subdir/TODO
/TODO

# Ігнорувати усі файли в теці build/
build/

# Ігнорувати doc/notes.txt, проте не doc/server/arch.txt
doc/*.txt

# Ігнорувати усі .pdf файли в теці doc/ та всіх її підтеках
doc/**/*.pdf
Tip

GitHub підтримує доволі вичерпний список гарних прикладів файлів .gitignore для десятків проектів та мов за адресою https://github.com/github/gitignore, якщо ви бажаєте мати зразок для свого проекту.

Note

In the simple case, a repository might have a single .gitignore file in its root directory, which applies recursively to the entire repository. However, it is also possible to have additional .gitignore files in subdirectories. The rules in these nested .gitignore files apply only to the files under the directory where they are located. (The Linux kernel source repository has 206 .gitignore files.)

It is beyond the scope of this book to get into the details of multiple .gitignore files; see man gitignore for the details.

Перегляд ваших доданих та недоданих змін

Якщо команда git status занадто зверхня для вас — ви хочете знати що саме ви змінили, а не просто які файли ви змінили — ви можете використати команду git diff. Ми докладніше розглянемо git diff пізніше, проте напевно найчастіше ви її будете використовувати для того щоб дізнатись дві речі: Що ви змінили, проте ще не індексували? Та що ви індексували та збираєтесь зберегти? Хоч git status відповідає на ці питання дуже загально — тільки показує список файлів, git diff показує вам усі індексовані та видалені рядки — латку, як вона є.

Припустімо, що ви внесли та проіндексували зміни до файлу README знову, а потім змінили файл CONTRIBUTING.md але не індексували його. Якщо ви виконаєте команду git status, ви знову побачите щось на кшталт:

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    modified:   README

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   CONTRIBUTING.md

Щоб побачити зміни, які ви ще не індексували, наберіть git diff без параметрів:

$ git diff
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8ebb991..643e24f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -65,7 +65,8 @@ branch directly, things can get messy.
 Please include a nice description of your changes when you submit your PR;
 if we have to read the whole diff to figure out why you're contributing
 in the first place, you're less likely to get feedback and have your change
-merged in.
+merged in. Also, split your changes into comprehensive chunks if your patch is
+longer than a dozen lines.

 If you are starting to work on a particular area, feel free to submit a PR
 that highlights your work in progress (and note in the PR title that it's

Ця команда порівнює вашу робочу директорію з індексом. Результат показує вам зміни, котрі ви зробили проте не індексували.

Якщо ви хочете побачити, що ви індексували та ввійде до вашого наступного коміту, ви можете скористатись git diff --staged. Ця команда порівнює індексовані зміни з вашим останнім знімком:

$ git diff --staged
diff --git a/README b/README
new file mode 100644
index 0000000..03902a1
--- /dev/null
+++ b/README
@@ -0,0 +1 @@
+My Project

Важливо пам’ятати, що команда git diff без опцій не відображає всіх змін з останнього коміту — тільки неіндексовані зміни. Якщо ви проіндексували всі свої зміни, вивід git diff буде порожнім.

Наведемо інший приклад, припустимо, що ви проіндексували файл CONTRIBUTING.md та знову його відредагували, ви можете скористатись git diff щоб побачити індексовані та неіндексовані зміни. Якщо наше середовище виглядає наступним чином:

$ git add CONTRIBUTING.md
$ echo '# test line' >> CONTRIBUTING.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    modified:   CONTRIBUTING.md

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   CONTRIBUTING.md

Тепер ви можете використати git diff щоб побачити неіндексовані зміни:

$ git diff
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 643e24f..87f08c8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -119,3 +119,4 @@ at the
 ## Starter Projects

 See our [projects list](https://github.com/libgit2/libgit2/blob/development/PROJECTS.md).
+test line

і git diff --cached щоб побачити наразі індексовані зміни (--staged і --cached мають однакове значення):

$ git diff --cached
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8ebb991..643e24f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -65,7 +65,8 @@ branch directly, things can get messy.
 Please include a nice description of your changes when you submit your PR;
 if we have to read the whole diff to figure out why you're contributing
 in the first place, you're less likely to get feedback and have your change
-merged in.
+merged in. Also, split your changes into comprehensive chunks if your patch is
+longer than a dozen lines.

 If you are starting to work on a particular area, feel free to submit a PR
 that highlights your work in progress (and note in the PR title that it's
Note
Git Diff у зовнішній утиліті.

Ми продовжимо використати команду git diff різноманітними шляхами в решті книги. Є інший шлях подивитись на різницю між файлами, якщо вам більш до смаку графічна чи зовнішня програма для відображення різниці. Якщо ви виконаєте git difftool замість git diff, ви зможете використати будь-яку з програм на кшталт emerge, vimdiff і багато інших (включно з комерційними програмами). Виконайте git difftool --tool-help щоб дізнатись, що доступно на вашій системі.

Збереження ваших змін у комітах

Припустімо, що ваш індекс саме в стані, який ви бажаєте, та тепер ви можете створити коміт з ваших зміни. Пам’ятайте, що будь-які неіндексовані зміни — будь-які файли, що ви їх створили чи змінили, але ви не виконали git add після їх редагування — не потраплять до цього коміту. Вони так і залишаться зміненими файлами на вашому диску. У цьому випадку, припустімо, що останнього разу, коли ви виконали git status, ви побачили, що всі зміни індексовані, отже ви готові зберегти ваші зміни. Найпростіший спосіб створити коміт — набрати git commit:

$ git commit

Це запустить ваш редактор. (Це редактор, який встановлено в змінній середовища EDITOR вашої оболонки — зазвичай vim або emacs, хоча ви можете налаштувати його як завгодно за допомогою команди git config --global core.editor, яку ви бачили в Вступ).

Редактор покаже вам наступний текст (це приклад екрану Vim):

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Your branch is up-to-date with 'origin/master'.
#
# Changes to be committed:
#	new file:   README
#	modified:   CONTRIBUTING.md
#
~
~
~
".git/COMMIT_EDITMSG" 9L, 283C

Ви бачите, згенероване повідомлення коміту містить закоментований останній вивід команди git status та один пустий рядок нагорі. Ви можете видалити ці коментарі на написати своє повідомлення коміту, або можете залишити їх, щоб вони допомогли вам пригадати що ви зберігаєте. (Для навіть більш докладного нагадування про ваші зміни, ви можете передати опцію -v до git commit. Це призведе то того, що у вашому редакторі також будуть відображені всі зміни, що ввійдуть до коміту.) Коли ви виходите з редактору, Git створює коміт з цим повідомленням коміту (після видалення коментарів та змін до файлів).

Іншим чином, ви можете набрати повідомлення коміту прямо в команді commit, якщо напишете її після опції -m, ось так:

$ git commit -m "Story 182: Fix benchmarks for speed"
[master 463dc4f] Story 182: Fix benchmarks for speed
 2 files changed, 2 insertions(+)
 create mode 100644 README

Тепер ви створили свій перший коміт! Ви можете бачити, що команда commit розповіла вам дещо про коміт: до якої гілки ви зберегли зміни (master), який SHA-1 хеш отримав коміт (463dc4f), скільки файлів було змінено, та статистику щодо індексованих та видалених рядків у коміті.

Пам’ятайте, що коміт записує знімок, який ви створили в індексі. Усе, що ви не проіндексували, залишиться зміненим. Ви можете зробити інший коміт, щоб додати ці зміни до історії. Щоразу ви створюєте коміт, ви записуєте знімок вашого проекту, до якого ви можете повернутися або порівняти щось пізніше.

Обходимо індекс

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

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   CONTRIBUTING.md

no changes added to commit (use "git add" and/or "git commit -a")
$ git commit -a -m 'added new benchmarks'
[master 83e38c7] added new benchmarks
 1 file changed, 5 insertions(+), 0 deletions(-)

Зауважте, що вам не довелося виконувати git add до файлу CONTRIBUTING.md у цьому випадку до того, як ви створили коміт. Це тому що опція -a включає всі змінені файли. Це зручно, проте будьте обережні; інколи ця опція є причиною додавання небажаних змін.

Видаляємо файли

Щоб видалити файл з Git, вам треба прибрати його з контрольованих файлів (вірніше, видалити його з вашого індексу) та створити коміт. Команда git rm це робить, а також видаляє файл з вашої робочої директорії, щоб наступного разу він не відображався неконтрольованим.

Якщо ви просто видалите файл з вашої робочої директорії, він з’явиться під заголовком “Changes not staged for commit” (тобто, неіндексованим) виводу команди git status:

$ rm PROJECTS.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        deleted:    PROJECTS.md

no changes added to commit (use "git add" and/or "git commit -a")

Потім, якщо ви виконаєте git rm, файл буде індексованим на видалення:

$ git rm PROJECTS.md
rm 'PROJECTS.md'
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    deleted:    PROJECTS.md

Наступного разу, коли ви створите коміт, файл зникне та більше не буде контрольованим. Якщо ви редагували файл та вже додали його до індексу, ви маєте примусово видалити його за допомогою опції -f. Це захід безпеки, щоб завадити випадковому видаленню даних, які ви не записали до знімку, і тому вони не можуть бути відновлені Git.

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

$ git rm --cached README

Ви можете передавати команді git rm файли, директорії або файлові ґлоб шаблони. Це означає, що ви можете зробити щось таке:

$ git rm log/\*.log

Зверніть увагу на зворотну похилу (\) попереду *. Вона необхідна адже Git має власне розкриття шаблону на додаток до розкриття шаблону вашої оболонки. Ця команда видаляє всі файли, що мають .log розширення та знаходяться в директорії log/. Або, ви можете зробити щось таке:

$ git rm \*~

Ця команда видаляє всі файли, чиї назви закінчуються на ~.

Пересування файлів

На відміну від багатьох інших СКВ, Git явно не стежить за пересуванням файлів. Якщо ви перейменуєте файл у Git, ніяких метаданих про це не буде збережено. Втім, Git доволі кмітливий, та може сам зрозуміти, що перейменування відбулося вже після нього — ми повернемося до виявлення пересування файлів трохи пізніше.

Тому присутність команди mv у Git трохи спантеличує. Якщо ви бажаєте перейменувати файл у Git, ви можете виконати щось таке:

$ git mv стара_назва нова_назва

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

$ git mv README.md README
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    renamed:    README.md -> README

Втім, це рівнозначно виконанню таких команд:

$ mv README.md README
$ git rm README.md
$ git add README

Git без підказок розуміє, що це перейменування файлу, тому неважливо, чи ви перейменували файл так, або за допомогою команди mv. Єдина дійсна різниця в тому, що git mv це одна команда замість трьох — ця команда існує тільки для зручності. Більш важливо, що ви можете використати будь-яку утиліту для перейменування файлу та зробити add/rm пізніше, до збереження коміту.