Как да премахна ангажиментите от хронологията на git, но иначе да поддържам графиката абсолютно същата, включително сливания?

Какво имам:

---A----B-----C-----D--------*-----E-------> (master)
                     \      /
                      1----2 (foo)

От какво имам нужда:

---A---------------D--------*-----E-------> (master)
                    \      /
                     1----2 (foo)

Преди известно време направих два ангажимента, които бих искал да премахна от моето git repo. Опитах множество различни „уроци“ за пребазиране и всички те завършваха със странни git истории, така че създадох пример за репо и резултатът не е това, което очаквах. Може ли някой да ми помогне да разбера какво пропускам?

Имам два клона, master и foo. Направих ангажимент B с един файл, който бих искал да премахна, и ангажимент C, където модифицирах този файл. Заедно с другите ангажименти, никога повече не докоснах този файл.

Идентификатори на ангажименти:

A: f0e0796
B: 5ccb371
C: a46df1c
D: 8eb025b
E: b763a46
1: f5b0116
2: 175e01f

Затова използвам rebase -i f0e0796 и премахвам B 5ccb371 и C a46df1c, нали? Ако интерпретирам резултата правилно, това е, което gitk ми показва за моето репо, въпреки че git branches все още изброява втория клон.

---A-----1----2---E------> (master)

Може ли някой да ми каже какво се случи тук?

Редактиране: Ето как да пресъздадете репото от първата графика:

git init foo
cd foo

touch A
git add A
git commit -m "add A"

touch B
git add B
git commit -m "add B"

echo "modify" > B
git add B
git commit -m "modify B"

touch C
git add C
git commit -m "add C"

git checkout -b foo

touch 1
git add 1
git commit -m "add 1"

touch 2
git add 2
git commit -m "add 2"

git switch master
git merge foo --no-ff

touch E
git add E
git commit -m "add E"

person Daniel Stephens    schedule 11.04.2020    source източник
comment
Съжалявам, ако разбирам правилно. Опитвате ли се да отмените два ангажимента от вашия master и да запазите състоянието същото в D, както всичко се случва в D от B & C? Или искате да отмените промените за постоянно, така че D не трябва да има промени, направени в B & C?   -  person VPaul    schedule 11.04.2020
comment
По-късното предположение е правилно. D не трябва да съдържа промените от B и C. Знам, че bfg може да помогне тук, но не искам да заличавам файла напълно от хронологията, тъй като файлът съществуваше законно преди няколко години, но беше изтрит. Просто беше случайно повторновъведено в тези два ангажимента   -  person Daniel Stephens    schedule 11.04.2020
comment
Предполагам, че публикуваният отговор трябва да помогне за разрешаването на проблема, тъй като няма нужда да премахвате историята от git за ангажиментите. Rebase не се използва при тези обстоятелства. Използвал съм го, за да доведа своя клон до същото ниво на майстор. Понякога сливането решава проблема ми и никога не ми се налага да използвам rebase.   -  person VPaul    schedule 11.04.2020
comment
Благодаря за последващите действия!   -  person Daniel Stephens    schedule 11.04.2020
comment
@VPaul Rebase със сигурност се използва при тези обстоятелства! Пребазирането, независимо дали го правите чрез git rebase или ръчно (отговорът ми показва и двете), е начинът да пренапишете, а не да промените историята .   -  person Inigo    schedule 24.04.2020


Отговори (5)


Докато това, което предлагам, ще ви даде чиста, линейна история; това е, което по същество трябва да прави rebase. Въпреки това се надявам, че това ви дава начин да премахнете B и B' от историята на ангажиментите. Ето и обяснението:

Repo recreation output:
---A----B-----B'-----C--------D-------> (master)
                      \      /
                       1----2 (foo)

git log --graph --all --oneline --decorate #initial view the git commit graph
* dfa0f63 (HEAD -> master) add E
*   843612e Merge branch 'foo'
|\  
| * 3fd261f (foo) add 2
| * ed338bb add 1
|/  
* bf79650 add C
* ff94039 modify B
* 583110a add B
* cd8f6cd add A

git rebase -i HEAD~5 #here you drop 583110a/add B and ff94039/modify B from
foo branch.

git log --graph --all --oneline --decorate
$ git rebase -i HEAD~5
* 701d9e7 (HEAD -> master) add E
* 5a4be4f add 2
* 75b43d5 add 1
* 151742d add C
| * 3fd261f (foo) add 2
| * ed338bb add 1
| * bf79650 add C
| * ff94039 modify B
| * 583110a add B
|/  
* cd8f6cd add A

$ git rebase -i master foo #drop 583110a/add B and ff94039/modify B again

$ git log --graph --all --oneline --decorate #view the git commit graph

* 701d9e7 (HEAD -> foo, master) add E
* 5a4be4f add 2
* 75b43d5 add 1
* 151742d add C
* cd8f6cd add A

И накрая, финалът може да не е в реда, който сте очаквали A--C--1---2---E. Можете обаче да пренаредите отново реда в интерактивния режим. Опитайте git rebase -i HEAD~n.

Забележка: Най-добре е да избягвате промяна на хронологията на ангажиране/публикуване. Аз съм начинаещ и проучвам git, надявам се горното решение да остане. Въпреки това съм сигурен, че има тонове други по-лесни решения, достъпни онлайн. Намерих тази статия за доста полезна, за бъдещи справки за всички.

person Smeet Thakkar    schedule 12.04.2020
comment
@DanielStephens Този отговор прави всичко, което поискате освен запазването на клона foo като отделен клон. Докато в този случай това няма значение, тъй като master и foo са идентични както преди, така и след като сте направили повторното базиране, няма да работи, ако не бяха, напр. имахте ангажименти между D и E на главния клон. Вижте stackoverflow.com/a/61411955/8910547 за това как да използвате git rebase, за да направите exactl y това, което искате. - person Inigo; 24.04.2020

git rebase по подразбиране пребазира само към една линия на хронологията на ангажиментите, защото това е по-често това, което хората искат. Ако не му кажете друго, то ще го направи за клона, който сте проверили (във вашия случай това беше master). Ето защо завършихте с пребазиран master клон с foo ангажименти, присадени, вместо обединени, и със самия foo непроменен и вече несвързан.

Ако имате git версия 2.18 или по-нова, можете да използвате опцията --rebase-merges*, за да кажете на git да пресъздаде хронологията на сливането, вместо да я линеаризира, както прави по подразбиране. Пребазираната хронология ще има същите разклонения и сливания обратно. По-долу ще ви преведа през стъпките за постигане на това, което искате, като използвате --rebase-merges.

Тези стъпки предполагат точното репо, което показвате във вашия въпрос.

  1. git checkout master
  2. git rebase -i --rebase-merges f0e0796
  3. in the interactive rebase todo file:
    • remove the two commits you wanted to drop (or comment them out, or change pick to drop or d)
    • на нов ред непосредствено след ред label foo добавете следното:
    exec git branch -f foo head
    
    (see below for explanation)
  4. запазете и затворете todo файла и voilà, git ще пребазира ангажиментите, като графиката изглежда точно както искате.


файлът todo е обяснен

git rebase просто автоматизира поредица от стъпки, които можете също така да правите ръчно. Тази последователност от стъпки е представена във файла todo. git rebase --interactive ви позволява да промените последователността, преди да се изпълни.

Ще го поясня с обяснение, включително как бихте го направили ръчно (добър опит за учене). Важно е да усетите това, ако правите много пребазирания в бъдеще, така че да имате добра ориентация, когато възникнат конфликти при сливане, или когато кажете на пребазирането да паузира в точки, за да можете да направите някои ръчни модификации.

label onto                  // labels "rebase onto" commit (f0e0796)
                            // this is what you would do in your head
                            // if doing this manually
# Branch foo
reset onto                  // git reset --hard <onto>
drop 5ccb371 add B          // skip this commit
drop a46df1c modify B       // skip this commit
pick 8eb025b add C          // git cherry-pick 8eb025b
label branch-point          // label this commit so we can reset back to it later
pick f5b0116 add 1          // git cherry-pick f5b0116
pick 175e01f add 2          // git cherry-pick 175e01f
label foo                   // label this commit so we can merge it later
                            //   This is just a rebase internal label. 
                            //   It does not affect the `foo` branch ref.
exec git branch -f foo head // point the `foo` branch ref to this commit 

reset branch-point # add C  // git reset --hard <branch-point>
merge -C b763a46 foo # Merge branch 'foo'  // git merge --no-ff foo
                                           // use comment from b763a46

exec git branch -f foo head обяснено

Както споменах по-горе, git rebase работи само на един клон. Това, което тази exec команда прави, е да промени реф foo, за да сочи към текущия head. Както можете да видите в последователността в todo файла, вие му казвате да направи това веднага след като е извършил последния комит на клона foo ("добавяне 2"), който е удобно означен като label foo във todo файла.

Ако вече не се нуждаете от foo ref (напр. това е клон на функция и това е последното му сливане), можете да пропуснете добавянето на този ред към файла със задачи.

Можете също така да пропуснете добавянето на този ред и отделно да насочите отново foo към ангажимента, който искате, след като повторното базиране е направено:

git branch -f foo <hash of the rebased commit that should be the new head of `foo`>

Уведомете ме, ако имате въпроси.


*Ако имате по-стара версия на git, можете да използвате вече остарялата опция --preserve-merges, въпреки че тя не е съвместима с интерактивния режим на rebase.

person Inigo    schedule 24.04.2020
comment
Добра точка. Гласуван за. Представих --rebase-merges в stackoverflow.com/a/50555740/6309. - person VonC; 24.04.2020
comment
@VonC благодаря и страхотно. --rebase-merges е много по-добър от --preserve-merges предвид това, което можете да правите с него интерактивно. Вижте моето обяснение на скрипта todo, който току-що добавих. Уведомете ме, ако обърках нещо, но току-що изпълних ръчните стъпки, както е показано, и работи по същия начин. - person Inigo; 24.04.2020

Затова използвам rebase -i f0e0796 и премахвам B 5ccb371 и C a46df1c, нали? Ако интерпретирам резултата правилно, това е, което gitk ми показва за моето репо, въпреки че git branches все още изброява втория клон.

...A---1---2---E    master

Може ли някой да ми каже какво се случи тук?

Това е, за което е създаден: да създава линейна история без сливане от един връх към една основа, запазвайки всички части, които все още може да имат нужда от обратно сливане към новата основа.

Документите за пребазиране може да са по-ясни за това: commits които са чисти избори (както е определено от git log --cherry-mark …) винаги се изпускат. се споменава само като отстрани в опция за това как да се третират празните ангажименти и по подразбиране повторното базиране просто ще премахне ангажиментите за сливане от списъка със задачи, и поставете повторно базираните ангажименти в един линеен клон. се споменава само по-нататък, в описанието на друга опция. Но това е целта му, за да автоматизира досадното идентифициране и елиминиране на вече приложени корекции и сливания на шум от иначе прост избор.


Дали git rebase е функцията, която търся за моя проблем?

Не точно. Опцията --rebase-merges се подобрява и отговорът на Inigo работи добре за вашия конкретен случай, но вижте предупрежденията в неговите документи: той има реални ограничения и предупреждения. Както посочва отговорът на Иниго, [т]ези стъпки предполагат точното репо, което показвате във вашия въпрос, и git rebase просто автоматизира поредица от стъпки, които също така можете да направите ръчно. Причината за този отговор е, че за еднократна работа обикновено е по-добре просто да я направите.

Rebase е създаден, за да автоматизира работен процес, при който имате клон, от който се сливате или по друг начин поддържате синхрон по време на разработката, и поне за последното обратно сливане (и може би няколко пъти преди това) искате да изчистите историята си.

Удобен е за много други приложения (особено за носене на лепенки), но отново: не е лекарство за всичко. Имате нужда от много чукове. Много от тях могат да бъдат разтегнати, за да служат в краен случай, и аз съм голям фен на всичко, което работи, но мисля, че това е най-доброто за хора, които вече са много добре запознати с техните инструменти.

Това, което искате, не е да създадете единична, чиста линейна история, вие искате нещо различно.

Общият начин да го направите с познати инструменти е лесен, като започнете от вашия демонстрационен скрипт

git checkout :/A; git cherry-pick :/D :/1 :/2; git branch -f foo
git checkout foo^{/D}; git merge foo; git cherry-pick :/E; git branch -f master

и сте готови.

Да, можете да накарате git rebase -ir да настрои това за вас, но когато погледнах списъка за избор, който произвежда, редактирането в правилните инструкции не изглеждаше по-просто или по-лесно от горната последователност. Има да разберете какъв точно резултат искате и да разберете как да накарате git rebase -ir да го направи вместо вас, и просто да го направите.

git rebase -r --onto :/A :/C master
git branch -f foo :/2

е какъвто и да работи отговор, за който вероятно бих използвал, тъй като Inigo казва точното репо, което показвате във вашия въпрос. Вижте git help revisions документа за синтаксиса за търсене на съобщения.

person jthill    schedule 24.04.2020

За да пренаредите хронологията на ангажиментите, има няколко начина.

Проблемът с rebase, когато искате да промените цялата история на репо, е, че той премества само един клон наведнъж. Освен това има проблеми при справянето със сливания, така че не можете просто да пребазирате D и E върху A, като същевременно запазвате по-новата история, както съществува сега (защото E е сливане).

Вие можете да заобиколите всичко това, но методът е сложен и податлив на грешки. Има инструменти, които са предназначени за пълно пренаписване на репо. Може да искате да погледнете filter-repo (инструмент, който замества filter-branch) - но изглежда, че просто се опитвате да изчистите конкретен файл от вашата хронология, което (1) може да е добра работа за BFG Repo Cleaner или ( 2) всъщност е достатъчно лесна задача с filter-branch

(Ако искате да разгледате BFG, https://rtyley.github.io/bfg-repo-cleaner/ ; ако искате да разгледате filter-repo, https://github.com/newren/git-filter-repo)

За да използвате filter-branch за тази цел

git filter-branch --index-filter 'git rm --cached --ignore-unmatch path/to/file' --prune-empty -- --all

Въпреки това - вие посочихте, че имате нужда файлът да не е в репото (като контра на нечие предложение просто да го изтриете от следващия комит). Така че трябва да разберете, че git не се отказва от информация толкова лесно. След като използвате някоя от тези техники, все още можете да извлечете файла от репото.

Това е нещо като голяма тема и е обсъждана много пъти в различни въпроси/отговори на SO, така че предлагам да потърсите това, което наистина трябва да попитате: как да премахнете завинаги файл, който никога не е трябвало да бъде под източник контрол.

Няколко бележки:

1 - Ако има пароли и те някога са били изпратени към споделено дистанционно, тези пароли са компрометирани. Не можете да направите нищо по въпроса; сменете паролите.

2 - Всяко репо (дистанционното и всеки клонинг) трябва да бъде умишлено изчистено или изхвърлено и заменено. (Фактът, че не можете да принудите някого да направи това, ако той не иска да сътрудничи, е една от причините за (1).)

3 - В локалното хранилище, където сте направили поправките, трябва да се отървете от рефлогите (както и от резервните реф, които може да са били създадени, ако сте използвали инструмент като filter-branch) и след това да стартирате gc. Или може да е по-лесно повторно клониране към ново репо, което извлича само новите версии на клоновете.

4 - Почистването на дистанционното може дори да не е възможно в зависимост от това как е хоствано. Понякога най-доброто, което можете да направите, е да активирате дистанционното и след това да го пресъздадете от нулата.

person Mark Adelsberger    schedule 11.04.2020
comment
Благодаря за уточнението!! - person Daniel Stephens; 11.04.2020
comment
Освен това има проблеми при справяне със сливания -- НЕ Е ВЯРНО! Вижте моя отговор. Всъщност е доста просто. - person Inigo; 24.04.2020
comment
@Иниго - Грешка. Заставам зад твърдението си. Подобно на много по-старата опция --preserve-merges, опцията --rebase-merges се опитва да обработва сливания, но не го прави добре, освен в много прости случаи (т.е. стратегия за сливане по подразбиране без конфликти) - person Mark Adelsberger; 25.04.2020
comment
Използвам го през цялото време и конфликтите се случват точно както се очаква да се случат. И това рядко е проблем, защото имам git rerere активиран. `stackoverflow.com/a/7241744/8910547 - person Inigo; 25.04.2020
comment
@Inigo - Отне ми точно един опит да създам тестово репо, където не работи пропелери. В този случай той тихомълком доведе до грешен резултат - дори нямаше конфликт, така че rerere нямаше да помогне. Няма да ме убедите, че това, което виждам с очите си, е грешно. О, и ако откриете, че използвате rerere, вие решавате проблеми, които сте създали за себе си. - person Mark Adelsberger; 25.04.2020
comment
@MarkAdelsberger не се колебайте да публикувате това репо някъде, за да можем аз и другите да видим какво имате предвид. - person Inigo; 25.04.2020
comment
@Иниго - Не. Ограниченията, за които говоря, са добре известни. Не чувствам необходимост да ти го доказвам само защото избираш да намекнеш, че ти дължа нещо. - person Mark Adelsberger; 25.04.2020
comment
да така си помислих - person Inigo; 25.04.2020

Първото нещо, което трябва да разберете е, че ангажиментите са неизменни обекти. Когато пренапишете историята, както предлагате, ще получите напълно различен набор от ангажименти. Родителят е част от неизменния хеш на всеки комит, наред с други неща, които не можете да промените. Ако направите това, което предлагате, вашата история ще изглежда така:

     D'-----E'-----> (master)
    /
---A----B-----C-----D--------E-------> (abandoned)
                     \      /
                      1----2 (foo)

За да постигнете това, трябва просто да пребазирате D..E върху A и да нулирате master на E'. Можете (но наистина не е нужно) след това да пребазирате 1..foo върху D'.

Много по-прост и по мое мнение правилен начин би бил просто да изтриете файла в нов комит:

---A----B-----C-----D--------E-----F-----> (master)
                     \      /
                      1----2 (foo)

Тук F е резултатът от git rm that_file. Целта на git е да поддържа историята. Подрязването му само защото не изглежда красиво не е продуктивно (отново мое мнение). Единственият път, когато бих препоръчал предишната опция, е във въпросния файл да има чувствителна информация като пароли.

Ако, от друга страна, почистването на файла е това, което искате, ще трябва да вземете по-крайни мерки. Например: Как да премахнете файл от хронологията на Git?

person Mad Physicist    schedule 11.04.2020
comment
Благодаря много за изчерпателния отговор! За съжаление файлът съдържа данни, които не трябва да бъдат в репо, така че мисля, че трябва да премина през предишната опция! - person Daniel Stephens; 11.04.2020
comment
@DanielStephens Ако се опитвате да изчистите файла от репото (напр. защото съдържа чувствителни данни), тогава процедурите в този отговор може да не са достатъчни. - person Mark Adelsberger; 11.04.2020