Inserir um commit antes do commit root no Git?

Eu perguntei antes sobre como esmagar os dois primeiros commits em um repository git.

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

Então, eu prefiro passar por dor apenas uma vez e, em seguida, ser capaz de 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 propósito de ser o primeiro. Nenhum código, nada. Apenas ocupando espaço para que possa ser a base para rebase.

A minha pergunta é: tendo um repository existente, como faço para inserir um novo e vazio commit antes do primeiro, e para mudar todo mundo para frente?

Meados de 2017 resposta

Criar um novo commit completamente vazio sem efeitos colaterais é provavelmente o melhor feito usando o encanamento do Git diretamente. Fazê-lo dessa forma evita quaisquer efeitos colaterais: não tocar na cópia de trabalho ou no índice, não há ramificações temporárias para limpar, etc.

  1. Para criar um commit, precisamos de uma tree de diretórios para isso, então criamos um vazio primeiro:

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

     commit=`git commit-tree -m 'root commit' $tree` 
  3. E agora podemos nos rebase para isso:

     git rebase --onto $commit --root master 

E é isso. Você pode reorganizar essa coisa toda em uma única linha se você conhece bem a sua casca.

(NB: na prática eu agora usaria o filter-branch . Edite isso mais tarde.)


Resposta histórica (referenciada por outras respostas)

Aqui está uma implementação mais limpa da mesma solução, na medida em que funciona sem a necessidade de criar um repository extra, interagir com remotas e corrigir um header desanexado:

 # first you need a new empty branch; let's call it `newroot` git checkout --orphan newroot git rm -rf . # then you apply the same steps git commit --allow-empty -m 'root commit' git rebase --onto newroot --root master git branch -d newroot 

Voila, você acabou no master com sua história reescrita para include um commit root vazio.


NB .: em versões antigas do Git que não possuem a opção --orphan , você precisa do encanamento para criar uma ramificação vazia:

 git symbolic-ref HEAD refs/heads/newroot git rm --cached -r . git clean -f -d 

Mesclar as respostas de Aristóteles Pagaltzis e Uwe Kleine-König e o 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 

(só para colocar tudo em um só lugar)

Eu gosto da resposta de Aristóteles. Mas descobriu que, para um grande repository (> 5000 commits), o ramo de filtro funciona melhor do que o rebase por vários motivos: 1) é mais rápido; 2) não requer intervenção humana quando há um conflito de mesclagem. 3) pode rewrite as tags – preservando-as. Note que o ramo de filtro funciona porque não há dúvidas sobre o conteúdo de cada commit – é exatamente o mesmo que antes deste ‘rebase’.

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 /"' --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.

Eu usei peças 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 /"' --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 irá rewrite todos os ramos (não apenas o master ), além de tags.

git rebase --root --onto $emptyrootcommit

deve fazer o truque com facilidade

Eu fiquei animado e escrevi uma versão ‘idempotente’ desse script legal … ele sempre inserirá o mesmo commit vazio, e se você executá-lo duas vezes, ele não alterará seus hashes de commit cada vez. Então, aqui está minha opinião sobre 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 complexidade extra? talvez não, mas vou usar este.

Isso também deve permitir executar esta operação em várias cópias clonadas do repository, e acabar com os mesmos resultados, então eles ainda são compatíveis … testando … sim, funciona, mas também precisa excluir e adicionar seu remotos novamente, por exemplo:

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

Bem, aqui está o que eu criei:

 # 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 

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

  • melhor desempenho
  • mais fácil e menos arriscado (você pode verificar seu resultado em cada etapa e desfazer o que você fez …)
  • trabalhe bem com múltiplos ramos com resultados garantidos

A ideia por trás disso é:

  • Crie um novo commit vazio no passado
  • Substitua a consolidação da raiz antiga por uma confirmação exatamente semelhante, exceto que a nova confirmação raiz é adicionada como pai
  • Verifique se tudo está conforme esperado e execute git filter-branch
  • Mais uma vez, verifique se tudo está OK e limpe os arquivos git não mais necessários

Aqui está um script para os dois 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ê poderia executar este script sem riscos (mesmo se fazer um backup antes de executar uma ação que você nunca fez antes é uma boa idéia;)), e se o resultado não for o esperado, basta deletar os arquivos criados na pasta .git/refs/replace e tente novamente;)

Depois de verificar se o estado do repository é o esperado, execute o seguinte comando para atualizar o histórico de todas as ramificações :

 git filter-branch -- --all 

Agora, você deve ver 2 histórias, a antiga e a nova (veja a ajuda na filter-branch para mais informações). Você pode comparar os 2 e verificar novamente se tudo estiver OK. Se estiver satisfeito, exclua os arquivos não mais necessários:

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

Você pode retornar para sua ramificação master e excluir a ramificação temporária:

 git checkout master git branch -D new-root 

Agora, tudo deve ser feito;)

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

  • verifica o ramo original, não apenas o master , quando feito;
  • Eu tentei evitar a ramificação temporária, mas git checkout --orphan só funciona com um branch, não com o estado de head desanexado, então é verificado o tempo suficiente para fazer com que a nova raiz seja confirmada e depois deletada;
  • ele usa o hash do novo commit de raiz durante o filter-branch (Kent deixou um espaço reservado para substituição manual);
  • a operação de filter-branch reescreve apenas as ramificações locais, não os controles remotos também
  • os metadados autor e committer são padronizados para que o commit raiz seja idêntico nos repositorys.

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

Para mudar o commit da raiz:

Primeiro, crie o commit desejado como o primeiro.

Em segundo lugar, mude a ordem dos commits usando:

git rebase -i –root

Um editor irá aparecer com os commits até o commit da raiz, como:

escolher 1234 mensagem raiz antiga

escolha 0294 A commit no meio

escolher 5678 confirmar você quer colocar na raiz

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

escolher 5678 confirmar você quer colocar na raiz

escolher 1234 mensagem raiz antiga

escolha 0294 A commit no meio

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

PS: Para alterar o editor git usa, execute:

git config –global core.editor name_of_the_editor_program_you_want_to_use

Aqui está um simples one-liner que pode ser usado para adicionar um commit vazio no começo de um repository, se você esqueceu de criar um commit vazio imediatamente após o “git init”:

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

Seguindo resposta 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 o seu repo não deve conter modificações locais esperando para ser confirmado.
Note git checkout --orphan irá funcionar em novas versões do git, eu acho.
Observe que a maior parte do tempo git status fornece dicas úteis.

Comece um novo repository.

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

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

Quando você chegar a hoje, troque os repositorys e pronto.

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

Isso também tornará mais agradável quando você decidir que o passado aconteceu de outra maneira daqui a uma semana.

Eu sei que este post é antigo, mas esta página é a primeira quando pesquisando “inserir commit git”.

Por que complicar as coisas simples?

Você tem ABC e quer o ABZC.

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

[Você pode fazer quantas git commit quantas quiser aqui para inserir mais commits. É claro, você pode ter problemas com o passo 5, mas resolver conflitos conflitantes 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 git!