Seções iMasters
Desenvolvimento

Dicas Git da semana: Trees e Commits

As dicas Git desta semana abordam o armazenamento git tree e o armazenamento de git commit.

Trees

Na semana passada, vimos como o Git armazena objetos no repositório local. Agora, vamos ver a forma como eles correspondem a diretórios, ou trees.

O Git usa um modelo de armazenamento uniforme para todos os seus objetos. Cada objeto é identificado com o seu hash, mas o tipo do objeto é armazenado em metadados juntamente com o objeto. Assim, é possível descobrir a partir de um ID qual é o seu tipo, assim como o seu conteúdo:

(master) $ # Note: objects from previous
(master) $ git cat-file -t e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
blob
(master) $ git cat-file -p e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
(master) $ git cat-file -t 8ab686eafeb1f44702738c8b0f24f2567c36da6d
blob
(master) $ git cat-file -p 8ab686eafeb1f44702738c8b0f24f2567c36da6d
Hello, World!

Como esses objetos são empacotados, para que você possa obtê-los em seu diretório de trabalho? Bem, os blobs são organizados em trees, o que corresponde aos diretórios em uma estrutura de diretório. Se tivermos um diretório com um arquivo chamado empty, podemos imprimir o seu conteúdo:

(master) $ git ls-tree master .
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 empty

Observe que isso não é listar o conteúdo no disco, mas sim mostrar a você o Git de exibição da pasta. Isso nos permite listar itens em ramos diferentes (ou tags) sem a necessidade de verificá-los primeiro e, na verdade, é como funcionam a hospedagem de sites como GitHub e as ferramentas como gitweb. O master está simplesmente pedindo para nos mostrar o branch com o mesmo nome.

O que acontece se adicionarmos outro arquivo, com o mesmo conteúdo?

(master) $ cp empty anotherEmpty
(master) $ git add anotherEmpty
(master) $ git commit -a
[master ca5fc4f] Another empty
0 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 anotherEmpty
(master) $ git ls-tree master .
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 anotherEmpty
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 empty

Temos uma nova entrada na tree, mas o pointer blob é exatamente o mesmo objeto, como um hard link em um sistema de arquivos UNIX. Tal como acontece com um hard link, se mudarmos um dos objetos, não alteramos o conteúdo; em vez disso, criamos uma nova cópia (desde que tenha um hash diferente) e a tree é atualizada para apontar para isso.

Então como que essa tree é armazenada no repositório? Bem, acontece que é outro tipo de objeto, armazenado no mesmo mecanismo como blobs. Você pode descobrir a tree a partir de um commit (ou branch) com o sufixo ^{tree}:

(master) $ git cat-file -t HEAD^{tree}
tree
(master) $ git cat-file -p HEAD^{tree}
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 anotherEmpty
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 empty
(master) $ git rev-parse HEAD^{tree}
2b61e34a91ca9780ea2f943e72f1a4a022cdd206

A tree representa um diretório, contendo uma mistura de blobs e trees. Podemos descobrir que ela resolve usar git rev-parse, para determinar que essa tree é um objeto com o hash 2b61e34….

Como é que essa tree é criada? Bem, mais uma vez, é um objeto bem formatado que é numerado de acordo com o sha1. O tipo de objeto é tree, e em vez de ter os valores simples, como o blob, a tree é um conjunto de valores de índices apontando para os objetos, juntamente com um modo (tipicamente 100644 para arquivos e 100755 para diretórios). No entanto, sabemos o tamanho do hash SHA, por isso não precisa ser em números legíveis, podemos serializá-lo como bytes. O comprimento fica em 28 bytes por linha, além de muitos bytes que estão no nome do arquivo. No nosso caso, temos 28 + “anotherEmpty” length () + 28 + “vazio” length (), ou 73 bytes no total:

(master) $ echo -en "tree 73\x00?
100644 anotherEmpty?
\x00?
\xe6\x9d\xe2\x9b\xb2\xd1\xd6\x43\x4b\x8b?
\x29\xae\x77\x5a\xd8\xc2\xe4\x8c\x53\x91?
100644 empty?
\x00?
\xe6\x9d\xe2\x9b\xb2\xd1\xd6\x43\x4b\x8b?
\x29\xae\x77\x5a\xd8\xc2\xe4\x8c\x53\x91?
" | shasum
2b61e34a91ca9780ea2f943e72f1a4a022cdd206 -

Criar uma tree, por outro lado, é um pouco mais complicado. Para resolver esse problema, o comando git mktree existe, o qual pode levar um fluxo git ls-tree formatado, e gerar um objeto tree para você. É um pouco como o git hash de objetos acima, mas sem ter que converter as referências a partir do hash string para uma sequência de caracteres hexadecimais. Além disso, ele garante que o conteúdo da tree seja adequadamente classificado, que é um pré-requisito obrigatório (a fim de apoiar a recuperação rápida).

(master) $ git ls-tree master .
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 anotherEmpty
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 empty
(master) $ git ls-tree master . | git mktree
2b61e34a91ca9780ea2f943e72f1a4a022cdd206

Isso nos permite facilmente criar uma nova tree, com um novo arquivo nela:

(master) $ echo -en? "
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391\tanotherEmpty\n?
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391\tempty\n?
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391\tvoid\n" | git mktree
d2d6bbd1c25c154fcbb045d66e8a6f9b83587a68

Temos agora três arquivos em uma tree (todos os mesmos conteúdos; tudo vazio), mas agora podemos nos referir à nova tree diretamente. Podemos até listá-la novamente:

(master) $ git ls-tree d2d6bbd1c25c154fcbb045d66e8a6f9b83587a68
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 anotherEmpty
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 empty
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 void

Embora não tenhamos mostrado aqui, se você quiser criar uma tree com outras trees (em vez de blobs), elas funcionarão exatamente da mesma maneira; a diferença é que a palavra “blob” é substituída por “tree” e, naturalmente, o objeto tem que apontar para o hash correto.

Commits

Depois de ver a maneira como as trees são armazenadas em Git (e na semana anterior como os objetos são armazenados em Git), agora vamos ver como eles são ligados a commits, que são a base de branches, tags e afins. Aqui está um exemplo de commit:

(master) $ git cat-file -p HEAD
tree 2b61e34a91ca9780ea2f943e72f1a4a022cdd206
parent f44c95384463187acd83ff418ddd9c48659db8dd
author Alex Blewitt <alex.blewitt@gmail.com> 1314178977 +0100
committer Alex Blewitt <alex.blewitt@gmail.com> 1314178977 +0100

Another empty
(master) $ git rev-parse HEAD
ca5fc4f022595972639331adcab40d810b9882a0

Não é uma surpresa que um commit é um objeto de hash, armazenado exatamente nos mesmos mecanismos em que blobs e trees estão. Um commit é um hash da mensagem de commit, com um tipo de identificação e comprimento (como para blobs e trees). Nesse caso, a mensagem de commit é de 236 bytes, portanto, nós escrevemos commit 236\0 seguido do conteúdo, e mostramos o hash:

(master) $ (echo -en "commit 236\0"; git cat-file -p HEAD) | shasum
ca5fc4f022595972639331adcab40d810b9882a0 -
(master) $ # Or, we can use this to find the size automatically:
(master) $ (echo -en "commit $((`git cat-file -p HEAD | wc -c`))\0"; ?
git cat-file -p HEAD) | shasum
ca5fc4f022595972639331adcab40d810b9882a0 -

Assim, dado esse conhecimento, podemos criar um novo commit. Tudo o que precisamos fazer é referenciar a uma tree (como d2d6bbd1c25c154fcbb045d66e8a6f9b83587a68 da última vez), referenciar à HEAD como o pai, e adicionar algumas informações de timestamp.

(master) $ # TIMENOW=`date +%s`
(master) $ TIMENOW=1314385772
(master) $ echo -en "tree d2d6bbd1c25c154fcbb045d66e8a6f9b83587a68\n?
parent ca5fc4f022595972639331adcab40d810b9882a0\n?
author Alex Blewitt <alex.blewitt@gmail.com> $TIMENOW +0100\n?
committer Alex Blewitt <alex.blewitt@gmail.com> $TIMENOW +0100\n?
\n?
Manually generated commit" | git hash-object -w --stdin -t commit
195751d8f0822325eb3f234de9c0e720ae53d8ff

Nós criamos o nosso primeiro (gerado manualmente) commit, e ele aponta para a tree da última vez. Uma vez que tudo já está bem, devemos ser capazes de verificar este commit:

(master) $ git checkout 195751d8f0822325eb3f234de9c0e720ae53d8ff
Note: checking out '195751d8f0822325eb3f234de9c0e720ae53d8ff'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

git checkout -b new_branch_name

HEAD is now at 195751d... Manually generated commit
((195751d...)) $ ls
anotherEmpty empty void
((195751d...)) apple[bar] $ git diff HEAD^
diff --git a/void b/void
new file mode 100644
index 0000000..e69de29

Isso representa a tree comitada que escrevemos da última vez. Podemos até fazer diffs entre a versão anterior para descobrir que o novo arquivo é na verdade o void que adicionamos anteriormente.

Agora que temos a capacidade de criar nossos próprios commits, podemos dar uma olhada mais profunda na estrutura de armazenamento do Git da próxima vez.

?

Textos originais disponíveis em http://alblue.bandlem.com/2011/08/git-tip-of-week-trees.html e http://alblue.bandlem.com/2011/09/git-tip-of-week-commits.html

Mensagem do anunciante:

Torne-se um Parceiro de Software Intel®. Filie-se ao Intel® Developer Zone. Intel®Developer Zone

Qual a sua opinião?