-
1. Начало
- 1.1 За Version Control системите
- 1.2 Кратка история на Git
- 1.3 Какво е Git
- 1.4 Конзолата на Git
- 1.5 Инсталиране на Git
- 1.6 Първоначална настройка на Git
- 1.7 Помощна информация в Git
- 1.8 Обобщение
-
2. Основи на Git
-
3. Клонове в Git
-
4. GitHub
-
5. Git инструменти
- 5.1 Избор на къмити
- 5.2 Интерактивно индексиране
- 5.3 Stashing и Cleaning
- 5.4 Подписване на вашата работа
- 5.5 Търсене
- 5.6 Манипулация на историята
- 5.7 Мистерията на командата Reset
- 5.8 Сливане за напреднали
- 5.9 Rerere
- 5.10 Дебъгване с Git
- 5.11 Подмодули
- 5.12 Пакети в Git (Bundling)
- 5.13 Заместване
- 5.14 Credential Storage система
- 5.15 Обобщение
-
6. Настройване на Git
- 6.1 Git конфигурации
- 6.2 Git атрибути
- 6.3 Git Hooks
- 6.4 Примерна Git-Enforced политика
- 6.5 Обобщение
-
7. Git и други системи
- 7.1 Git като клиент
- 7.2 Миграция към Git
- 7.3 Обобщение
-
8. Git на ниско ниво
- 8.1 Plumbing и Porcelain команди
- 8.2 Git обекти
- 8.3 Git референции
- 8.4 Packfiles
- 8.5 Refspec спецификации
- 8.6 Транспортни протоколи
- 8.7 Поддръжка и възстановяване на данни
- 8.8 Environment променливи
- 8.9 Обобщение
-
9. Приложение A: Git в други среди
-
10. Приложение B: Вграждане на Git в приложения
- 10.1 Git от команден ред
- 10.2 Libgit2
- 10.3 JGit
- 10.4 go-git
- 10.5 Dulwich
-
A1. Приложение C: Git команди
- A1.1 Настройки и конфигурация
- A1.2 Издърпване и създаване на проекти
- A1.3 Snapshotting
- A1.4 Клонове и сливане
- A1.5 Споделяне и обновяване на проекти
- A1.6 Инспекция и сравнение
- A1.7 Дебъгване
- A1.8 Patching
- A1.9 Email команди
- A1.10 Външни системи
- A1.11 Административни команди
- A1.12 Plumbing команди
5.7 Git инструменти - Мистерията на командата Reset
Мистерията на командата Reset
Преди да преминем към по-специализираните инструменти на Git, нека поговорим още малко за reset
и checkout
командите.
Тези команди са два от най-смущаващите аспекти в Git, когато за първи път се сблъскате с тях.
Правят толкова много неща, че изглежда безнадеждно да бъдат разбрани и използвани ефективно.
Ето защо, препоръчваме една проста метафора.
Трите дървета
Един по-лесен подход да мислите за reset
и checkout
е да гледате на Git като мениджър на съдържание за три различни дървета.
Като казваме “дърво”, в действителност разбираме “колекция от файлове”, а не структурата от данни.
Има няколко ситуации, където индексът на практика не работи като дърво, но за нашите цели е по-лесно да го възприемаме като такова.
Git като система управлява три дървета в нормалната си работа:
Дърво | Роля |
---|---|
HEAD |
Snapshot на последния къмит, родител на следващия |
Index |
Snapshot за следващия къмит |
Работна директория |
Работна област |
Дървото HEAD
HEAD е указателят към референцията на текущия клон, която от своя страна сочи към последния къмит направен в този клон. Това означава, че HEAD ще бъде родител на следващия създаден къмит. Най-лесно е да гледаме на HEAD като на snapshot на последния ни къмит в този клон.
В действителност, лесно е да видим как изглежда този snapshot. Ето пример за извличане на реалния листинг на директория и SHA-1 чексумите за всеки файл в HEAD snapshot-а:
$ git cat-file -p HEAD
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
author Scott Chacon 1301511835 -0700
committer Scott Chacon 1301511835 -0700
initial commit
$ git ls-tree -r HEAD
100644 blob a906cb2a4a904a152... README
100644 blob 8f94139338f9404f2... Rakefile
040000 tree 99f1a6d12cb4b6f19... lib
Командите на Git cat-file
и ls-tree
са “plumbing” команди използвани за неща на по-ниско ниво и рядко се използват в ежедневната работа, но ни помагат да видим какво се случва тук.
Индексът
Индексът е очаквания следващ къмит.
Наричаме го още “Staging Area” понеже това е мястото, от което Git взема данни, когато изпълните git commit
.
Git попълва индекса със списък от съдържанието на всички файлове, които последно са били извлечени в работната директория и как са изглеждали те когато първоначално са били извлечени.
Вие след това замествате част от файловете с техни актуализирани версии и git commit
конвертира това в дървото за нов къмит.
$ git ls-files -s
100644 a906cb2a4a904a152e80877d4088654daad0c859 0 README
100644 8f94139338f9404f26296befa88755fc2598c289 0 Rakefile
100644 47c6340d6459e05787f644c2447d2595f5d3a54b 0 lib/simplegit.rb
Отново, тук използваме git ls-files
, която е задкулисна команда, показаща ви как изглежда текущия ви индекс.
Технически, индексът не е дървовидна структура — реално той е имплементиран като плосък манифест — но за нашите цели можем да кажем, че прилича на дърво.
Работната директория
Накрая идва третото Git дърво, работната ви директория, известно още като “working tree”.
Другите две съхраняват съдържанието си в ефективен, но неудобен за разглеждане вид, в директорията .git
.
Работната директория, от своя страна, разпакетира съдържанието в действителните файлове, с които работим.
Можем да гледаме на нея като на опитно поле, в което пробваме промените си преди да ги изпратим в индексната област и след това в историята на проекта.
$ tree
.
├── README
├── Rakefile
└── lib
└── simplegit.rb
1 directory, 3 files
Работният процес
Основната работна последователност на Git е да записва snapshot-и на проекта ни в последователни серии, манипулирайки тези три дървета.
![reset workflow](png/reset-workflow.png)
Нека онагледим процеса: да кажем, че отиваме в нова директория с един файл в нея.
Ще наречем това v1 на файла и ще го маркираме в синьо.
Сега изпълняваме git init
, което ще създаде ново Git хранилище с HEAD референция, която сочи към все още несъществуващ клон (master
все още не е създаден).
![reset ex1](png/reset-ex1.png)
На този етап, единствено работната ни директория има някакво съдържание.
Сега ще искаме да индексираме този файл, така че използваме git add
за да вземем съдържанието от работната област и да го копираме в индексната.
![reset ex2](png/reset-ex2.png)
След това, изпълняваме git commit
, което ще вземе съдържанието на индекса и ще го запише като перманентен snapshot, ще създаде къмит обект, който сочи към този snapshot и ще настрои нашия master
клон да сочи към този къмит.
![reset ex3](png/reset-ex3.png)
Ако сега изпълним git status
няма да видим промени, защото трите ни дървета са идентични.
Сега правим промяна по файла и го къмитваме. Ще минем през същия процес, първо променяме файла в работната директория. Нека наречем това v2 на файла и да го маркираме в червено.
![reset ex4](png/reset-ex4.png)
Когато изпълним git status
в този момент, ще видим този файл в червено в секцията “Changes not staged for commit”, защото той сега се различава от копието си в индекса.
След това изпълняваме git add
и го индексираме.
![reset ex5](png/reset-ex5.png)
В момента, git status
, ще ни покаже файла в зелен цвят в секцията “Changes to be committed” защото индексът и HEAD се различават — тоест нашият очаквам следващ къмит е различен от последно съхранения.
Последно, изпълняваме git commit
за да финализираме новия къмит.
![reset ex6](png/reset-ex6.png)
Сега git status
няма да покаже разлики, защото трите дървета отново са еднакви.
Клонирането и превключването на клонове минават през подобен процес. Когато превключим към друг клон, HEAD се променя и сочи към референцията на този клон, индексът се попълва със snapshot-а на този къмит и след това съдържанието на индекса се копира в работната директория.
Ролята на Reset
Командата`reset` придобива по-ясно значение, когато се разглежда в такъв контекст.
За целта на тези примери, нека кажем, че сме променили file.txt
отново и сме го къмитнали за трети път.
Така историята ни сега ще изглежда по този начин:
![reset start](png/reset-start.png)
Нека минем през това какво точно прави reset
, когато я изпълним.
Тя директно манипулира тези три дървета по прост и предвидим начин.
Извършват се три основни операции.
Стъпка 1: Преместване на HEAD
Първото нещо, което reset
прави е да смени мястото, където HEAD сочи.
Това не означава, че самия HEAD се променя (което става с checkout
), reset
премества клона, към който сочи HEAD.
Което ще рече, че ако HEAD е на master
клона (тоест в момента сте в този клон), изпълнението на git reset 9e5e6a4
ще започне като направи master
да сочи към 9e5e6a4
.
![reset soft](png/reset-soft.png)
Без значение каква форма на reset
с къмит сте изпълнили, това е първото нещо, което командата винаги ще опита да направи.
С reset --soft
, тя просто ще завърши тук.
Сега погледнете пак последната диаграма и ще видите какво се е случило: командата практически е отменила последно изпълнената git commit
команда.
Когато изпълните git commit
, Git създава нов къмит и премества клона, към който сочи HEAD към този къмит.
Когато ресетнете обратно към HEAD~
(тоест родителя на HEAD), вие премествате клона обратно където е бил без да променяте индекса или работната директория.
Сега можете да обновите индекса и да изпълните git commit
отново, така че да постигнете резултата, който бихте имали с git commit --amend
(вижте Промяна на последния къмит).
Стъпка 2: Обновяване на индекса (--mixed)
Ако сега пуснете git status
, ще видите в зелено разликата между индекса и новия HEAD.
Следващото нещо, което reset
ще направи е да обнови индекса със съдържанието на snapshot-а, към който вече сочи HEAD.
![reset mixed](png/reset-mixed.png)
Ако подадете аргумента --mixed
, reset
ще спре процеса в тази точка.
Този аргумент се подразбира, така че ако не подадете никакви аргументи, а просто изпълните git reset HEAD~
, това е точката в която командата ще спре процеса.
Поглеждайки отново диаграмата, осъзнаваме че командата пак е отменила последната commit
, но в допълнение е деиндексирала всичко.
По същество сега се върнахте обратно до момента преди изпълнението на командите git add
и git commit
.
Стъпка 3: Обновяване на работната директория (--hard)
Третото нещо, което командата reset
може да стори, е да обнови съдържанието на работната директория така, че да я направи като индексната.
Ако подадете параметъра --hard
тя ще стигне чак до там.
![reset hard](png/reset-hard.png)
Нека да помислим какво се случи току що.
Вие отменихте последния къмит (командите git add
и git commit
) и също така цялата работа, която сте свършили в работната си област.
Важно е да подчертаем, че параметърът --hard
е единственият, който може да направи командата reset
наистина опасна и е едно от нещата, което могат да причинят загуба на данни в Git.
Всяко друго reset
изпълнение може лесно да се отмени, но опцията --hard
не може, тя безвъзвратно презаписва файловете в работната директория.
В този примерен случай, ние все още имаме v3 версията на файла в къмит в нашата база данни на Git и бихме могли да го извлечем поглеждайки в reflog
-а ни, но ако не бяхме го къмитнали преди, Git щеше да го презапише без връщане назад.
Обобщение
Командата reset
презаписва съдържанието на трите дървета в специфичен ред, спирайки там, където сме ѝ указали:
-
Премества клона, към който сочи HEAD (спира дотук с параметъра
--soft
). -
Модифицира индекса да изглежда като HEAD (спира дотук, ако не е подаден параметър
--hard
). -
Модифицира работната директория да изглежда като индексната област.
Reset с път
Дотук разгледахме reset
в основната ѝ форма, но можете също така да ѝ посочите път, по който да работи.
Ако укажете път, reset
ще пропусне стъпка 1 и ще ограничи действието си до специфичен файл/файлове.
В това всъщност има смисъл — HEAD е просто указател и вие не можете да сочите част от един къмит и част от друг.
Но индексът и работната директория могат да бъдат частично обновени, така че reset преминава към стъпки 2 и 3.
Да допуснем, че сте изпълнили git reset file.txt
.
Тази форма (понеже не сте указали SHA-1 на къмит или клон, както и параметрите --soft
или --hard
) е съкратена версия на командата git reset --mixed HEAD file.txt
, която:
-
Ще премести клона, към който сочи HEAD (пропуска се).
-
Ще направи индекса да изглежда като HEAD (спира тук).
Така практически командата просто копира file.txt
от HEAD в индекса.
![reset path1](png/reset-path1.png)
Ефективно това деиндексира файла.
Ако погледнем диаграмата за тази команда и помислим какво прави git add
, ще установим че те работят точно по обратен начин.
![reset path2](png/reset-path2.png)
Това е причината, поради която изходът на git status
ви съветва да направите това за да деиндексирате файл (вижте Изваждане на файл от индекса за подробности).
Можем също толкова лесно да кажем на Git да “не изтегля данните от HEAD” указвайки специфичен къмит, от който да извлечем файла вместо това.
В такива случаи изпълняваме нещо като git reset eb43bf file.txt
.
![reset path3](png/reset-path3.png)
Това ще направи същото нещо, както ако бяхме върнали назад съдържанието на файла до версията v1 в работната директория, бяхме изпълнили git add
върху него и след това го бяхме възстановили обратно отново във версия v3 (без действително да минаваме през всички тези стъпки).
Ако сега изпълним git commit
, тя ще запише промяна, която връща файла до версия v1, въпреки че тази версия никога не сме я имали отново в работната директория.
Интересно е да се отбележи и, че подобно на git add
, reset
също приема --patch
аргумент за да деиндексира съдържание в hunk-by-hunk стил.
Така можете селективно да деиндексирате или връщате съдържание.
Обединяване
Нека видим как да направим нещо интересно с тази нова функционалност — да обединяваме къмити (squashing).
Да кажем, че имате серия къмити със съобщения като “oops.”, “WIP” и “forgot this file”.
Можете да използвате reset
за да ги обедините на бърза ръка в един общ къмит, което ще ви спечели уважение в очите на колегите.
Обединяване на къмити показва друг начин да направите това, но в този пример е по-лесно да използваме reset
.
Да приемем, че имате проект, в който първият къмит има един файл, вторият добавя нов файл и модифицира първия, а третият къмит модифицира първия файл още един път. Вторият къмит е бил work in progress и искате да го обедините с някой друг.
![reset squash r1](png/reset-squash-r1.png)
Може да изпълните git reset --soft HEAD~2
за да преместите HEAD клона назад към по-стар къмит (най-скорошния, който искате да запазите):
![reset squash r2](png/reset-squash-r2.png)
След това просто изпълнете git commit
отново:
![reset squash r3](png/reset-squash-r3.png)
Сега може да видите, че достъпната ви история, тази която ще публикувате, вече съдържа един къмит с file-a.txt
v1 и след това втори, който е модифицирал file-a.txt
до версия v3 и е добавил file-b.txt
.
Къмитът с версия v2 на файла вече е извън историята.
Check It Out
Накрая, може да се запитате каква е разликата между checkout
и reset
.
Подобно на reset
, checkout
манипулира трите дървета и може да е различна в зависимост от това дали ѝ подавате път или не.
Без пътища
Изпълнението на git checkout [branch]
е доста подобно по резултат от това на git reset --hard [branch]
по отношение на това, че обновява всички три дървета, така че да изглеждат като [branch]
, но с две основни разлики.
Първо, за разлика от reset --hard
, checkout
работи безопасно за работната ви директория, тя първо ще се увери, че в нея няма промени преди да превключи към другия клон.
Всъщност, нещата са дори още по-интелигентни — командата опитва да направи тривиално сливане в работната директория, така че всички файлове, които не сте променили ще бъдат обновени.
reset --hard
, от друга страна, просто ще презапише всичко без никаква проверка.
Другата важна разлика е в това как checkout
обновява HEAD.
Докато reset
ще премести клона, към който сочи HEAD, checkout
ще премести самия HEAD да сочи към друг клон.
Нека имаме клонове master
и develop
, които сочат към различни къмити и се намираме в develop
(HEAD сочи там).
Ако изпълним git reset master
, develop
сега ще сочи към същия къмит, към който сочи master
.
Ако вместо това изпълним git checkout master
, develop
не се променя, мести се само HEAD.
HEAD сега ще сочи към master
.
Така и в двата случая променяме HEAD да сочи към commit A, но начинът, по който го правим е много различен.
reset
ще премести клона, към който сочи HEAD, checkout
ще премести самия HEAD.
![reset checkout](png/reset-checkout.png)
С пътища
Друг начин да изпълним checkout
е с път към файл, което както и reset
, не премества HEAD.
То е точно като git reset [branch] file
по смисъла на това, че обновява индекса с този файл в този къмит, но в допълнение на това презаписва и файла в работната област.
Резултатът ще е подобен на git reset --hard [branch] file
(ако reset
ви позволи изпълнението) — не е безопасен за работната директория и не премества HEAD.
В допълнение на това, както git reset
и git add
, checkout
също приема аргумента --patch
, за да ви позволи селективно извличане на част от файл в hunk-by-hunk маниер.
Обобщение
Надяваме се, че сега се чувствате по-удобно с командата reset
, но въпреки това знаем, че тя предизвиква конфуз, когато я сравнявате с checkout
и е твърде възможно да забравите всички правила и различни начини на изпълнението ѝ.
Ето кратка таблица с това коя команда кое от дърветата променя. Колоната “HEAD” съдържа “REF”, ако командата отляво премества референцията (клона), към който сочи HEAD и съдържа “HEAD” ако командата премества самия HEAD. Обърнете специално внимание на 'WD Safe?' (безопасна за работната директория) колоната — ако тя съдържа NO, помислете добре преди да я изпълните.
HEAD | Index | Workdir | WD Safe? | |
---|---|---|---|---|
Commit Level |
||||
|
REF |
NO |
NO |
YES |
|
REF |
YES |
NO |
YES |
|
REF |
YES |
YES |
NO |
|
HEAD |
YES |
YES |
YES |
File Level |
||||
|
NO |
YES |
NO |
YES |
|
NO |
YES |
YES |
NO |