Inserir uma confirmação antes da confirmação raiz no Git?


231

Eu perguntei antes sobre como compactar os dois primeiros commits em um repositório git.

Embora as soluções sejam bastante interessantes e não sejam tão perturbadoras quanto outras coisas no git, elas ainda são um pouco da proverbial bolsa de mágoa se você precisar repetir o procedimento várias vezes ao longo do desenvolvimento do seu projeto.

Portanto, prefiro sofrer apenas uma vez e depois poder usar para sempre o rebase interativo padrão.

O que eu quero fazer, então, é ter um commit inicial vazio que existe apenas com o objetivo de ser o primeiro. Sem código, sem nada. Apenas ocupando espaço para que possa ser a base para a recuperação.

Minha pergunta então é: ter um repositório existente, como faço para inserir um novo commit vazio antes do primeiro e fazer com que todos os outros avancem?


3
;) Eu acho que isso merece uma resposta de qualquer maneira. Estou explorando as várias maneiras de enlouquecer editando obsessivamente a história. Não se preocupe, não é um repositório compartilhado.
kch

11
De um editor de história obsessivo e louco para outro, obrigado por postar a pergunta! ; D
Marco

10
Na defesa de @ kch, um motivo perfeitamente legítimo é aquele em que me encontro: Adicionando um instantâneo de uma versão histórica que nunca foi capturada no repositório.
Old McStopher

4
Eu tenho outro motivo legítimo! Adicionando um commit vazio antes do primeiro, para poder rebase para o primeiro commit e remover o inchaço binário adicionado no commit inicial de um repositório (:
pospi

Respostas:


314

Existem 2 etapas para conseguir isso:

  1. Crie uma nova confirmação vazia
  2. Reescreva o histórico para iniciar a partir desta confirmação vazia

Colocaremos o novo commit vazio em uma ramificação temporária newrootpor conveniência.

1. Crie uma nova confirmação vazia

Há várias maneiras de fazer isso.

Usando apenas encanamento

A abordagem mais limpa é usar o encanamento do Git para criar um commit diretamente, o que evita tocar na cópia de trabalho ou no índice ou em qual filial está com check-out, etc.

  1. Crie um objeto de árvore para um diretório vazio:

    tree=`git hash-object -wt tree --stdin < /dev/null`
    
  2. Envolva um commit em torno dele:

    commit=`git commit-tree -m 'root commit' $tree`
    
  3. Crie uma referência a ele:

    git branch newroot $commit
    

É claro que você pode reorganizar todo o procedimento em uma linha, se conhecer bem o seu shell.

Sem encanamento

Com comandos regulares de porcelana, você não pode criar uma confirmação vazia sem verificar a newrootramificação e atualizar o índice e a cópia de trabalho repetidamente, sem um bom motivo. Mas alguns podem achar isso mais fácil de entender:

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

Observe que nas versões muito antigas do Git sem a --orphanopção de mudar checkout, você deve substituir a primeira linha por esta:

git symbolic-ref HEAD refs/heads/newroot

2. Reescreva o histórico para começar com este commit vazio

Você tem duas opções aqui: reorganizar ou reescrever um histórico limpo.

Rebasing

git rebase --onto newroot --root master

Isso tem a virtude da simplicidade. No entanto, também atualizará o nome e a data do consolidador em cada último commit na ramificação.

Além disso, com alguns históricos de casos extremos, pode até falhar devido a conflitos de mesclagem - apesar do fato de você estar reestruturando em um commit que não contém nada.

Reescrever o histórico

A abordagem mais limpa é reescrever a ramificação. Ao contrário de git rebase, você precisará procurar em qual commit seu branch começa:

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

A reescrita acontece no segundo passo, obviamente; é o primeiro passo que precisa de explicação. O que git replacefaz é dizer ao Git que, sempre que vir uma referência a um objeto que você deseja substituir, o Git deve procurar a substituição desse objeto.

Com a --graftopção, você está dizendo algo ligeiramente diferente do normal. Você está dizendo que ainda não tem um objeto de substituição, mas deseja substituir o <currentroot>objeto de confirmação por uma cópia exata de si mesmo, exceto que o (s) commit (s) pai da substituição deve ser aquele que você listou (ou seja, o newrootcommit ) Em seguida git replace, prossegue e cria esse commit para você e declara esse commit como substituto para o commit original.

Agora, se você fizer um git log, verá que as coisas já estão como você deseja: o ramo começa a partir newroot.

No entanto, observe que git replace na verdade não modifica o histórico - nem se propaga para fora do seu repositório. Ele simplesmente adiciona um redirecionamento local ao seu repositório de um objeto para outro. O que isso significa é que ninguém mais vê o efeito dessa substituição - somente você.

É por isso que o filter-branchpasso é necessário. Com git replacevocê, crie uma cópia exata com confirmações pai ajustadas para a confirmação raiz; git filter-branchem seguida, repete esse processo para todas as confirmações a seguir. É aí que a história é realmente reescrita para que você possa compartilhá-la.


1
Essa --onto newrootopção é redundante; você pode ficar sem ele porque o argumento que você passa newroot, é o mesmo que o argumento upstream - newroot.
wilhelmtell

7
Por que não usar porcelana em vez de comandos de encanamento? Eu substituí git refs CABEÇA simbólico-ref / heads / NewRoot com git checkout --orphan NewRoot
albfan

4
@ nenopera: porque esta resposta foi escrita antes git-checkouttinha essa opção. Eu atualizei para mencionar essa abordagem primeiro, obrigado pelo ponteiro.
Aristóteles Pagaltzis

1
Se sua nova raiz não estiver vazia, use git rebase --merge -s recursive -X theirs --onto newroot --root masterpara resolver todos os conflitos automaticamente (consulte esta resposta). @AlexanderKuzin
user

1
@ Geremia Você pode alterar apenas o último commit, por isso, se o seu repositório contiver apenas o commit raiz, ele poderá funcionar, caso contrário, você precisará refazer todos os outros commits no repositório, no repositório raiz, de qualquer maneira. Mas, mesmo assim, o tópico implica que você não deseja alterar a confirmação da raiz, mas deseja inserir outra antes da raiz existente.
usuário

30

Mesclagem das respostas de Aristóteles Pagaltzis e Uwe Kleine-König e do comentário de Richard Bronosky.

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

(apenas para colocar tudo em um só lugar)


Isto e excelente. Seria bom se isso pudesse ser o que um git rebase -i --root fez internamente.
Aredridel

Sim, fiquei surpreso ao descobrir que não.
Antony Hatchkins

Eu tive que mudar o comando rebase para git rebase newroot master, por causa de erro.
Marcel82

@ antony-hatchkins obrigado por isso. Eu tenho um repositório git existente e (por várias razões que não abordarei aqui) estou tentando anexar um commit git NÃO-VAZIO como meu primeiro commit. Então substituí o git commit --allow-empty -m 'initial' pelo git add.; git commit -m "commit inicial do laravel"; empurrão git; E então esta etapa de rebase: git rebase --onto newroot --root master está falhando com uma tonelada de conflitos de mesclagem. Algum conselho? : ((
kp123 31/12/19

@ kp123 tentar cometer um vazio :)
Antony Hatchkins

12

Eu gosto da resposta de Aristóteles. Mas descobriu que, para um repositório grande (> 5000 confirmações), o filtro-branch funciona melhor do que o rebase por várias razões 1) é mais rápido 2) não requer intervenção humana quando há um conflito de mesclagem. 3) pode reescrever as tags - preservando-as. Observe que o branch-filter funciona porque não há dúvidas sobre o conteúdo de cada confirmação - é exatamente o mesmo que antes dessa 're-refração'.

Meus passos são:

# 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

Observe que as opções '--tag-name-filter cat' significam que as tags serão reescritas para apontar para as confirmações recém-criadas.


Isso não ajuda a criar confirmações não vazias, que também é um caso de uso interessante.
precisa saber é o seguinte

Em comparação com outras soluções, a sua tem apenas um efeito colateral insignificante: muda os hashes, mas toda a história permanece intocada. Obrigado!
Vladyslav Savchenko

5

Usei partes da resposta de Aristóteles e Kent com sucesso:

# 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

Isso também reescreverá todas as ramificações (não apenas master), além das tags.


o que essa última linha faz?
Diederick C. Niehorster

Ele pesquisa refs/original/e exclui cada referência. As referências que ele exclui já devem ser referenciadas por algum outro ramo, para que elas não desapareçam, apenas refs/original/são removidas.
Ldav1s

Isso funcionou para mim. Além disso, eu costumava timedatectl set-time '2017-01-01 00:00:00'dar newrootum timestamp antigo.
Chrm 14/05

4

git rebase --root --onto $emptyrootcommit

deve fazer o truque facilmente


3
$emptyrootcommité uma variável shell que se expande para nada, com certeza?
Flimm

@Flimm: $ emptyrootcommit é o sha1 de um commit vazio que o pôster original já parece ter.
Uwe Kleine-König

4

Eu acho que usar git replacee git filter-branché uma solução melhor do que usar um git rebase:

  • melhor performance
  • mais fácil e menos arriscado (você pode verificar seu resultado a cada etapa e desfazer o que fez ...)
  • funciona bem com várias ramificações com resultados garantidos

A idéia por trás disso é:

  • Crie uma nova confirmação vazia no passado
  • Substitua a confirmação raiz antiga por uma confirmação exatamente semelhante, exceto que a nova confirmação raiz foi adicionada como pai
  • Verifique se tudo está como o esperado e execute git filter-branch
  • Mais uma vez, verifique se está tudo bem e limpe os arquivos git que não são mais necessários

Aqui está um script para os 2 primeiros passos:

#!/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"

Você pode executar esse script sem risco (mesmo que seja uma boa idéia fazer um backup antes de executar uma ação que nunca fez antes;)) e, se o resultado não for o esperado, exclua os arquivos criados na pasta .git/refs/replacee tente novamente; )

Depois de verificar se o estado do repositório é o que você espera, execute o seguinte comando para atualizar o histórico de todas as ramificações :

git filter-branch -- --all

Agora, você deve ver 2 históricos, o antigo e o novo (consulte a ajuda filter-branchpara obter mais informações). Você pode comparar o 2 e verificar novamente se está tudo bem. Se estiver satisfeito, exclua os arquivos desnecessários:

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

Você pode retornar ao seu masterramo e excluir o ramo temporário:

git checkout master
git branch -D new-root

Agora, tudo deve ser feito;)


3

Fiquei empolgado e escrevi uma versão 'idempotente' desse bom script ... ele sempre inserirá o mesmo commit vazio, e se você executá-lo duas vezes, ele não muda seus hashes de commit toda vez. Então, aqui está minha opinião sobre o 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

Vale a pena a complexidade extra? talvez não, mas eu vou usar este.

Isso DEVE também permitir executar esta operação em várias cópias clonadas do repositório e terminar com os mesmos resultados, para que eles ainda sejam compatíveis ... testes ... sim, funciona, mas também é necessário excluir e adicionar seu arquivo. controles remotos novamente, por exemplo:

git remote rm origin
git remote add --track master user@host:path/to/repo

3

Para adicionar uma confirmação vazia no início de um repositório, se você esqueceu de criar uma confirmação vazia imediatamente após o "git init":

git rebase --root --onto $(git commit-tree -m 'Initial commit (empty)' 4b825dc642cb6eb9a060e54bf8d69288fbee4904)

1
4b825dc ... é o hash da árvore vazia: stackoverflow.com/questions/9765453/…
mrks

2

Bem, aqui está o que eu vim com:

# 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

2

Aqui está o meu bashscript baseado na resposta de Kent com melhorias:

  • verifica o ramo original, não apenas masterquando está pronto;
  • Tentei evitar a ramificação temporária, mas git checkout --orphanfunciona apenas com uma ramificação, sem um estado de cabeça desanexada, por isso foi verificado o tempo suficiente para fazer com que a nova raiz seja confirmada e excluída;
  • ele usa o hash do novo commit raiz durante o filter-branch(Kent deixou um espaço reservado para substituição manual);
  • a filter-branchoperação reescreve apenas as ramificações locais, e não os controles remotos também
  • os metadados do autor e do commit são padronizados para que o commit raiz seja idêntico nos repositórios.

#!/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='nobody@example.org'
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"

2

Para alternar a confirmação raiz:

Primeiro, crie o commit que você deseja como o primeiro.

Segundo, mude a ordem dos commits usando:

git rebase -i --root

Um editor aparecerá com as confirmações até a confirmação da raiz, como:

escolha 1234 mensagem raiz antiga

pick 0294 Um commit no meio

escolha 5678 commit que você deseja colocar na raiz

Você pode colocar o commit que deseja primeiro, colocando-o na primeira linha. No exemplo:

escolha 5678 commit que você deseja colocar na raiz

escolha 1234 mensagem raiz antiga

pick 0294 Um commit no meio

Saia do editor, a ordem de confirmação será alterada.

PS: Para alterar o editor que o git usa, execute:

git config --global core.editor name_of_the_editor_program_you_want_to_use


1
Agora que o rebase tem --root, esta é de longe a solução mais limpa.
Ross Burton

1

Combinando o melhor e o mais recente. Sem efeitos colaterais, sem conflitos, mantendo as tags.

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

Observe que, no GitHub, você perderá os dados de execução do IC e o PR poderá ficar bagunçado, a menos que outras ramificações também sejam corrigidas.


0

Resposta a seguir Aristóteles Pagaltzis e outros, mas usando comandos mais simples

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).

Observe que seu repositório não deve conter modificações locais aguardando confirmação.
Nota git checkout --orphanfuncionará em novas versões do git, eu acho.
Observe que na maioria das vezes git statusfornece dicas úteis.


-6

Inicie um novo repositório.

Defina sua data de volta para a data de início desejada.

Faça tudo da maneira que você gostaria que tivesse feito, ajustando a hora do sistema para refletir quando você desejasse ter feito dessa maneira. Puxe arquivos do repositório existente conforme necessário para evitar muita digitação desnecessária.

Quando você chegar hoje, troque os repositórios e pronto.

Se você é apenas louco (estabelecido), mas razoavelmente inteligente (provavelmente, porque você precisa ter uma certa quantidade de inteligência para pensar em idéias malucas como essa), você guiará o processo.

Isso também será mais agradável quando você decidir que deseja que o passado aconteça de outra maneira daqui a uma semana.


Tenho um mau pressentimento sobre uma solução que exige que você mexa com a data do sistema, mas você me deu uma idéia, que eu desenvolvi um pouco e, infelizmente, funcionou. Então obrigado.
kch

-7

Eu sei que este post é antigo, mas esta página é a primeira no Google "inserindo commit git".

Por que complicar as coisas simples?

Você tem ABC e deseja ABZC.

  1. git rebase -i trunk (ou qualquer coisa antes de B)
  2. alterar pick para editar na linha B
  3. faça suas alterações: git add ..
  4. git commit( git commit --amendque editará B e não criará Z)

[Você pode fazer quantas git commitquiser aqui para inserir mais confirmações. Obviamente, você pode ter problemas com a etapa 5, mas resolver conflitos de fusão com o git é uma habilidade que você deve ter. Se não, pratique!]

  1. git rebase --continue

Simples, não é?

Se você entende git rebase, adicionar um commit 'root' não deve ser um problema.

Divirta-se com o git!


5
A pergunta pede a inserção de um primeiro commit: do ABC você deseja o ZABC. Um simples git rebasenão pode fazer isso.
precisa
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.