É possível mover / mudar o nome dos ficheiros no Git e manter o seu histórico?
Eu gostaria de mudar o nome/mover uma sub-árvore de projecto no Git, mudando-a de
/project/xyz
a
/components/xyz
Se eu usar um simples {[[2]}, então todo o histórico de commit para o xyz project
se perde. Existe uma maneira de mover isso de tal forma que a história seja mantida?
9 answers
O Git detecta renomeações em vez de persistir a operação com o commit, por isso não importa se você usa git mv
ou mv
.
O comando log leva um argumento --follow
que continua a história antes de uma operação de mudança de nome, isto é, procura por conteúdo semelhante usando as heurísticas:
Http://git-scm.com/docs/git-log
Para procurar o histórico completo, use o seguinte comando:
git log --follow ./path/to/file
É possível mudar o nome de um ficheiro e manter o histórico intacto, embora Faça com que o ficheiro seja renomeado ao longo de toda a história do repositório. Isto é provavelmente apenas para os obsessivos amantes de git-log, e tem algumas implicações sérias, incluindo estas:
- Você pode estar reescrevendo uma história compartilhada, que é o mais importante Não enquanto usa Git. Se alguém clonou o repositório, você vai quebrá-lo fazendo isso. Terão de voltar a clonar evitar dores de cabeça. Isto pode ser OK se o nome for importante o suficiente, mas você vai precisar considerar isso cuidadosamente -- você pode acabar perturbando uma comunidade inteira de opensource!
- se referenciou o ficheiro usando o seu nome antigo mais cedo no histórico do repositório, está efectivamente a quebrar versões anteriores. Para remediar isto, vais ter de fazer mais saltos de arco. Não é impossível, apenas tedioso e possivelmente não vale a pena.
filter-tree
!
Assume que vais mover um ficheiro old
para uma pasta dir
e dá-lhe o nome new
git mv old dir/new && git add -u dir/new
, mas isso quebra a história.
Em vez disso:
git filter-branch --tree-filter 'if [ -f old ]; then mkdir dir && mv old dir/new; fi' HEAD
Will refazer {[[9]} cada commit no ramo, executando o comando nos carrapatos para cada iteração. Muitas coisas podem correr mal quando fazes isto. Eu normalmente teste para ver se o arquivo está presente (caso contrário, ainda não está lá para se mover) e, em seguida, executar os passos necessários para Engraxar a árvore ao meu gosto. Aqui você pode procurar através de arquivos para alterar as referências ao arquivo e assim por diante. Diverte-te! :)
Quando concluído, o ficheiro é movido e o registo está intacto. Sentes-te como um pirata ninja.
Também; a pasta mkdir só é necessária se mover o ficheiro para uma nova pasta, claro. O SE irá evitar a criação desta pasta mais cedo no histórico do que o seu ficheiro existir.
A resposta curta é Não , não é possível mudar o nome de um ficheiro no Git e recordar a história. E é uma dor.
Há rumores de quegit log --follow
--find-copies-harder
vai funcionar, mas não funciona para mim, mesmo que haja zero alterações no conteúdo do arquivo, e os movimentos foram feitos com git mv
.
(inicialmente usei o Eclipse para mudar o nome e actualizar os pacotes numa única operação, o que pode ter confundido o git. Mas isso é muito comum. uma coisa a fazer. --follow
parece funcionar se apenas um mv
for realizado e então um commit
e o mv
não for muito longe.)
É realmente irritanteque tantas pessoas tenham repetido sem pensar a afirmação de que o git segue automaticamente os movimentos. Eles fizeram-me perder tempo. O Git não coisa. por design (!) Git não segue movimentos de todo.
A minha solução é mudar o nome dos ficheiros para os locais originais. Mude o software para caber no controle de fonte. Com o git, parece que precisas de agir correctamente da primeira vez. Infelizmente, isso quebra o Eclipse, que parece usar--follow
.git log --follow
Às vezes não mostra o histórico completo de arquivos com histórias de renomeação complicadas, mesmo que
git log
o. (Eu não sei porquê.)
[13] (Há alguns hacks muito inteligentes que voltam e recomeçam o trabalho antigo, mas eles são bastante assustadores. Ver GitHub-Gist: emiller / git-mv-with-history.)
git log --follow [file]
Vou mostrar-te a história através de novos nomes.
Eu faço:
git mv {old} {new}
git add -u {new}
Objectivo
- Usar
git am
(inspirado Smar, emprestado de Exherbo) - adicionar o histórico de commit dos ficheiros copiados/movidos
- de um directório para outro
- ou de um repositório para outro
Limitação
- As Marcas e os ramos não são mantidos
- o histórico é cortado na mudança de nome do ficheiro path (mudança de nome da pasta)
Resumo
-
Extracto histórico no formato de E-mail a usar
- reorganizar a árvore de ficheiros e actualizar os nomes dos ficheiros
- adicionar um novo histórico usando
cat extracted-history | git am --committer-date-is-author-date
git log --pretty=email -p --reverse --full-index --binary
1. Extrair o histórico no formato de E-mail
Exemplo: extracto histórico defile3
, file4
e file5
my_repo
├── dirA
│ ├── file1
│ └── file2
├── dirB ^
│ ├── subdir | To be moved
│ │ ├── file3 | with history
│ │ └── file4 |
│ └── file5 v
└── dirC
├── file6
└── file7
Definir / limpar o destino
export historydir=/tmp/mail/dir # Absolute path
rm -rf "$historydir" # Caution when cleaning the folder
Extrair o histórico de cada ficheiro em formato de E-mail
cd my_repo/dirB
find -name .git -prune -o -type d -o -exec bash -c 'mkdir -p "$historydir/${0%/*}" && git log --pretty=email -p --stat --reverse --full-index --binary -- "$0" > "$historydir/$0"' {} ';'
Infelizmente a opção --follow
ou --find-copies-harder
Não pode ser combinada com --reverse
. É por isso que a história é cortada quando o ficheiro é renomeado (ou quando uma pasta-mãe é renomeada).
Histórico temporário em formato de E-mail:
/tmp/mail/dir
├── subdir
│ ├── file3
│ └── file4
└── file5
O Dan Bonachea sugere inverter os loops do comando git log generation neste primeiro passo: em vez de executar o git log uma vez por ficheiro, executá-lo exactamente uma vez com uma lista de ficheiros na linha de comandos e gerar um único registo unificado. Desta forma, commits que modificam vários arquivos permanecem um único commit no resultado, e todos os commits novos manter o seu ordem relativa original. Note que isso também requer mudanças no segundo passo abaixo quando reescrever nomes de arquivos no (agora unificado) log.
2. Reorganizar a árvore de ficheiros e actualizar os nomes dos ficheiros
Suponha que você quer mover estes três arquivos neste outro repo (pode ser o mesmo repo).
my_other_repo
├── dirF
│ ├── file55
│ └── file56
├── dirB # New tree
│ ├── dirB1 # from subdir
│ │ ├── file33 # from file3
│ │ └── file44 # from file4
│ └── dirB2 # new dir
│ └── file5 # from file5
└── dirH
└── file77
Por isso reorganize os seus ficheiros.
cd /tmp/mail/dir
mkdir -p dirB/dirB1
mv subdir/file3 dirB/dirB1/file33
mv subdir/file4 dirB/dirB1/file44
mkdir -p dirB/dirB2
mv file5 dirB/dirB2
A sua história temporária é agora:
/tmp/mail/dir
└── dirB
├── dirB1
│ ├── file33
│ └── file44
└── dirB2
└── file5
Muda também os nomes dos ficheiros dentro do histórico:
cd "$historydir"
find * -type f -exec bash -c 'sed "/^diff --git a\|^--- a\|^+++ b/s:\( [ab]\)/[^ ]*:\1/$0:g" -i "$0"' {} ';'
3. Aplicar nova história
O teu outro acordo é:
my_other_repo
├── dirF
│ ├── file55
│ └── file56
└── dirH
└── file77
Aplicar autorizações a partir de ficheiros históricos temporários:
cd my_other_repo
find "$historydir" -type f -exec cat {} + | git am --committer-date-is-author-date
--committer-date-is-author-date
preserva os selos de tempo originais do commit ([83]}O Comentário de Dan Bonachea ).
O teu outro acordo é agora:
my_other_repo
├── dirF
│ ├── file55
│ └── file56
├── dirB
│ ├── dirB1
│ │ ├── file33
│ │ └── file44
│ └── dirB2
│ └── file5
└── dirH
└── file77
Use git status
para ver a quantidade de autorizações prontas a serem empurradas: -)
Truque Extra: verificar ficheiros renomeados / movidos dentro do seu repo
Para listar os ficheiros que foram renomeados:
find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow {} ';' | grep '=>'
Mais personalizações:poderá completar o comando git log
usando as opções --find-copies-harder
ou --reverse
. Você também pode remover as duas primeiras colunas usando cut -f3-
e limpar o padrão completo' {.* => .*}'.
find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow --find-copies-harder --reverse {} ';' | cut -f3- | grep '{.* => .*}'
Para um dado git log
use a opção-M:
Git log-p-M
com uma versão atual do git.
Isto funciona para outros comandos como git diff
também.
Há um custo no uso do cpu sempre que você pedir ao git para descobrir onde os arquivos foram renomeados, então se você usá-lo ou não, e quando, depende de você.
Se quiser ter sempre o seu histórico relatado com a detecção de novos nomes num repositório em particular, pode usar:
Git config comparacao.mudar o nome de 1
Os ficheiros que se deslocam de uma pasta para outra são detectados. Aqui está um exemplo:
commit c3ee8dfb01e357eba1ab18003be1490a46325992
Author: John S. Gruber <[email protected]>
Date: Wed Feb 22 22:20:19 2017 -0500
test rename again
diff --git a/yyy/power.py b/zzz/power.py
similarity index 100%
rename from yyy/power.py
rename to zzz/power.py
commit ae181377154eca800832087500c258a20c95d1c3
Author: John S. Gruber <[email protected]>
Date: Wed Feb 22 22:19:17 2017 -0500
rename test
diff --git a/power.py b/yyy/power.py
similarity index 100%
rename from power.py
rename to yyy/power.py
Por favor, note que isto funciona sempre que estiver a usar diff, não apenas com git log
. Por exemplo:
$ git diff HEAD c3ee8df
diff --git a/power.py b/zzz/power.py
similarity index 100%
rename from power.py
rename to zzz/power.py
Como um teste que eu fiz uma pequena alteração em um arquivo em um ramo de funcionalidade e compromete-lo e, em seguida, no branch master eu renomeei o arquivo, comprometida e, em seguida, fez uma pequena alteração em outra parte do arquivo e comprometida que. Quando fui para a secção de recursos e fundiu-se a partir do master o merge renomeou o arquivo e fundiu as alterações. Aqui está o resultado da junção:
$ git merge -v master
Auto-merging single
Merge made by the 'recursive' strategy.
one => single | 4 ++++
1 file changed, 4 insertions(+)
rename one => single (67%)
O resultado foi um diretório de trabalho com o arquivo renomeado e ambas as alterações de texto feitas. Então é possível para o git fazer a coisa certa apesar do fato de que ele não rastreia explicitamente renomeações.
Esta é uma resposta tardia a uma pergunta antiga para que as outras respostas possam ter sido corretas para a versão git na época.
Eu gostaria de mudar o nome/mover uma sub-árvore de projecto no Git, mudando-a de
/project/xyz
A
/ componentes / xyz
Se eu usar um plain
git mv project components
, Então todo o histórico de commit para o projetoxyz
fica perdido.
Não (8 anos depois, Git 2.19, Q3 2018), porque o Git irá detectar a mudança de nome do directório, e isto está agora melhor documentado.
Ver comitologia b00bf1c, Comissão 1634688, Comissão 0661e49, cometer 4d34dff, cometer 983f464, cometer c840e1a, cometer 9929430 (27 de Junho de 2018), e cometer d4e8062, cometer 5dacd4a (25 de Junho de 2018) por Elias Newren (newren
).
(Intercalado por Junio C Hamano -- gitster
-- em cometer 0ce5a69, 24 Jul 2018)
Isso é agora explicado em Documentation/technical/directory-rename-detection.txt
:
Exemplo:
Mas são muitos outros casos, como:Quando todos
x/a
,x/b
ex/c
mudaram-se paraz/a
,z/b
ez/c
, é provável quex/d
adicionado entretanto também queira mudar paraz/d
por tendo em conta a sugestão de que o directório completo "x
"foi transferido para"z
".
Um lado da história muda o nome
x -> z
, e o outro muda o nome de algum ficheiro parax/e
, causando a necessidade da junção fazer uma mudança de nome transitiva.
Para simplificar a detecção da mudança de nome das pastas, essas regras são aplicadas por Git:
Algumas regras básicas limitam quando aplica-se a mudança de nome da pasta:
- se um determinado directório ainda existir em ambos os lados de uma junção, não consideramos que tenha sido renomeado.
- se um subconjunto de ficheiros a mudar de nome tem um ficheiro ou directório no caminho (ou estaria no caminho um do outro), "desligue" a mudança de nome do directório para esses sub-caminhos específicos e comunique o conflito ao utilizador.
- se o outro lado da história fez um a pasta muda o nome para uma localização que o seu lado do histórico mudou de nome, ignorando depois essa mudança de nome do outro lado do histórico para qualquer mudança de nome implícito da pasta (mas avisando o utilizador).
Você pode ver muito de testes em t/t6043-merge-rename-directories.sh
, o que também indica que:
- a) se mudar o nome de dividir uma pasta em duas ou mais outras, a pasta com o maior número de nomes, "wins".
- b) evitar o directório-mudar o nome-detecção para um path, se esse caminho for a fonte de um novo nome em ambos os lados de uma junção.
- c) aplicar apenas as renomeações implícitas das pastas se o outro lado da história é aquele que faz a mudança de nome.
Eu faço mover os arquivos e depois fazer
git add -A
Que colocam na área de satélite Todos os ficheiros apagados / novos. Aqui git percebe que o arquivo é movido.
git commit -m "my message"
git push
Não sei porquê, mas isto funciona comigo.