Entendendo git rev-list

Oct 16 2020

Enquanto procurava exemplos de git hook, me deparei com a seguinte postagem: https://github.com/Movidone/git-hooks/blob/master/pre-receive e eu queria entender o seguinte comando:

git rev-list $new_list --not --all 

onde new_list é obtida de:

NULL_SHA1="0000000000000000000000000000000000000000" # 40 0's
new_list=
any_deleted=false
while read oldsha newsha refname; do
    case $oldsha,$newsha in *,$NULL_SHA1) # it's a delete
            any_deleted=true;;
        $NULL_SHA1,*) # it's a create new_list="$new_list $newsha";; *,*) # it's an update new_list="$new_list $newsha";;
    esac
done

Achei que rev-list mostra commits em ordem cronológica reversa.

Mas, alguém pode compartilhar uma ideia mais clara sobre o que -note -allopções são destinados para?

Conforme a documentação:

--not
Reverses the meaning of the ^ prefix (or lack thereof) for all following revision specifiers, up to the next --not.
--all
Pretend as if all the refs in refs/ are listed on the command line as <commit>. 

Não consigo entender completamente essas opções.

[Atualizar] Depois de fazer alguns commits de teste, percebi que se eu não usar as opções --note --allentão, git rev-listlista todos os commits no branch e não o que pretendo fazer push.

Porém, queria entender por que não imprime os valores de sha no terminal quando a --allopção é passada?

Respostas

4 torek Oct 17 2020 at 13:18

O git rev-listcomando é um comando muito complicado e muito central no Git, pois o que ele faz é percorrer o gráfico . A palavra gráfico aqui se refere ao próprio gráfico de commits e, em alguns casos, ao próximo nível abaixo (objetos Git acessíveis a partir de commits).

Achei que rev-list mostra commits em ordem cronológica reversa.

Não exatamente, mas perto:

  • O pedido pode ser alterado. O padrão é cronológico reverso.
  • O padrão é percorrer alguns commits, mas você pode rev-listir mais fundo para incluir objetos de árvore e blob e até objetos de tag. Isso é para programas como git fetche git push(que invocam git pack-objects) e git pack-objects. Pretendo ignorar totalmente essa possibilidade aqui, mas sinto que deveria pelo menos mencioná-la. 😀

Portanto, o padrão é listar alguns commits em ordem cronológica reversa. É importante, e um pouco complicado, especificar exatamente quais partes do gráfico iremos git rev-listpercorrer: algumas em alguns commits .

Mas, alguém pode compartilhar uma ideia mais clara sobre o que --note --allopções são destinados para?

Como observa VonC , o efeito aqui é listar commits que são novos para o repositório receptor. Isso depende do fato de que este git rev-listcomando está sendo executado em um gancho de pré-recepção . Geralmente, ele não faz nada de útil fora desse gancho específico. Portanto, como você pode ver, o ambiente de tempo de execução de um gancho, no Git, costuma ser um pouco especial. (Isso é verdade para mais do que apenas o gancho de pré-recebimento: é preciso pensar sobre o contexto de ativação de cada gancho.)

Mais sobre --not --all

A --allopção faz exatamente o que você citou na documentação:

Finja que todos os refs refs/estão listados na linha de comando ...

Portanto, isso faz o equivalente a a git for-each-ref refs: ele faz um loop sobre cada referência. Isso inclui nomes do ramo ( masterou main, develop, feature/talle assim por diante, todos os quais estão realmente em refs/heads/), nomes de marca ( v1.2que é realmente refs/tags/v1.2), nomes remota de rastreamento ( origin/developque é realmente refs/remotes/origin/develop), refs substituição (em refs/replace/), o estoque ( refs/stash), bisection refs, Gerrit refs se você estiver usando Gerrit e assim por diante. Observe que ele não faz um loop sobre as entradas de reflog.

O --notprefixo é uma operação booleana simples. Na sintaxe do gitrevisions - veja a documentação do gitrevisions - podemos escrever coisas como develop, o quedevelop significa que eu digo a você para começar e trabalhar para trás e incluir esses commits , mas também coisas como ^develop, o quedevelop significa que eu digo a você para começar e trabalhar para trás e excluir esses commits . Então, se eu escrever:

git rev-list feature1 feature2 ^main

Estou pedindo ao Git para percorrer os commits acessíveis a partir dos commits identificados pelos nomes feature1e feature2, mas para excluir os commits alcançáveis ​​dos commits identificados por main. Para (muito) mais sobre a ideia geral de alcançabilidade e gráfico de caminhada, consulte Think Like (a) Git .

O --notoperador efetivamente vira o ^em cada ref:

git rev-list --not feature1 feature2 ^main

é uma abreviatura, por assim dizer, para:

git rev-list ^feature1 ^feature2 main

Isso percorre a lista de commits acessíveis de main, mas exclui aqueles alcançáveis ​​de feature1ou feature2.

Normalmente, todos os commits podem ser encontrados com--all

Se você estiver usando o Git da maneira normal do dia a dia, e não tiver um "HEAD separado" no momento - o modo HEAD separado não é exatamente anormal, mas não é a maneira normal de trabalhar - a --allopção de git rev-listdiz a ele para incluir todos os commits , porque todos os commits são acessíveis a partir de todas as referências. 1 Portanto, --not --allefetivamente exclui todos os commits. Portanto, adicionar --not --alla qualquer um git rev-listque de outra forma listaria alguns commits tem o efeito de inibir a lista. A saída está vazia: por que nos incomodamos?

Se você está no modo HEAD desanexado e fez vários novos commits - isso pode acontecer quando você está no meio de um rebase interativo ou conflitante, por exemplo - então git rev-list HEAD --not --alllistaria aqueles commits que são acessíveis, HEADmas não de qualquer nome de branch. Nesse rebase, por exemplo, seriam apenas aqueles commits que você copiou até agora.

Portanto, o modo "desanexado HEAD" seria um lugar onde git rev-list --not --allpoderia ser útil na linha de comando. Mas para a situação que você está examinando - um gancho de pré-recebimento - não estamos realmente na linha de comando.

Ganchos de pré-recebimento

Quando alguém costuma git pushenviar commits para seu próprio Git, seu Git:

  • configura uma área de quarentena para conter quaisquer novos objetos (novos commits e blobs e assim por diante); 1
  • negocia com o remetente para decidir o que o remetente deve enviar;
  • recebe esses objetos; e
  • obtém uma lista de solicitações de atualização de referência . Essas solicitações de atualização basicamente dizem que esse nome contém este ID de hash . 2

Antes de realmente fazer qualquer uma das atualizações solicitadas, seu Git:

  1. Alimenta a lista inteira para o gancho de pré-recebimento. Esse gancho pode dizer "não"; em caso afirmativo, todo o push, como um todo, é rejeitado.
  2. Se disser "ok", alimenta a lista, uma solicitação de cada vez, para o gancho de atualização. Quando aquele gancho diz "ok", faz a atualização. Se o gancho disser "não", seu Git rejeita a atualização, mas passa a examinar outras.
  3. Depois que todas as atualizações são aceitas ou rejeitadas na etapa 2, alimenta a lista de aceitos para o gancho pós-recebimento.

Os objetos necessários, que foram adicionados a alguma referência na etapa 2, são movidos da quarentena para o banco de dados de objetos do Git. Aqueles que foram rejeitados, não.

Agora, pense em um típico git push. Recebemos alguns novos commit (s) e uma solicitação: cria um novo nome de branchfeature/short , ou obtemos alguns novos commit (s) e uma solicitação: atualiza o nome do branch existente developpara incluir esses novos commits, junto com os antigos .

Na etapa 1 acima, temos um único novo hash ID. Executamos um loop para ler todos os nomes de ref e seus IDs de hash atuais e novos propostos, e o loop foi executado apenas uma vez, porque apenas um nome estava sendo git pushalterado. Esse hash ID refere-se ao novo commit ou commits, que serão adicionados a este branch existente ou serão a dica e outros commits exclusivos para o novo branch.

Agora gostaríamos de inspecionar esses commits, e não qualquer um dos commits existentes que podem ser acessados ​​de qualquer branch existente. Para simplificar, ao invés $new_listda minha outra resposta, vamos supor que nós apenas um novo hash ID, $newe o antigo hash ID para o nome do branch,: $oldall-zeros se o branch for totalmente novo, ou algum commit existente válido se for um nome de filial existente.

Se os novos commits estiverem em um branch completamente novo, então:

git rev-list $new ^master ^develop ^feature/short ^feature/tall

cobriríamos eles, por exemplo, se soubéssemos que os únicos branches existentes são esses quatro (e que não há tags, etc. com que nos preocupar). Mas e se eles estiverem sendo adicionados, digamos , develop? Então nós gostaríamos de excluir os commits que estão atualmente em develop. Podemos usar o $oldhash ID para fazer isso:

git rev-list $new ^master ^$old ^feature/short ^feature/tall

Isso listaria novamente apenas os novos commits que quem quer que esteja executando git push origin developdeseja adicionar ao nosso develop.

Mas pense $old. Este é um ID de hash. Onde Git conseguiu isso? Git obteve esse hash ID do nome develop . Este é um gancho de pré-recepção ; o nome develop ainda não foi atualizado . Portanto, o nome develop é um nome para o ID de hash antigo $old. Que significa:

git rev-list $new ^master ^develop ^feature/short ^feature/tall

vai também fazer o trabalho.

Se git rev-list $newseguido por "and not all existing" fará o trabalho, então:

git rev-list $new --not --branches

fará o trabalho. Isso é quase o que temos aqui.

O bug de apenas usar --branchesé que ele não recebe nenhuma tag ou outras referências. Poderíamos usar --not --branches --tagsmas --not --allé mais curto e também recebe todos os outros refs.

Então é daí que --not --allvem: depende do caso especial de um gancho de pré-recepção. Listamos os novos hash IDs, conforme proposto por quem está executando um git push, que nosso Git nos passou como uma lista de linhas. Temos git rev-listandar o gráfico proposto-a-ser-atualizados cometer, olhando para as novas submissões na área de quarentena, mas excluindo todos os commits que já estão em nosso repositório. O comando rev-list produz esses IDs de hash, um por linha, que lemos em um loop de shell e fazemos o que quisermos para inspecionar cada confirmação.


1 A área de quarentena era nova no Git 2.11. Antes disso, novos objetos podem permanecer no repositório por um tempo, mesmo se o push for rejeitado. A área de quarentena não é realmente um grande problema para a maioria das pessoas, mas para grandes servidores como o GitHub, pode economizar muito espaço em disco.

2 A solicitação pode ser forçada ou não forçada e, se forçada, pode ser forçada ou não. Essas informações não estão disponíveis no gancho de pré-recebimento (nem no gancho de atualização), que é, hum, vamos apenas dizer que não é tão bom , mas há problemas de compatibilidade ao adicioná-lo. É tudo habitável, principalmente, no entanto. O gancho pode dizer se é criar um novo ref ou excluir a solicitação de ref existente, porque se for assim, um dos dois IDs de hash - antigo ou novo - será o "hash nulo" de todos os zeros (que é reservado; nenhum ID de hash é permitido para serem todos zeros).

5 VonC Oct 17 2020 at 01:59

Isso significa:

  • Listar commits que podem ser acessados ​​seguindo os links pai dos commits dados, aqui $new_list, os novos, modificados ou deletados
  • mas exclua commits que são alcançáveis ​​daquele (s) dado (s) com um ^na frente deles, aqui " all ", isto é, todos os commits HEADS, ou commits marcados.

Isso limita a lista de rev apenas aos novos commits recebidos, e não a todos os commits (recebidos e já presentes no repositório de recebimento)