Git
Chapters ▾ 2nd Edition

2.2 Основи на Git - Запис на промени в хранилището

Запис на промени в хранилището

Вече имате чисто ново Git хранилище и работещо копие на файловете от проекта. Сега трябва да започнете да правите промените, които желаете и да записвате snapshot-и на промените в хранилището всеки път, когато проектът ви достигне състояние, което бихте желали да запишете на сигурно място.

Помнете, че всеки файл от работната ви директория може да бъде в два статуса - проследяван или не (tracked/untracked). Tracked файловете са тези от последния snapshot, те може да са непроменени, променени и индексирани (staged). Накратко, tracked файловете са тези, които Git познава.

Untracked файловете са всичко останало - всички файлове в работната ви директория, които не са били в последния ви snapshot и не са в staging областта. Когато за пръв път клонирате хранилище, всички ваши файлове ще бъдат tracked и същевременно - unmodified, защото Git току що ги е извлякъл и вие все още не сте променяли нищо по тях.

Когато започнете да променяте файловете, Git ги вижда вече като modified, понеже сте ги редактирали след последния къмит. Вие индексирате тези променени файлове, след това къмитвате промените им и този цикъл се повтаря в течение на работата ви.

Промяната в статуса на вашите файлове.
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 също така не вижда никакви непроследени файлове, иначе щеше да ги покаже тук. Накрая, командата ви казва в кой клон (branch) на проекта се намирате и че не се отклонявате от същия клон на сървъра. Засега, този клон е винаги “master”, както е по подразбиране, към момента това не ви интересува. Клонове в Git ще разгледа клоновете и референциите в подробности.

Нека сега добавим нов файл в проекта, прост README файл. Ако файлът не е съществувал преди и изпълните git status, ще видите untracked файла си така:

$ 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 вижда файл, който не е присъствал в предишния snapshot (commit) и Git няма сам да започне да го прибавя към следващите commits докато вие не укажете това изрично. Това е умишлено поведение и ви предпазва от ситуации, в които бихте могли автоматично да добавяте файлове, които не желаете, например генерирани binary файлове. Вие обаче искате да включите README файла, така че нека го направим.

Проследяване на нови файлове

За да започнете да следите нов файл, използвайте командата git add. За вашия README файл, изпълнете това:

$ git add README

Ако след това изпълните отново статус командата, ще видите че 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”. Ако къмитнете в този момент, файлът който ще попадне в следващия snapshot-а ще e в състоянието, в което е бил, когато сте изпълнили git add командата за него. Може да си спомните, че когато по-рано изпълнихте git init, след това изпълнихте и git add <files> — това беше за да започнете да следите файлове във вашата директория. Командата 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 е многоцелева команда — използвате я както за да започнете да следите файлове, така и за да ги индексирате в staging областта и дори да правите по-различни неща, като например да маркирате отбелязани като конфликтни по време на сливане файлове като коректни такива. Би могло да ви е от полза да приемате значението ѝ повече като “добави това съдържание в следващия къмит” вместо като “добави този файл към проекта”. Нека сега изпълним 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 се показва едновременно като staged и unstaged. Как е възможно това? Оказва се, че Git индексира файла точно както е бил, когато сте изпълнили git add. Ако къмитнете сега, версията на CONTRIBUTING.md, която ще отиде в snapshot-а ще е тази, след която сте изпълнили 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 и т.н. Изходът е в две колони — лявата показва статуса на staging областта, а дясната статуса на работната директория. Така в горния пример, README файлът е променен в работната област, но не е индексиран, докато файлът lib/simplegit.rb е променен и индексиран. Файлът Rakefile е променен, индексиран и след това променен отново, така че по него има промени които са индексирани и такива, които не са.

Игнориране на файлове

Често, ще имате класове от файлове, за които няма да искате Git да ги добавя автоматично и дори да ви ги показва като непроследени. Това обикновено са автоматично генерирани файлове - лог-файлове или такива създадени от компилиращата ви система. В подобни случаи, можете да създадете файл с име .gitignore, в който да ги опишете с подходяща маска за имената им. Ето един примерен такъв файл:

$ cat .gitignore
*.[oa]
*~

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

Правилата за маските, които можете да включвате в .gitignore файла са както следва:

  • Празните редове и редовете започащи с # се игнорират.

  • Работят стандартните глобални правила за маски и те ще бъдат приложени рекурсивно по цялото работно дърво.

  • Можете да започвате маските с (/) за да избегнете рекурсия.

  • Можете да завършвате маските с (/) за да указвате директория.

  • Можете да обърнете логиката на маската като я започнете с (!).

Глобалните правила са подобни на опростени регулярни изрази, които шеловете използват. Звездичката (*) търси за нула или повече символа; [abc] търси за кой да е символ в скобите (в този случай a, b, или c); въпросителният знак (?) търси единичен символ; символи в скоби с тире между тях ([0-9]) търсят за произволен символ в обхвата между символите (в този случай от 0 до 9). Можете да използвате две звездички за да търсите в под-директории; a/**/z ще открие a/z, a/b/z, a/b/c/z, и т.н.

Ето друг примерен .gitignore файл:

# без .a файлове
*.a

# но lib.a се включва, въпреки че игнорирате всички .а файлове отгоре
!lib.a

# игнорирай само TODO файла в текущата директория, не и под-директориите съдържащи 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

В общия случай, едно хранилище би могло да има единичен .gitignore файл в най-горната директория, който се прилага върху всички други. Обаче, възможно е да имате и допълнителни .gitignore файлове в поддиректориите. Правилата в тези вложени .gitignore файлове ще се прилагат само към файловете, намиращи се в директорията, в която се пазят. (Linux kernel хранилището например има 206 .gitignore файла.)

Извън темата на тази книга е да се впускаме в детайли за множеството .gitignore файлове; погледнете man gitignore, ако желаете повече информация.

Преглед на индексираните и неиндексирани промени

Ако командата 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 difftool --tool-help за да видите какви diff инструменти са налични за вашата система. Ние ще продължим да ползваме git diff командата по различни начини в книгата. Има и друг начин за преглед на промените, ако предпочитате графичен или друг способ. Ако изпълните git difftool вместо git diff, можете да гледате всяко от сравненията в софтуери като emerge, vimdiff и много други подобни, вкл. комерсиални такива.

Публикуване на промените (commit)

Сега, след като индексната област е в състоянието, което искате, можете да публикувате (къмитнете) вашите промени. Помнете, че всичко, което все още не е индексирано — всякакви файлове, които сте създали или редактирали след последната git add команда — няма да отидат в това публикуване. Те ще останат като променени файлове на вашия диск. Нека кажем, че последния път когато сте пуснали git status, вие сте видели, че всичко е индексирано и сте готови да къмитнете промените. Най-простият начин да запишете е изпълнявайки командата git commit:

$ git commit

Правейки това, Git ще стартира вашия текстов редактор. (Това се определя от EDITOR environment променливата на вашия шел -– обикновено 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 в коментиран вид и един празен ред над него. Можете да изтриете тези коментари и да напишете собствено съобщение или да ги оставите там за да ви припомнят по-късно какво точно сте публикували. (Ако искате още по-подробно напомняне за това какво сте модифицирали, можете да изпълните командата с параметър git commit -v. Това ще включи в съобщението и самите промени, така че да можете точно да проследите какво сте къмитнали.) Когато излезете от редактора запазвайки промените, Git ще публикува промените заедно със съобщението (коментарите и diff информацията се премахват)

Вместо да пускате текстовия редактор, можете да подадете къмит съобщението директно като параметър на командата с флага -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

Сега вече къмитнахте за пръв път промените си! Можете да видите, че това действие ви дава и допълнителна информация за себе си: към кой клон сте къмитнали (master), каква е SHA-1 чексумата на къмита (463dc4f), колко на брой файлове са променени и статистика за добавените и премахнати редове код.

Помнете, че къмитът съдържа моментна снимка на това, което е имало в индексната област (staging area). Всичко, което не е било там няма да присъства в къмита и файловете ще си стоят като променени. За да ги добавите - трябва да направите следващ къмит. Всеки път, когато къмитвате промени, вие правите snapshot на състоянието на вашия проект и по-късно можете да го възстановите или да го ползвате за сравнение.

Прескачане на Staging областта

Въпреки, че може да е много полезна за фина настройка на вашите промени, понякога индексната област може да се прескочи в процеса на работа. Ако искате директно да къмитнете променени файлове без да ги добавяте в нея, 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” (тоест, unstaged) секцията от изхода на 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 \*~

Тази команда премахва всички файлове, имената на които завършват със символа ~.

Преименуване на файлове

За разлика от много други VCS системи, Git не следи експлицитно преименуването на файлове. Ако преименувате файл в Git, никакви мета данни няма да се съхранят в Git базата, така че да му указва, че сте преименували файла. Обаче, Git е достатъчно интелигентен за да усети това — ще се занимаем с детекцията на преименуваните файлове малко по-късно.

Затова е малко смущаващо, че Git всъщност има mv команда. Ако искате да преименувате файл в Git, можете да изпълните това:

$ git mv file_from file_to

и то си работи. На практика, ако изпълните командата и погледнете в статуса, ще видите че 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 действията по-късно, преди да къмитнете промените.