[Git] É possível commitar apenas parte das alterações?
Este post é mais um da série sobre Git que estou escrevendo. Os anteriores são:
Esta é uma dica rápida, que já usei algumas vezes. Suponha que vc fez várias alterações diferentes nos seus arquivos, mas nem todas estarão no próximo commit. Ou seja, vc precisa escolher quais partes irão e quais ficarão de fora.
Até daria para salvar tudo em um branch à parte (ou fazer um stash), em seguida desfazer as alterações que não irão no commit, e depois restaurá-las. Mas para que tudo isso, se o próprio git add
já fornece um meio mais simples de resolver?
Supondo que tenho este arquivo no meu repositório (já devidamente commitado):
abc
def
Então eu adiciono algumas linhas nele:
abc
def
nova linha 1
nova linha 2
nova linha 3
nova linha 4
nova linha 5
Mas eu só quero que as linhas 1, 4 e 5 estejam no próximo commit. As linhas 2 e 3 devem estar em um commit futuro, mas eu não quero remover para depois adicioná-las de volta. Em um caso mais realista, suponha que em vez de linhas, eu fiz alterações mais complicadas que deram bastante trabalho para finalizar. Eu não quero ter que ficar removendo e pondo de volta longos trechos.
Neste caso, basta usar a opção -p
(ou --patch
) do git add
.
Por exemplo, se eu rodar git add -p arquivo
, aparecerá algo assim:
git add -p arquivo
diff --git a/arquivo b/arquivo
index 5f5521f..1f45ec2 100644
--- a/arquivo
+++ b/arquivo
@@ -1,2 +1,7 @@
abc
def
+nova linha 1
+nova linha 2
+nova linha 3
+nova linha 4
+nova linha 5
(1/1) Stage this hunk [y,n,q,a,d,e,?]?
Ele mostra as linhas adicionadas (indicando isso com um +
no início), e pergunta o que fazer. Você pode digitar ?
e ENTER para mais detalhes, e vai aparecer algo assim:
y - stage this hunk
n - do not stage this hunk
q - quit; do not stage this hunk or any of the remaining ones
a - stage this hunk and all later hunks in the file
d - do not stage this hunk or any of the later hunks in the file
e - manually edit the current hunk
? - print help
O Git chama todo o trecho modificado de hunk (literalmente um "pedaço", mas vou usar o nome em inglês daqui pra frente).
Veja que você pode optar por adicionar tudo (y
), mas aí ele vai adicionar as 5 linhas. Também pode ignorar com n
, e aí ele vai para o próximo hunk - nesse caso só tem um, mas poderia ter mais (inclusive, se você rodar apenas git add -p
, ele iria para o próximo arquivo, por exemplo). Também pode encerrar tudo com q
.
Mas no caso queremos escolher quais modificações queremos enviar, então a opção a ser escolhida é e
(editar manualmente o hunk). Ao digitar e
e ENTER, ele abre o editor com o seguinte:
# Manual hunk edit mode -- see bottom for a quick guide.
@@ -1,2 +1,7 @@
abc
def
+nova linha 1
+nova linha 2
+nova linha 3
+nova linha 4
+nova linha 5
# ---
# To remove '-' lines, make them ' ' lines (context).
# To remove '+' lines, delete them.
# Lines starting with # will be removed.
# If the patch applies cleanly, the edited hunk will immediately be marked for staging.
# If it does not apply cleanly, you will be given an opportunity to
# edit again. If all lines of the hunk are removed, then the edit is
# aborted and the hunk is left unchanged.
Repare que para remover linhas que foram adicionadas (as que começam com +
), basta apagá-las (To remove '+' lines, delete them). No caso, eu quero que apenas as linhas 1, 4 e 5 estejam no próximo commit, então basta apagar as linhas 2 e 3, ficando assim:
# Manual hunk edit mode -- see bottom for a quick guide.
@@ -1,2 +1,7 @@
abc
def
+nova linha 1
+nova linha 4
+nova linha 5
Repare também que nas duas primeiras linhas tem um espaço no início. Não remove este espaço: ele foi adicionado para mostrar que esta linha não foi afetada e continua fazendo parte do arquivo.
Basta salvar e pronto. Se não tiverem mais hunks a serem analisados, o comando se encerra. Caso tenha, seja no mesmo arquivo, ou em outro arquivo se você fez apenas git add -p
(ou ainda git add -p arquivo1 arquivo2 etc
), ele mostra o próximo hunk e as mesmas opções já citadas acima (adicionar, pular, editar, etc).
Agora se rodarmos git status
, veremos algo interessante:
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: arquivo
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: arquivo
O mesmo arquivo está listado para ir no próximo commit ("Changes to be committed"), mas ele também consta como "not staged for commit". Isso porque tem trechos que irão no próximo commit (no caso, as linhas 1, 4 e 5) e trechos que não irão (as linhas 2 e 3).
Podemos ver isso com git diff
e git diff --cached
(a opção --cached
mostra o primeiro caso):
# diferença entre o último commit e o que foi adicionado (e portanto irá no próximo commit)
$ git diff --cached
diff --git a/arquivo b/arquivo
index 5f5521f..2de37b9 100644
--- a/arquivo
+++ b/arquivo
@@ -1,2 +1,5 @@
abc
def
+nova linha 1
+nova linha 4
+nova linha 5
# diferença entre o que não foi adicionado e o que irá no próximo commit
$ git diff
diff --git a/arquivo b/arquivo
index 2de37b9..1f45ec2 100644
--- a/arquivo
+++ b/arquivo
@@ -1,5 +1,7 @@
abc
def
nova linha 1
+nova linha 2
+nova linha 3
nova linha 4
nova linha 5
A seguir, basta fazer o commit, e apenas as linhas 1, 4 e 5 estarão nele:
$ git commit -m"apenas linhas 1, 4 e 5"
Mas vale lembrar que as linhas 2 e 3 continuam no arquivo:
# arquivo continua listado como modificado (mas ainda não foi adicionado para ir no próximo commit)
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: arquivo
no changes added to commit (use "git add" and/or "git commit -a")
# A diferença para o último commit são as linhas 2 e 3
$ git diff
diff --git a/arquivo b/arquivo
index 2de37b9..1f45ec2 100644
--- a/arquivo
+++ b/arquivo
@@ -1,5 +1,7 @@
abc
def
nova linha 1
+nova linha 2
+nova linha 3
nova linha 4
nova linha 5
Ou seja, vc ainda pode decidir enviá-las em um commit futuro (ou continuar editando o arquivo, etc).
Adaptado da minha resposta no Stack Overflow.