Como ler a saída do git diff?

A página man do git-diff é bastante longa e explica muitos casos que não parecem ser necessários para um iniciante. Por exemplo:

 git diff origin/master 

Vamos dar uma olhada no exemplo diff avançado do git history (em commit 1088261f no repository git.git ):

 diff --git a/builtin-http-fetch.cb/http-fetch.c similarity index 95% rename from builtin-http-fetch.c rename to http-fetch.c index f3e63d7..e8f44ba 100644 --- a/builtin-http-fetch.c +++ b/http-fetch.c @@ -1,8 +1,9 @@ #include "cache.h" #include "walker.h" -int cmd_http_fetch(int argc, const char **argv, const char *prefix) +int main(int argc, const char **argv) { + const char *prefix; struct walker *walker; int commits_on_stdin = 0; int commits; @@ -18,6 +19,8 @@ int cmd_http_fetch(int argc, const char **argv, const char *prefix) int get_verbosely = 0; int get_recover = 0; + prefix = setup_git_directory(); + git_config(git_default_config, NULL); while (arg < argc && argv[arg][0] == '-') { 

Vamos analisar este patch linha por linha.

  • A primeira linha

      diff --git a / builtin-http-fetch.cb / http-fetch.c 

    é um header "git diff" no formato diff --git a/file1 b/file2 . Os nomes de arquivo a a/ e b/ são os mesmos, a menos que renomear / copiar esteja envolvido (como no nosso caso). O --git significa que diff está no formato "git" diff.

  • Em seguida, há uma ou mais linhas de header estendidas. Os três primeiros

      índice de similaridade 95%
     renomear de builtin-http-fetch.c
     renomear para http-fetch.c 

    diga-nos que o arquivo foi renomeado de builtin-http-fetch.c para http-fetch.c e que esses dois arquivos são 95% idênticos (que foi usado para detectar essa renomeação).

    A última linha no header diferenciado estendido, que é

      índice f3e63d7..e8f44ba 100644 

    conte-nos sobre o modo de dado arquivo ( 100644 significa que é um arquivo comum e não ex. symlink, e que ele não tem bit de permissão executável), e sobre encurtado hash de preimage (a versão do arquivo antes de dada mudança) e postimage ( a versão do arquivo após a mudança). Esta linha é usada pelo git am --3way para tentar fazer uma mesclagem de 3 vias se o patch não puder ser aplicado.

  • Em seguida está o header diff unificado de duas linhas

      --- a / builtin-http-fetch.c
     +++ b / http-fetch.c 

    Comparado ao resultado diff -U ele não possui nomes de arquivos from-file-modification-time nem to-file-modification-time após origem (preimage) e destino (postimage). Se o arquivo foi criado, a fonte é /dev/null ; se o arquivo foi excluído, o destino é /dev/null .
    Se você definir a variável de configuração diff.mnemonicPrefix como true, no lugar de a/ e a/ b/ prefixos neste header de duas linhas, você poderá ter os prefixos c/ , i/ , w/ e o/ as, respectivamente, para o que você compara; veja git-config (1)

  • Em seguida vem um ou mais pedaços de diferenças; Cada pedaço mostra uma área onde os arquivos são diferentes. Pedaços de formato unificado começa com a linha como

      @@ -1,8 +1,9 @@ 

    ou

      @@ -18,6 +19,8 @@ int cmd_http_fetch (int arg, const char ** argv, ... 

    Está no formato @@ from-file-range to-file-range @@ [header] . O intervalo de arquivo está no formato -, e intervalo de arquivo é +, . Tanto a linha de largada como o número de linhas referem-se à posição e comprimento do pedaço na pré-imagem e pós-imagem, respectivamente. Se o número de linhas não mostrado, significa que é 0.

    O header opcional mostra a function C onde cada mudança ocorre, se é um arquivo C (como a opção -p no GNU diff), ou o equivalente, se houver, para outros tipos de arquivos.

  • Em seguida vem a descrição de onde os arquivos são diferentes. As linhas comuns aos dois arquivos começam com um caractere de espaço. As linhas que realmente diferem entre os dois arquivos têm um dos seguintes caracteres indicadores na coluna de impressão à esquerda:

    • '+' - Uma linha foi adicionada aqui ao primeiro arquivo.
    • '-' - Uma linha foi removida do primeiro arquivo.

    Então, por exemplo, primeiro pedaço

      #include "cache.h" #include "walker.h" -int cmd_http_fetch(int argc, const char **argv, const char *prefix) +int main(int argc, const char **argv) { + const char *prefix; struct walker *walker; int commits_on_stdin = 0; int commits; 

    significa que cmd_http_fetch foi substituído por main e esse const char *prefix; linha foi adicionada.

    Em outras palavras, antes da mudança, o fragment apropriado do arquivo 'builtin-http-fetch.c' era assim:

     #include "cache.h" #include "walker.h" int cmd_http_fetch(int argc, const char **argv, const char *prefix) { struct walker *walker; int commits_on_stdin = 0; int commits; 

    Após a mudança, este fragment do arquivo 'http-fetch.c' agora se parece com isto:

     #include "cache.h" #include "walker.h" int main(int argc, const char **argv) { const char *prefix; struct walker *walker; int commits_on_stdin = 0; int commits; 
  • Pode haver

      Nenhuma linha nova no final do arquivo 

    linha presente (não está no exemplo diff).

Como Donal Fellows disse que é melhor praticar a leitura de diferenças em exemplos da vida real, onde você sabe o que mudou.

Referências:

  • git-diff (1) manpage , section "Gerando correções com -p"
  • (diff.info) Nó unificado detalhado , "Descrição detalhada do formato unificado".

@@ -1,2 +3,4 @@ parte do diff

Essa parte me levou algum tempo para entender, então criei um exemplo mínimo.

O formato é basicamente o mesmo diff -u diff unificado.

Por exemplo:

 diff -u <(seq 16) <(seq 16 | grep -Ev '^(2|3|14|15)$') 

Aqui nós removemos as linhas 2, 3, 14 e 15. Saída:

 @@ -1,6 +1,4 @@ 1 -2 -3 4 5 6 @@ -11,6 +9,4 @@ 11 12 13 -14 -15 16 

@@ -1,6 +1,4 @@ significa:

  • -1,6 : esta peça corresponde à linha 1 a 6 do primeiro arquivo:

     1 2 3 4 5 6 

    - significa "antigo", como geralmente o invocamos como diff -u old new .

  • +1,4 diz que esta peça corresponde à linha 1 a 4 do segundo arquivo.

    + significa "novo".

    Nós só temos 4 linhas em vez de 6 porque 2 linhas foram removidas! O novo pedaço é justo:

     1 4 5 6 

@@ -11,6 +9,4 @@ para o segundo hunk é análogo:

  • no arquivo antigo, temos 6 linhas, começando na linha 11 do arquivo antigo:

     11 12 13 14 15 16 
  • No novo arquivo, temos 4 linhas, começando na linha 9 do novo arquivo:

     11 12 13 16 

    Note que a linha 11 é a 9ª linha do novo arquivo porque nós já removemos 2 linhas no último pedaço: 2 e 3.

Cabeçalho do pedaço

Dependendo da sua versão e configuração do git, você também pode obter uma linha de código ao lado da linha @@ , por exemplo, o func1() { in:

 @@ -4,7 +4,6 @@ func1() { 

Isso também pode ser obtido com o sinalizador -p do diff simples.

Exemplo: arquivo antigo:

 func1() { 1; 2; 3; 4; 5; 6; 7; 8; 9; } 

Se removermos a linha 6 , o diff mostra:

 @@ -4,7 +4,6 @@ func1() { 3; 4; 5; - 6; 7; 8; 9; 

Note que esta não é a linha correta para func1 : pulou as linhas 1 e 2 .

Este recurso impressionante frequentemente diz exatamente a qual function ou class cada pedaço pertence, o que é muito útil para interpretar o diff.

Como o algoritmo para escolher o header funciona exatamente é discutido em: De onde vem o trecho no header git diff hunk?

Aqui está o exemplo simples.

 diff --git a/file b/file index 10ff2df..84d4fa2 100644 --- a/file +++ b/file @@ -1,5 +1,5 @@ line1 line2 -this line will be deleted line4 line5 +this line is added 

Aqui está uma explicação (veja detalhes aqui ).

  • --git não é um comando, isso significa que é uma versão git do diff (não unix)
  • a/ b/ são diretórios, eles não são reais. é apenas uma conveniência quando lidamos com o mesmo arquivo (no meu caso a / está no índice eb / está no diretório de trabalho)
  • 10ff2df..84d4fa2 são IDs de 10ff2df..84d4fa2 desses 2 arquivos
  • 100644 são os “bits de modo”, indicando que este é um arquivo regular (não executável e não um link simbólico)
  • --- a/file +++ b/file menos sinais mostra linhas na / versão mas ausente da versão b /; e sinais de mais mostra linhas faltando em a / mas presentes em b / (no meu caso — significa linhas excluídas e +++ significa linhas adicionadas em b / e este o arquivo no diretório de trabalho)
  • @@ -1,5 +1,5 @@ para entender isso, é melhor trabalhar com um arquivo grande; Se você tiver duas mudanças em lugares diferentes, receberá duas inputs como @@ -1,5 +1,5 @@ ; suponha que você tem arquivo line1 … line100 e excluiu line10 e adicionar novo line100 – você terá:
 @@ -7,7 +7,6 @@ line6 line7 line8 line9 -this line10 to be deleted line11 line12 line13 @@ -98,3 +97,4 @@ line97 line98 line99 line100 +this is new line100 

O formato de saída padrão (que originalmente vem de um programa conhecido como diff se você quiser procurar mais informações) é conhecido como “diff unificado”. Ele contém essencialmente 4 tipos diferentes de linhas:

  • linhas de contexto, que começam com um único espaço,
  • linhas de inserção que mostram uma linha que foi inserida, que começa com um + ,
  • linhas de exclusão, que começam com um - e
  • linhas de metadados que descrevem coisas de nível mais alto, como quais arquivos estão falando, quais opções foram usadas para gerar o diff, se o arquivo mudou suas permissions, etc.

Eu aconselho que você pratique a leitura de diferenças entre duas versões de um arquivo, onde você sabe exatamente o que você mudou. Assim, você reconhecerá exatamente o que está acontecendo quando o vir.

No meu mac:

info diff então selecione: Output formats -> Context -> Unified format -> Detailed Unified :

Ou online man diff no gnu seguindo o mesmo caminho para a mesma seção:

Arquivo: diff.info, Nó: Detalhado Unificado, Próximo: Exemplo Unificado, Acima: Formato Unificado

Descrição detalhada do formato unificado ………………………………..

O formato de saída unificado começa com um header de duas linhas, que se parece com isto:

  --- FROM-FILE FROM-FILE-MODIFICATION-TIME +++ TO-FILE TO-FILE-MODIFICATION-TIME 

O registro de data e hora se parece com `2002-02-21 23: 30: 39.942229878 -0800 ‘para indicar a data, hora com segundos fracionários e fuso horário.

Você pode alterar o conteúdo do header com a opção `–label = LABEL ‘; veja * Nota Nomes Alternativos ::.

Em seguida vem um ou mais pedaços de diferenças; Cada pedaço mostra uma área onde os arquivos são diferentes. Nacos de formato unificado se parecem com isso:

  @@ FROM-FILE-RANGE TO-FILE-RANGE @@ LINE-FROM-EITHER-FILE LINE-FROM-EITHER-FILE... 

As linhas comuns aos dois arquivos começam com um caractere de espaço. As linhas que realmente diferem entre os dois arquivos têm um dos seguintes caracteres indicadores na coluna de impressão à esquerda:

`+ ‘Uma linha foi adicionada aqui ao primeiro arquivo.

`- ‘Uma linha foi removida do primeiro arquivo.

Não está claro a partir de sua pergunta que parte dos diffs você acha confuso: o diff de fato, ou o git de informações de header extra imprime. Apenas no caso, aqui está uma visão geral rápida do header.

A primeira linha é algo como diff --git a/path/to/file b/path/to/file – obviamente ele está apenas dizendo a você qual arquivo esta seção do diff é para. Se você definir o diff.mnemonic prefix variável de configuração booleana, b serão alterados para letras mais descritivas, como c w (tree de commit e work).

Em seguida, há “linhas de modo” – linhas que fornecem uma descrição das alterações que não envolvem a alteração do conteúdo do arquivo. Isso inclui arquivos novos / excluídos, arquivos renomeados / copiados e alterações de permissions.

Finalmente, há uma linha como o index 789bd4..0afb621 100644 . Você provavelmente nunca se importará com isso, mas esses números hexadecimais de 6 dígitos são os hashes SHA1 abreviados dos antigos e novos blobs para este arquivo (um blob é um object git que armazena dados brutos como o conteúdo de um arquivo). E, claro, o 100644 é o modo do arquivo – os últimos três dígitos são obviamente permissions; os três primeiros fornecem informações de metadados de arquivo extras ( postagem SO descrevendo isso ).

Depois disso, você está no padrão de saída unificada do diff (assim como o diff -U clássico diff -U ). Ele é dividido em pedaços – um pedaço é uma seção do arquivo que contém mudanças e seu contexto. Cada pedaço é precedido por um par de linhas --- e +++ denotando o arquivo em questão, então o diff real é (por padrão) três linhas de contexto em ambos os lados das linhas - e + mostrando as linhas removidas / adicionadas .

No version control, as diferenças entre duas versões são apresentadas no que é chamado de “diff” (ou, sinonimamente, um “patch”). Vamos dar uma olhada detalhada em tal diff – e aprender a lê-lo.

Olhe para a saída de um diff. Com base nessa saída, entenderemos a saída do git diff.

insira a descrição da imagem aqui

Arquivos Comparados a / b

Nosso diff compara dois itens um com o outro: item A e item B. Na maioria dos casos, A e B serão o mesmo arquivo, mas em versões diferentes. Embora não seja usado com muita frequência, um diff também pode comparar dois arquivos completamente diferentes entre si para mostrar como eles diferem. Para deixar claro o que é realmente comparado, uma saída diff sempre começa declarando quais arquivos são representados por “A” e “B”.

Metadados de arquivos

Os metadados do arquivo mostrados aqui são uma informação muito técnica que você provavelmente nunca precisará na prática. Os dois primeiros números representam os hashes (ou, simplesmente, “IDs”) dos nossos dois arquivos: o Git salva cada versão não apenas do projeto, mas também de cada arquivo como um object. Tal hash identifica um object de arquivo em uma revisão específica. O último número é um identificador de modo de arquivo interno (100644 é apenas um “arquivo normal”, enquanto 100755 especifica um arquivo executável e 120000 representa um link simbólico).

Marcadores para a / b

Mais abaixo na saída, as mudanças reais serão marcadas como vindas de A ou B. Para diferenciá-las, A e B recebem um símbolo: para a versão A, trata-se de um sinal de menos (“-“) e para a versão B, um sinal de mais (“+”) é usado.

Pedaço

Um diff não mostra o arquivo completo do começo ao fim: você não gostaria de ver tudo em um arquivo de 10.000 linhas, quando apenas 2 linhas foram alteradas. Em vez disso, mostra apenas as partes que foram realmente modificadas. Tal porção é chamada de “pedaço” (ou “pedaço”). Além das linhas reais alteradas, um pedaço também contém um pouco de “contexto”: algumas linhas (inalteradas) antes e depois da modificação para que você possa entender melhor em que contexto essa mudança aconteceu.

Cabeçalho do Pedaço

Cada um desses pedaços é prefixado por um header. Fechado em dois sinais “@” cada, o Git informa quais linhas foram afetadas. No nosso caso, as seguintes linhas são representadas no primeiro trecho:

  • Do arquivo A (representado por um “-“), 6 linhas são extraídas, começando da linha no. 34

  • A partir do arquivo B (representado por um “+”), são exibidas 8 linhas, iniciando também a partir da linha no. 34

O texto após o par de fechamento de “@@” tem como objective esclarecer o contexto, novamente: o Git tenta exibir um nome de método ou outra informação contextual de onde esse fragment foi retirado no arquivo. No entanto, isso depende muito da linguagem de programação e não funciona em todos os cenários.

Alterar

Cada linha alterada é precedida por um símbolo “+” ou “-“. Como explicado, esses símbolos ajudam você a entender exatamente como as versões A e B são exibidas: uma linha precedida por um sinal “-” vem de A, enquanto uma linha com um sinal “+” vem de B. Na maioria dos casos, Git pega A e B de tal forma que você pode pensar em A / – como conteúdo “antigo” e B / + como conteúdo “novo”.

Vamos dar uma olhada no nosso exemplo:

  • Alterar # 1 contém duas linhas prefixadas com um “+”. Como nenhuma contrapartida em A existia para essas linhas (nenhuma linha com “-“), isso significa que essas linhas foram adicionadas.

  • A mudança # 2 é exatamente o oposto: em A, temos duas linhas marcadas com sinais “-“. No entanto, B não tem um equivalente (sem linhas “+”), o que significa que eles foram excluídos.

  • Na alteração 3, finalmente, algumas linhas foram realmente modificadas: as duas linhas “-” foram alteradas para se parecer com as duas linhas “+” abaixo.

Fonte