Como faço para um Git confirmar no passado?

222

Estou convertendo tudo para o Git para meu uso pessoal e encontrei algumas versões antigas de um arquivo já no repositório. Como comprometê-lo com o histórico na ordem correta, de acordo com a "data de modificação" do arquivo, para que eu tenha um histórico preciso do arquivo?

Foi-me dito que algo assim funcionaria:

git filter-branch --env-filter="GIT_AUTHOR_DATE=... --index-filter "git commit path/to/file --date " --tag-name-filter cat -- --all  
desconhecido
fonte
Curto e respostas simples: stackoverflow.com/a/34639957/2708266
Yash
33
Pergunto-me se as pessoas que procuram a resposta a esta só quer manter sua GitHub "streak contribuição" contínua desta forma
Zitro
1
@ZitRo yes. E simplesmente definir git commit --date="xxx day ago" -m "yyy"é suficiente para esse fim, se alguém estiver se perguntando.
Vlas Sokolov
alexpeattie.com/blog/working-with-dates-in-git : se procura uma explicação gentil
thepurpleowl

Respostas:

198

O conselho que você recebeu é falho. Configurar incondicionalmente GIT_AUTHOR_DATE em um --env-filterreescreveria a data de cada confirmação. Além disso, seria incomum usar o git commit dentro --index-filter.

Você está lidando com vários problemas independentes aqui.

Especificando datas diferentes de "agora"

Cada confirmação tem duas datas: a data do autor e a data do confirmador. Você pode substituir cada um deles fornecendo valores por meio das variáveis ​​de ambiente GIT_AUTHOR_DATE e GIT_COMMITTER_DATE para qualquer comando que grava uma nova confirmação. Veja “Formatos de Data” no git-commit (1) ou abaixo:

Git internal format = <unix timestamp> <time zone offset>, e.g.  1112926393 +0200
RFC 2822            = e.g. Thu, 07 Apr 2005 22:13:13 +0200
ISO 8601            = e.g. 2005-04-07T22:13:13

O único comando que grava uma nova confirmação durante o uso normal é git commit . Ele também possui uma --dateopção que permite especificar diretamente a data do autor. Seu uso antecipado inclui git filter-branch --env-filtertambém as variáveis ​​de ambiente mencionadas acima (elas fazem parte do "env" após o qual a opção é nomeada; consulte "Opções" em git-filter-branch (1) e o comando "encanamento" subjacente git-commit -árvore (1) .

Inserindo um arquivo em um único histórico ref

Se o seu repositório é muito simples (ou seja, você possui apenas uma ramificação, sem tags), provavelmente poderá usar o git rebase para fazer o trabalho.

Nos comandos a seguir, use o nome do objeto (hash SHA-1) da confirmação em vez de "A". Não se esqueça de usar um dos métodos de "substituição de data" ao executar o git commit .

---A---B---C---o---o---o   master

git checkout master
git checkout A~0
git add path/to/file
git commit --date='whenever'
git tag ,new-commit -m'delete me later'
git checkout -
git rebase --onto ,new-commit A
git tag -d ,new-commit

---A---N                      (was ",new-commit", but we delete the tag)
        \
         B'---C'---o---o---o   master

Se você deseja atualizar A para incluir o novo arquivo (em vez de criar um novo commit onde foi adicionado), use em git commit --amendvez de git commit. O resultado ficaria assim:

---A'---B'---C'---o---o---o   master

O procedimento acima funciona, desde que você possa nomear o commit que deve ser o pai do seu novo commit. Se você realmente deseja que seu novo arquivo seja adicionado por meio de um novo commit raiz (sem pais), precisará de algo um pouco diferente:

B---C---o---o---o   master

git checkout master
git checkout --orphan new-root
git rm -rf .
git add path/to/file
GIT_AUTHOR_DATE='whenever' git commit
git checkout -
git rebase --root --onto new-root
git branch -d new-root

N                       (was new-root, but we deleted it)
 \
  B'---C'---o---o---o   master

git checkout --orphané relativamente novo (Git 1.7.2), mas existem outras maneiras de fazer a mesma coisa que funcionam em versões mais antigas do Git.

Inserir um arquivo em um histórico multi- referência

Se seu repositório é mais complexo (ou seja, possui mais de uma referência (ramificações, tags, etc.)), provavelmente você precisará usar o git filter-branch . Antes de usar o git filter-branch , você deve fazer uma cópia de backup de todo o seu repositório. Um arquivo tar simples de toda a sua árvore de trabalho (incluindo o diretório .git) é suficiente. O git filter-branch faz refs de backup, mas geralmente é mais fácil se recuperar de uma filtragem não muito correta apenas excluindo seu .gitdiretório e restaurando-o do seu backup.

Nota: Os exemplos abaixo usam o comando de nível inferior em git update-index --addvez de git add. Você poderia usar o git add , mas primeiro seria necessário copiar o arquivo de algum local externo para o caminho esperado ( --index-filterexecuta seu comando em um GIT_WORK_TREE temporário que está vazio).

Se você deseja que seu novo arquivo seja adicionado a cada confirmação existente, você pode fazer o seguinte:

new_file=$(git hash-object -w path/to/file)
git filter-branch \
  --index-filter \
    'git update-index --add --cacheinfo 100644 '"$new_file"' path/to/file' \
  --tag-name-filter cat \
  -- --all
git reset --hard

Não vejo realmente nenhuma razão para alterar as datas dos commits existentes --env-filter 'GIT_AUTHOR_DATE=…'. Se você o usasse, o tornaria condicional para que ele reescrevesse a data de cada confirmação.

Se você deseja que seu novo arquivo apareça apenas nas confirmações após alguma confirmação existente ("A"), faça o seguinte:

file_path=path/to/file
before_commit=$(git rev-parse --verify A)
file_blob=$(git hash-object -w "$file_path")
git filter-branch \
  --index-filter '

    if x=$(git rev-list -1 "$GIT_COMMIT" --not '"$before_commit"') &&
       test -n "$x"; then
         git update-index --add --cacheinfo 100644 '"$file_blob $file_path"'
    fi

  ' \
  --tag-name-filter cat \
  -- --all
git reset --hard

Se você deseja que o arquivo seja adicionado por meio de uma nova confirmação que deve ser inserida no meio do seu histórico, será necessário gerar a nova confirmação antes de usar o git filter-branch e adicionar --parent-filterao git filter-branch :

file_path=path/to/file
before_commit=$(git rev-parse --verify A)

git checkout master
git checkout "$before_commit"
git add "$file_path"
git commit --date='whenever'
new_commit=$(git rev-parse --verify HEAD)
file_blob=$(git rev-parse --verify HEAD:"$file_path")
git checkout -

git filter-branch \
  --parent-filter "sed -e s/$before_commit/$new_commit/g" \
  --index-filter '

    if x=$(git rev-list -1 "$GIT_COMMIT" --not '"$new_commit"') &&
       test -n "$x"; then
         git update-index --add --cacheinfo 100644 '"$file_blob $file_path"'
    fi

  ' \
  --tag-name-filter cat \
  -- --all
git reset --hard

Você também pode providenciar para que o arquivo seja adicionado pela primeira vez em uma nova confirmação de raiz: crie sua nova confirmação de raiz através do método “órfão” na seção git rebase (capture-a new_commit), use o incondicional --index-filtere --parent-filtersimilares "sed -e \"s/^$/-p $new_commit/\"".

Chris Johnsen
fonte
No primeiro exemplo do caso de uso, "Inserindo um arquivo em um histórico de várias referências": existe uma maneira de se --index-filteraplicar às confirmações retornadas por git rev-list? No momento, estou vendo o filtro de índice aplicado a um subconjunto de rev-list. Aprecie qualquer insight.
Hedgehog
@ Ouriço: Todos os exemplos de “multi-ref” usam -- --allpara processar todos os commits acessíveis a partir de qualquer ref. O último exemplo mostra como alterar apenas determinadas confirmações (basta testar GIT_COMMIT para o que você quiser). Para alterar apenas uma lista específica de confirmações, você pode salvar a lista antes de filtrar (por exemplo git rev-list … >/tmp/commits_to_rewrite) e testar a associação dentro do filtro (por exemplo if grep -qF "$GIT_COMMIT" /tmp/commits_to_rewrite; then git update-index …). O que exatamente você está tentando realizar? Você pode querer iniciar uma nova pergunta se for demais para explicar nos comentários.
Chris Johnsen
No meu caso, tenho um pequeno projeto que pretendia colocar sob controle de origem. (Eu não tinha feito isso, porque é um projeto solo e não havia decidido qual sistema usar). Meu "controle de origem" era apenas uma questão de duplicar toda a minha árvore de projeto em um novo diretório e renomear o diretório da versão anterior. Eu sou novo no Git (depois de usar o SCCS, RCS e um derivado RCS proprietário feio que lembra o CVS), então não se preocupe em falar comigo. Tenho a impressão de que a resposta envolve uma variação da seção "Inserir um arquivo em um único histórico". Corrigir?
21440 Steve Steve
1
@ Steve: O cenário “ref-única” se aplica se o seu histórico for de uma única linha de desenvolvimento (ou seja, faria sentido se todos os instantâneos fossem vistos como pontos sucessivos de um único ramo linear). O cenário "multi-ref" se aplica se você tem (ou teve) várias ramificações em seu histórico. A menos que seu histórico seja complicado (versões "bifurcadas" para diferentes clientes, mantinha uma ramificação "bugfix" enquanto novos trabalhos aconteciam em "desenvolvimento" etc.)), provavelmente você está olhando para uma situação de "referência única". No entanto , parece que sua situação difere da questão…
Chris Johnsen
1
@Steve: Como você tem uma série de instantâneos de diretório históricos - e ainda não possui um repositório Git -, provavelmente pode usar apenas os import-directories.perldo Git contrib/(ou import-tars.perl, ou import-zips.py…) para criar um novo repositório Git a partir dos seus instantâneos (mesmo com Timestamps "antigos"). As rebase/ filter-branchtécnicas na minha resposta são necessárias apenas se você deseja inserir um arquivo que foi "deixado de fora" do histórico de um repositório existente.
Chris Johnsen
119

Você pode criar a confirmação como de costume, mas, ao confirmar, defina as variáveis ​​de ambiente GIT_AUTHOR_DATEe GIT_COMMITTER_DATEas datas apropriadas.

Obviamente, isso fará o commit na ponta do seu ramo (ou seja, na frente do commit HEAD atual). Se você deseja empurrá-lo para mais longe no repositório, é preciso ter um pouco de fantasia. Digamos que você tenha esse histórico:

o--o--o--o--o

E você deseja que seu novo commit (marcado como "X") apareça em segundo :

o--X--o--o--o--o

A maneira mais fácil seria ramificar a partir do primeiro commit, adicionar seu novo commit e depois refazer todos os outros commit sobre o novo. Igual a:

$ git checkout -b new_commit $desired_parent_of_new_commit
$ git add new_file
$ GIT_AUTHOR_DATE='your date' GIT_COMMITTER_DATE='your date' git commit -m 'new (old) files'
$ git checkout master
$ git rebase new_commit
$ git branch -d new_commit
mipadi
fonte
25
Eu sempre encontro essa boa resposta, mas tenho que me apressar para encontrar o formato da data, então aqui está para a próxima vez 'Sex Jul 26 19:32:10 2013 -0400'
MeBigFatGuy
94
GIT_AUTHOR_DATE='Fri Jul 26 19:32:10 2013 -0400' GIT_COMMITTER_DATE='Fri Jul 26 19:32:10 2013 -0400' git commit
Xeoncross
15
Há mais, por exemplo, o bastante mnemônico 2013-07-26T19:32:10: kernel.org/pub/software/scm/git/docs/…
Marian
3
A propósito, a parte '-0400' especifica o deslocamento do fuso horário. Esteja ciente de selecioná-lo corretamente ... porque, caso contrário, seu tempo mudará com base nisso. Por exemplo, no meu caso, que moro em Chicago, tive que escolher '-0600' (CST da América do Norte). Você pode encontrar os códigos aqui: timeanddate.com/time/zones
evaldeslacasa
2
desde que a data precisa ser repetida, achei mais fácil criar outra variável: #THE_TIME='2019-03-30T8:20:00 -0500' GIT_AUTHOR_DATE=$THE_TIME GIT_COMMITTER_DATE=$THE_TIME git commit -m 'commit message here'
Frank Henard #
118

Eu sei que essa pergunta é bastante antiga, mas foi o que realmente funcionou para mim:

git commit --date="10 day ago" -m "Your commit message" 
Hyder B.
fonte
16
Isso funcionou perfeitamente. Surpreende-me que --datetambém suporte o formato de data relativa legível por humanos.
ZitRo 18/05
@ZitRo por que não 10 dias?
Alex78191
10
Aviso: git --datemodificará apenas $ GIT_AUTHOR_DATE ... portanto, dependendo das circunstâncias, você verá a data atual anexada ao commit ($ GIT_COMMITTER_DATE)
Guido U. Draheim
3
Marcando o que o @ GuidoU.Draheim disse. Você pode verificar os detalhes completos de um commit usando git show <commit-hash> --format=fuller. Lá, você verá AuthorDatea data especificada, mas a CommitDatedata real da confirmação.
d4nyll
Portanto, não há como alterar o CommitDate ou fazer um commit completamente datado? @ d4nyll
Rahul
35

No meu caso, com o tempo, salvei várias versões do myfile como myfile_bak, myfile_old, myfile_2010, backups / myfile etc. Eu queria colocar o histórico do myfile no git usando suas datas de modificação. Então, mudar o nome do mais antigo para myfile, git add myfilee, em seguida git commit --date=(modification date from ls -l) myfile, mudar o nome mais antigo em seguida para myfile, outro git commit com --date, repita ...

Para automatizar isso um pouco, você pode usar o shell-foo para obter o tempo de modificação do arquivo. Comecei com ls -le cut, mas o stat (1) é mais direto

git commit - data = "` stat -c% y meuarquivo `" meuarquivo

skierpage
fonte
1
Agradável. Certamente já tive arquivos anteriores aos meus dias de git para os quais queria usar o horário de modificação do arquivo. No entanto, isso não só definir a data commit (não a data autor?)
Xeoncross
Em git-scm.com/docs/git-commit: --date"Substitua a data do autor usada no commit." git logData parece ser o AuthorDate, git log --pretty=fullermostra AuthorDate e CommitDate.
Skierpage
16
A git commitopção --datemodificará apenas o GIT_AUTHOR_DATE, não GIT_COMMITTER_DATE. Como o Pro Git Book explica: "O autor é a pessoa que originalmente escreveu a obra, enquanto o autor é a pessoa que aplicou a obra pela última vez". No contexto de datas, GIT_AUTHOR_DATEé a data em que o arquivo foi alterado, enquanto GIT_COMMITTER_DATEa data em que foi confirmado. É importante observar aqui que, por padrão, git logexibe as datas do autor como "Data", mas depois usa as datas de confirmação para filtragem quando uma --sinceopção é fornecida .
Christopher
Não há opção -c para stat no OS X 10.11.1
thinsoldier
O equivalente stat -c %yno macOS (e outras variantes do BSD) é stat -f %m.
victor
21

O seguinte é o que eu uso para confirmar as alterações no foopara N=1dias no passado:

git add foo
git commit -m "Update foo"
git commit --amend --date="$(date -v-1d)"

Se você quer se comprometer com uma data ainda mais antiga, digamos, 3 dias de volta, basta alterar o dateargumento: date -v-3d.

Isso é realmente útil quando você esquece de cometer algo ontem, por exemplo.

UPDATE : --datetambém aceita expressões como --date "3 days ago"ou mesmo --date "yesterday". Portanto, podemos reduzi-lo a um comando de linha:

git add foo ; git commit --date "yesterday" -m "Update"
Vilson Vieira
fonte
6
Aviso: git --datemodificará apenas $ GIT_AUTHOR_DATE ... portanto, dependendo das circunstâncias, você verá a data atual anexada ao commit ($ GIT_COMMITTER_DATE)
Guido U. Draheim
misturar 2 abordagens faz um ajuste quase perfeito:git commit --amend --date="$(stat -c %y fileToCopyMTimeFrom)"
andrej 05/04
Impressionante! Mesmo comando com uma data legível por humanosgit commit --amend --date="$(date -R -d '2020-06-15 16:31')"
pixelbrackets 17/06
16

No meu caso, ao usar a opção --date, meu processo git travou. Pode ser que eu fiz algo terrível. E, como resultado, algum arquivo index.lock apareceu. Então, eu apaguei manualmente os arquivos .lock da pasta .git e os executei, para que todos os arquivos modificados fossem confirmados em datas passadas e funcionou dessa vez. Obrigado por todas as respostas aqui.

git commit --date="`date --date='2 day ago'`" -am "update"
JstRoRR
fonte
5
Veja o comentário acima emgit commit --date . Além disso, o exemplo mensagem de confirmação deve ser alterado para desencorajar mensagens de linha de um pobres como estes @JstRoRR
Christopher
4
Aviso: git --datemodificará apenas $ GIT_AUTHOR_DATE ... portanto, dependendo das circunstâncias, você verá a data atual anexada ao commit ($ GIT_COMMITTER_DATE)
Guido U. Draheim
9

Para fazer um commit que parece ter sido feito no passado, você deve definir ambos GIT_AUTHOR_DATEe GIT_COMMITTER_DATE:

GIT_AUTHOR_DATE=$(date -d'...') GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" git commit -m '...'

onde date -d'...'pode ser a data exata 2019-01-01 12:00:00ou a relativa 5 months ago 24 days ago.

Para ver as duas datas no log do git, use:

git log --pretty=fuller

Isso também funciona para confirmações de mesclagem:

GIT_AUTHOR_DATE=$(date -d'...') GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" git merge <branchname> --no-ff
mx0
fonte
2

Você sempre pode alterar uma data no seu computador, fazer uma confirmação e alterar a data de volta e pressionar.

Nik
fonte
1
Eu acho que essa não é a prática correta, ele pode criar alguns problemas desconhecidos
Vino