Wstawić zatwierdzenie przed zatwierdzeniem głównym w Git?

231

Pytałem wcześniej, jak zmiażdżyć pierwsze dwa commity w repozytorium git.

Chociaż rozwiązania są dość interesujące i nie tak zawrotne w głowie, jak niektóre inne rzeczy w git, wciąż są trochę przysłowiową torbą cierpienia, jeśli trzeba powtarzać procedurę wiele razy podczas opracowywania projektu.

Wolę więc przejść przez ból tylko raz, a potem móc wiecznie korzystać ze standardowej interaktywnej bazy.

To, co chcę zrobić, to mieć puste początkowe zatwierdzenie, które istnieje wyłącznie w celu bycia pierwszym. Bez kodu, bez niczego. Zajmuje tylko miejsce, dzięki czemu może być bazą dla bazy.

Moje pytanie brzmi zatem: mając istniejące repozytorium, jak przejść do wstawiania nowego, pustego zatwierdzenia przed pierwszym i przesuwania wszystkich do przodu?

kch
źródło
3
;) Myślę, że i tak uzasadnia to odpowiedź. W pewnym sensie badam wiele sposobów, w jakie można oszaleć, obsesyjnie edytując historię. Nie martw się, nie wspólne repozytorium.
kch
11
Od jednego obsesyjnego, szalonego edytora historii do drugiego, dziękuję za opublikowanie pytania! ; D
Marco
10
W obronie @ kch znajduje się jeden całkowicie uzasadniony powód: dodanie migawki historycznej wersji, która nigdy nie została uchwycona w repozytorium.
Old McStopher
4
Mam inny uzasadniony powód! Dodanie pustego zatwierdzenia przed pierwszym, aby móc dokonać zmiany bazy na pierwszy zatwierdzenie i usunąć binarne wzdęcia dodane w początkowym zatwierdzeniu repozytorium (:
pospi

Odpowiedzi:

314

Aby to osiągnąć, należy wykonać 2 kroki:

  1. Utwórz nowy pusty zatwierdzenie
  2. Przepisz historię, aby zacząć od tego pustego zatwierdzenia

newrootDla wygody umieścimy nowy pusty zatwierdzenie w tymczasowej gałęzi .

1. Utwórz nowy pusty zatwierdzenie

Można to zrobić na wiele sposobów.

Używając tylko hydrauliki

Najczystszym podejściem jest użycie hydrauliki Gita do bezpośredniego utworzenia zatwierdzenia, co pozwala uniknąć dotykania kopii roboczej lub indeksu lub gałęzi, która jest pobierana itp.

  1. Utwórz obiekt drzewa dla pustego katalogu:

    tree=`git hash-object -wt tree --stdin < /dev/null`
    
  2. Zawiń wokół niego zatwierdzenie:

    commit=`git commit-tree -m 'root commit' $tree`
    
  3. Utwórz odniesienie do niego:

    git branch newroot $commit
    

Możesz oczywiście przestawić całą procedurę na jednowarstwową, jeśli dobrze znasz swoją powłokę.

Bez kanalizacji

Za pomocą zwykłych poleceń porcelany nie można utworzyć pustego zatwierdzenia bez sprawdzenia newrootgałęzi i wielokrotnego aktualizowania indeksu i kopii roboczej, bez wyraźnego powodu. Ale niektórym może to być łatwiejsze do zrozumienia:

git checkout --orphan newroot
git rm -rf .
git clean -fd
git commit --allow-empty -m 'root commit'

Zauważ, że w bardzo starych wersjach Gita, które nie mają --orphanprzełącznika na checkout, musisz zastąpić pierwszy wiersz tym:

git symbolic-ref HEAD refs/heads/newroot

2. Przepisz historię, aby zacząć od tego pustego zatwierdzenia

Masz tutaj dwie opcje: bazowanie lub czyste przepisywanie historii.

Rebasing

git rebase --onto newroot --root master

Ma to zaletę prostoty. Jednak zaktualizuje także nazwę i datę osoby zatwierdzającej przy każdym ostatnim zatwierdzeniu w oddziale.

Ponadto, w przypadku niektórych historii przypadków, może nawet zawieść z powodu konfliktów scalania - pomimo tego, że bazujesz na zatwierdzeniu, które nie zawiera niczego.

Przepisywanie historii

Czystsze podejście polega na przepisaniu gałęzi. W przeciwieństwie do git rebase, musisz sprawdzić, które zatwierdzenie rozpoczyna się w twoim oddziale:

git replace <currentroot> --graft newroot
git filter-branch master

Oczywiście przepisywanie odbywa się na drugim etapie; to pierwszy krok, który wymaga wyjaśnienia. Co git replacemówi Git, że ilekroć widzi odniesienie do obiektu, który chcesz zastąpić, Git powinien zamiast tego spojrzeć na zastąpienie tego obiektu.

Za pomocą --graftprzełącznika mówisz mu coś nieco innego niż zwykle. Mówisz, że nie masz jeszcze obiektu zastępującego, ale chcesz zastąpić <currentroot>obiekt zatwierdzenia jego dokładną kopią, z wyjątkiem tego, że nadrzędne zatwierdzenia zastąpienia powinny być tymi wymienionymi przez Ciebie (tj. newrootZatwierdzeniem ). Następnie git replaceidzie do przodu i tworzy to zatwierdzenie dla ciebie, a następnie deklaruje to jako zamiennik oryginalnego zatwierdzenia.

Teraz, jeśli to zrobisz git log, zobaczysz, że rzeczy już wyglądają tak, jak chcesz: gałąź zaczyna się newroot.

Należy jednak pamiętać, że git replace tak naprawdę nie modyfikuje historii - ani nie rozprzestrzenia się poza repozytorium. Po prostu dodaje lokalne przekierowanie do twojego repozytorium z jednego obiektu do drugiego. Oznacza to, że nikt inny nie widzi efektu tej zamiany - tylko ty.

Dlatego ten filter-branchkrok jest konieczny. Po git replaceutworzeniu dokładnej kopii ze skorygowanymi zatwierdzeniami nadrzędnymi dla zatwierdzenia głównego; git filter-branchnastępnie powtarza ten proces dla wszystkich następujących zatwierdzeń. Właśnie tam historia jest przepisywana, abyś mógł ją udostępnić.

Arystoteles Pagaltzis
źródło
1
Ta --onto newrootopcja jest zbędna; możesz się bez niego obejść, ponieważ przekazany argument newrootjest taki sam, jak argument poprzedzający - newroot.
wilhelmtell
7
Dlaczego nie użyć porcelany zamiast poleceń hydraulicznych ?. Chciałbym zastąpić git ref symboliczne-HEAD bibl / głowice / NewRoot z git checkout --orphan NewRoot
albfan
4
@ nenopera: ponieważ ta odpowiedź została napisana wcześniej, git-checkoutmiał tę zmianę. Najpierw zaktualizowałem to, aby wspomniało o tym podejściu, dzięki za wskaźnik.
Arystoteles Pagaltzis
1
Jeśli Twój nowy katalog główny nie jest pusty, użyj go, git rebase --merge -s recursive -X theirs --onto newroot --root masteraby automatycznie rozwiązać wszystkie konflikty (zobacz odpowiedź). @AlexanderKuzin
użytkownik
1
@ Geremia Możesz zmienić tylko ostatnie zatwierdzenie, więc jeśli twoje repozytorium zawiera tylko zatwierdzenie główne, może ono działać, w przeciwnym razie będziesz musiał zmienić bazę wszystkich innych zatwierdzeń w repozytorium nad poprawionym zatwierdzeniem głównym. Ale nawet wtedy temat sugeruje, że nie chcesz zmieniać zatwierdzenia roota, ale zamiast tego chcesz wstawić inny przed istniejącym rootem.
użytkownik
30

Scalenie odpowiedzi Arystotelesa Pagaltzisa i Uwe Kleine-Königa oraz komentarza Richarda Bronosky'ego.

git symbolic-ref HEAD refs/heads/newroot
git rm --cached -r .
git clean -f -d
# touch .gitignore && git add .gitignore # if necessary
git commit --allow-empty -m 'initial'
git rebase --onto newroot --root master
git branch -d newroot

(po prostu umieścić wszystko w jednym miejscu)

Antony Hatchkins
źródło
To jest wspaniałe. Byłoby miło, gdyby to mogło być to, co zrobił git rebase -i --root.
aredridel
Tak, byłem zaskoczony, gdy dowiedziałem się, że tak nie jest.
Antony Hatchkins,
Z git rebase newroot masterpowodu błędu musiałem zmienić polecenie rebase na .
marbel82
@ antony-hatchkins dzięki za to. Mam istniejące repozytorium git i (z różnych powodów, do których nie będę tutaj wchodził) próbuję dołączyć NIEPUSTNE zatwierdzenie git jako moje pierwsze zatwierdzenie. Więc zastąpiłem git commit --allow-empty -m 'initial' git add.; git commit -m "initial laravel commit"; git push; A potem ten krok zmiany: git rebase --onto newroot - root root nie działa z TONĄ konfliktów scalania. Jakakolwiek rada? : ((
kp123,
@ kp123 spróbuj pustego zatwierdzenia :)
Antony Hatchkins
12

Podoba mi się odpowiedź Arystotelesa. Okazało się jednak, że dla dużego repozytorium (> 5000 zatwierdzeń) odgałęzienie filtru działa lepiej niż rebase z kilku powodów 1) jest szybsze 2) nie wymaga interwencji człowieka, gdy występuje konflikt scalenia. 3) może przepisać tagi - zachowując je. Zauważ, że odgałęzienie filtru działa, ponieważ nie ma wątpliwości co do zawartości każdego zatwierdzenia - jest dokładnie takie samo jak przed tym „rebase”.

Moje kroki to:

# first you need a new empty branch; let's call it `newroot`
git symbolic-ref HEAD refs/heads/newroot
git rm --cached -r .
git clean -f -d

# then you apply the same steps
git commit --allow-empty -m 'root commit'

# then use filter-branch to rebase everything on newroot
git filter-branch --parent-filter 'sed "s/^\$/-p <sha of newroot>/"' --tag-name-filter cat master

Zauważ, że opcje „--tag-name-filter cat” oznaczają, że tagi zostaną przepisane, aby wskazywały na nowo utworzone zatwierdzenia.

Kent
źródło
Nie pomaga to w tworzeniu niepustych zatwierdzeń, które są również interesującym przypadkiem użycia.
ceztko
W porównaniu z innymi rozwiązaniami, twój ma tylko jeden nieznaczny efekt uboczny: zmienia skróty, ale cała historia pozostaje nietknięta. Dziękuję Ci!
Władysław Sawczenko
5

Z powodzeniem wykorzystałem fragmenty odpowiedzi Arystotelesa i Kenta:

# first you need a new empty branch; let's call it `newroot`
git checkout --orphan newroot
git rm -rf .
git commit --allow-empty -m 'root commit'
git filter-branch --parent-filter \
'sed "s/^\$/-p <sha of newroot>/"' --tag-name-filter cat -- --all
# clean up
git checkout master
git branch -D newroot
# make sure your branches are OK first before this...
git for-each-ref --format="%(refname)" refs/original/ | \
xargs -n 1 git update-ref -d

Spowoduje to również przepisanie wszystkich gałęzi (nie tylko master) oprócz znaczników.

ldav1s
źródło
co robi ta ostatnia linia?
Diederick C. Niehorster
Przeszukuje refs/original/i usuwa każdy numer referencyjny. Odwołania, które usuwa, powinny już być przywoływane przez inną gałąź, więc tak naprawdę nie znikają, tylko zostają refs/original/usunięte.
ldav1s
To zadziałało dla mnie. Dodatkowo użyłem timedatectl set-time '2017-01-01 00:00:00'dać newrootstarą sygnaturę czasową.
chrm
4

git rebase --root --onto $emptyrootcommit

powinien zrobić to łatwo

Uwe Kleine-König
źródło
3
$emptyrootcommitjest z pewnością zmienną powłoki, która z pewnością się rozszerza?
Flimm,
@Flimm: $ emptyrootcommit to sha1 pustego zatwierdzenia, które już wydaje się mieć w oryginalnym plakacie.
Uwe Kleine-König
4

Myślę, że użycie git replacei git filter-branchjest lepszym rozwiązaniem niż użycie git rebase:

  • lepsza wydajność
  • łatwiej i mniej ryzykownie (możesz zweryfikować swój wynik na każdym kroku i cofnąć to, co zrobiłeś ...)
  • działa dobrze z wieloma gałęziami z gwarantowanymi wynikami

Ideą tego jest:

  • Utwórz nowy pusty zatwierdzenie daleko w przeszłości
  • Zamień stary główny zatwierdzenie na podobny dokładnie, z tym wyjątkiem, że nowy główny zatwierdzenie jest dodawany jako rodzic
  • Sprawdź, czy wszystko jest zgodne z oczekiwaniami i uruchom git filter-branch
  • Jeszcze raz sprawdź, czy wszystko jest w porządku i wyczyść niepotrzebne pliki git

Oto skrypt dla 2 pierwszych kroków:

#!/bin/bash
root_commit_sha=$(git rev-list --max-parents=0 HEAD)
git checkout --force --orphan new-root
find . -path ./.git -prune -o -exec rm -rf {} \; 2> /dev/null
git add -A
GIT_COMMITTER_DATE="2000-01-01T12:00:00" git commit --date==2000-01-01T12:00:00 --allow-empty -m "empty root commit"
new_root_commit_sha=$(git rev-parse HEAD)

echo "The commit '$new_root_commit_sha' will be added before existing root commit '$root_commit_sha'..."

parent="parent $new_root_commit_sha"
replacement_commit=$(
 git cat-file commit $root_commit_sha | sed "s/author/$parent\nauthor/" |
 git hash-object -t commit -w --stdin
) || return 3
git replace "$root_commit_sha" "$replacement_commit"

Możesz uruchomić ten skrypt bez ryzyka (nawet jeśli wykonanie kopii zapasowej przed wykonaniem czynności, której nigdy wcześniej nie robiłeś, jest dobrym pomysłem;)), a jeśli wynik nie jest zgodny z oczekiwaniami, po prostu usuń pliki utworzone w folderze .git/refs/replacei spróbuj ponownie; )

Po sprawdzeniu, że stan repozytorium jest zgodny z oczekiwaniami, uruchom następujące polecenie, aby zaktualizować historię wszystkich oddziałów :

git filter-branch -- --all

Teraz musisz zobaczyć 2 historie, starą i nową ( filter-branchwięcej informacji na ten temat znajdziesz w pomocy ). Możesz porównać 2 i ponownie sprawdzić, czy wszystko jest w porządku. Jeśli jesteś zadowolony, usuń niepotrzebne pliki:

rm -rf ./.git/refs/original
rm -rf ./.git/refs/replace

Możesz wrócić do swojego masteroddziału i usunąć oddział tymczasowy:

git checkout master
git branch -D new-root

Teraz wszystko powinno być zrobione;)

Philippe
źródło
3

Byłem podekscytowany i napisałem „idempotentną” wersję tego ładnego skryptu ... zawsze wstawi ten sam pusty zatwierdzenie, a jeśli uruchomisz go dwa razy, nie zmieni to za każdym razem skrótów zatwierdzenia. Oto moje zdanie na temat git-insert-empty-root :

#!/bin/sh -ev
# idempotence achieved!
tmp_branch=__tmp_empty_root
git symbolic-ref HEAD refs/heads/$tmp_branch
git rm --cached -r . || true
git clean -f -d
touch -d '1970-01-01 UTC' .
GIT_COMMITTER_DATE='1970-01-01T00:00:00 +0000' git commit \
  --date='1970-01-01T00:00:00 +0000' --allow-empty -m 'initial'
git rebase --committer-date-is-author-date --onto $tmp_branch --root master
git branch -d $tmp_branch

Czy to warte dodatkowej złożoności? może nie, ale użyję tego.

POWINIEN także pozwolić na wykonanie tej operacji na kilku sklonowanych kopiach repozytorium i uzyskać takie same wyniki, aby były nadal kompatybilne ... testowanie ... tak, działa, ale trzeba również usunąć i dodać ponownie piloty, np .:

git remote rm origin
git remote add --track master user@host:path/to/repo
Sam Watkins
źródło
3

Aby dodać puste zatwierdzenie na początku repozytorium, jeśli zapomniałeś utworzyć puste zatwierdzenie natychmiast po „git init”:

git rebase --root --onto $(git commit-tree -m 'Initial commit (empty)' 4b825dc642cb6eb9a060e54bf8d69288fbee4904)
Rory
źródło
1
4b825dc ... jest skrótem pustego drzewa: stackoverflow.com/questions/9765453/…
mrks
2

Oto co wymyśliłem:

# Just setting variables on top for clarity.
# Set this to the path to your original repository.
ORIGINAL_REPO=/path/to/original/repository

# Create a new repository…
mkdir fun
cd fun
git init
# …and add an initial empty commit to it
git commit --allow-empty -m "The first evil."

# Add the original repository as a remote
git remote add previous $ORIGINAL_REPO
git fetch previous

# Get the hash for the first commit in the original repository
FIRST=`git log previous/master --pretty=format:%H  --reverse | head -1`
# Cherry-pick it
git cherry-pick $FIRST
# Then rebase the remainder of the original branch on top of the newly 
# cherry-picked, previously first commit, which is happily the second 
# on this branch, right after the empty one.
git rebase --onto master master previous/master

# rebase --onto leaves your head detached, I don't really know why)
# So now you overwrite your master branch with the newly rebased tree.
# You're now kinda done.
git branch -f master
git checkout master
# But do clean up: remove the remote, you don't need it anymore
git remote rm previous
kch
źródło
2

Oto mój bashskrypt oparty na odpowiedzi Kenta z ulepszeniami:

  • po zakończeniu sprawdza oryginalną gałąź master;
  • Próbowałem ominąć gałąź tymczasową, ale git checkout --orphandziała tylko z gałęzią, a nie stanem odłączonym, więc jest sprawdzany wystarczająco długo, aby nowy root zatwierdził, a następnie usunięty;
  • używa skrótu nowego zatwierdzenia głównego podczas filter-branch(Kent zostawił tam symbol zastępczy do ręcznej zamiany);
  • filter-branchoperacja przepisuje tylko lokalne oddziały, nie zbyt piloty
  • metadane autora i osoby zatwierdzającej są znormalizowane, dzięki czemu zatwierdzenie główne jest identyczne w repozytoriach.

#!/bin/bash

# Save the current branch so we can check it out again later
INITIAL_BRANCH=`git symbolic-ref --short HEAD`
TEMP_BRANCH='newroot'

# Create a new temporary branch at a new root, and remove everything from the tree
git checkout --orphan "$TEMP_BRANCH"
git rm -rf .

# Commit this empty state with generic metadata that will not change - this should result in the same commit hash every time
export GIT_AUTHOR_NAME='nobody'
export GIT_AUTHOR_EMAIL='[email protected]'
export GIT_AUTHOR_DATE='2000-01-01T00:00:00+0000'
export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"
export GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"
export GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"
git commit --allow-empty -m 'empty root'
NEWROOT=`git rev-parse HEAD`

# Check out the commit we just made and delete the temporary branch
git checkout --detach "$NEWROOT"
git branch -D "$TEMP_BRANCH"

# Rewrite all the local branches to insert the new root commit, delete the 
# original/* branches left behind, and check out the rewritten initial branch
git filter-branch --parent-filter "sed \"s/^\$/-p $NEWROOT/\"" --tag-name-filter cat -- --branches
git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d
git checkout "$INITIAL_BRANCH"
luxagen
źródło
2

Aby przełączyć zatwierdzenie główne:

Najpierw utwórz zatwierdzenie, które chcesz jako pierwsze.

Po drugie, zmień kolejność zatwierdzeń, używając:

git rebase -i --root

Edytor będzie pojawiał się z zatwierdzeniami aż do zatwierdzenia głównego, na przykład:

wybierz 1234 starą wiadomość główną

wybierz 0294 Zatwierdź pośrodku

wybierz 5678 zatwierdzenie, które chcesz umieścić w katalogu głównym

Następnie możesz umieścić zatwierdzenie, które chcesz, umieszczając je w pierwszym wierszu. W przykładzie:

wybierz 5678 zatwierdzenie, które chcesz umieścić w katalogu głównym

wybierz 1234 starą wiadomość główną

wybierz 0294 Zatwierdź pośrodku

Wyjdź z edytora, aby zmienić kolejność zatwierdzeń.

PS: Aby zmienić edytor, którego używa git, uruchom:

git config --global core.editor name_of_the_editor_program_you_want_to_use

tigre200
źródło
1
Teraz, gdy rebase ma --root, jest to zdecydowanie najładniejsze rozwiązanie.
Ross Burton,
1

Łącząc najnowsze i najlepsze. Bez skutków ubocznych, bez konfliktów, utrzymanie tagów.

git log --reverse

tree=`git hash-object -wt tree --stdin < /dev/null`
commit=`git commit-tree -m 'Initialize empty repository' $tree`
echo $commit # copy below, interpolation didn't work for me

git filter-branch --parent-filter 'sed "s/^\$/-p <commit>/"' --tag-name-filter cat master

git log --reverse

Pamiętaj, że w GitHub stracisz dane uruchamiania CI, a PR może zostać pomieszany, chyba że inne gałęzie również zostaną naprawione.

to mój projekt
źródło
0

Po odpowiedzi Arystoteles Pagaltzis i inni, ale używając prostszych poleceń

zsh% git checkout --orphan empty     
Switched to a new branch 'empty'
zsh% git rm --cached -r .
zsh% git clean -fdx
zsh% git commit --allow-empty -m 'initial empty commit'
[empty (root-commit) 64ea894] initial empty commit
zsh% git checkout master
Switched to branch 'master'
zsh% git rebase empty
First, rewinding head to replay your work on top of it...
zsh% git branch -d empty 
Deleted branch empty (was 64ea894).

Pamiętaj, że Twoje repo nie powinno zawierać żadnych lokalnych modyfikacji oczekujących na zatwierdzenie.
Uwaga git checkout --orphanbędzie działać w nowych wersjach git.
Uwaga: przez większość czasu git statusprzydatne wskazówki.

ony
źródło
-6

Uruchom nowe repozytorium.

Ustaw datę z powrotem na żądaną datę początkową.

Rób wszystko, co chcesz, aby to zrobić, dostosowując czas systemowy do odzwierciedlenia tego, kiedy tego chciałeś. W razie potrzeby wyciągaj pliki z istniejącego repozytorium, aby uniknąć niepotrzebnego pisania.

Kiedy dotrzesz do dzisiaj, zamień repozytoria i gotowe.

Jeśli jesteś po prostu szalony (ugruntowany), ale dość inteligentny (prawdopodobnie, ponieważ musisz mieć pewną liczbę bystrych pomysłów, aby wymyślić takie szalone pomysły), napisz ten proces.

Ułatwi to również, gdy zdecydujesz, że przeszłość wydarzy się w inny sposób za tydzień.

MarkusQ
źródło
Mam złe przeczucia co do rozwiązania, które wymaga od ciebie bałagania się z datą systemową, ale dałeś mi pomysł, który trochę opracowałem i, niestety, zadziałał. Więc dziękuję.
kch
-7

Wiem, że ten post jest stary, ale ta strona jest pierwsza, gdy Googling „wstawia commit git”.

Po co komplikować proste rzeczy?

Masz ABC i chcesz ABZC.

  1. git rebase -i trunk (lub cokolwiek przed B)
  2. zmień wybierz, aby edytować w linii B.
  3. wprowadź zmiany: git add ..
  4. git commit( git commit --amendktóry będzie edytować B, a nie tworzyć Z)

[Możesz tutaj zrobić tyle, git commitile chcesz, aby wstawić więcej zatwierdzeń. Oczywiście możesz mieć problemy z krokiem 5, ale rozwiązywanie konfliktu z git to umiejętność, którą powinieneś mieć. Jeśli nie, ćwicz!]

  1. git rebase --continue

Proste, prawda?

Jeśli rozumiesz git rebase , dodanie zatwierdzenia „root” nie powinno stanowić problemu.

Baw się dobrze z git!

Alex
źródło
5
Pytanie dotyczy wstawienia pierwszego zatwierdzenia: z ABC chcesz ZABC. Bezpośredni git rebasenie może tego zrobić.
Petr Viktorin