Executando verificação de segurança...
32

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

Carregando publicação patrocinada...
4

Cara!! Legal demais isso!!

Quem nunca teve aquela situação que você empolga demais no código e acaba fazendo modificações de contextos totalmente diferentes? Eu faço isso sempre, rs.

Tá aí uma coisa que eu sempre quis, mas também procastinei em pesquisar sobre para ver se existia.

Muito obrigado! Agora eu vou fazer meus commits ficarem mais autoexplicativos 😎

3

Que legal, eu não sabia que dava para fazer isso manualmente. Eu faço algo semelhante, mas uso a IDE. No IntelliJ, com o arquivo alterado, ao apertar Ctrl + D, abre uma janela onde você pode escolher o que vai incluir no commit.

image

3

Bom artigo.

O VSCode tem uma funcionalidade que ajuda a fazer isso também, ao selecionar o código e abrir o menu de contexto você pode fazer stage só de uma parte, ir juntando tudo o que você quer e ele faz o stage pra você.

Aí você pode criar um commit.

Por baixo dos panos ele está fazendo exatamente o que diz no artigo.

2

Boa dica @kht, uma outra forma bem parecida é utilizar o git add no modo interativo, com git add -i, ele libera algumas opções a mais, uma delas também é utilizar o path para adicionar apenas parte das alterações.

1
1

No VSCode é rapidão. Só selecionar as linhas que quer fazer o stage e escolher "Stage selected range". Dá pra fazer por blocos de várias linhas de uma vez também, e é mais visual. Então vai bem mais rápido e é muito mais fácil do que ir pela linha de comando.

1