A parábola do Git
Resolvi fazer uma tradução livre do texto The Git Parable do Tom Preston-Werner.
Boa leitura....
A parábola do Git
Git é um sistema simples mas extremamente poderoso.
A maioria das pessoas tentam ensinar Git pela demonstração de alguns comandos chegando a um "tadaaaaa.", acredito que este método seja ineficiente. Esta abordagem vai ensiná-lo a usar Git para realizar tarefas simples, mas os comandos irão sempre parecer encantamentos mágicos.
Fazer qualquer coisa além do trivial será aterrorizador. Enquanto não compreender os conceitos nos quais o Git é feito você se sentirá perdido.
A seguinte parábola vai levá-lo através da criação de um sistema similar ao Git a partir de sua base. Entender os conceitos apresentados aqui é a melhor preparação para alcançar o poder total do Git.
Os conceitos em si são bastante simples, mas criam uma imensa gama de funcionalidades. Leia esta parábola do começo ao fim e deverá ter poucos problemas para tornar-se um mestre nos vários comandos do Git, liberando o maravilhoso poder que o mesmo disponibiliza.
A parábola
Imagine que você tem um computador que não tem nada além de um editor de textos e alguns comandos de sistema de arquivos.
Agora, imagine que você decidiu escrever um grande programa neste sistema. Como você é um desenvolvedor responsável, decidiu que precisa inventar alguma forma de manter o registro das versões de seu programa de forma que possa recuperar códigos que tenham sido alterados ou excluídos.
O que segue é uma história sobre como você pode arquitetar tal sistema de controle de versão (VCS) e a razão por traz das escolhas da arquitetura.
Fotocópias ( Snapshots )
Alfredo é um amigo seu que trabalha no supermercado como fotógrafo em uma boutique tipo "Momentos Especiais".
Fica o dia inteiro tirando fotos de garotinhos com poses esquisitas com um fundo de selva ou oceano.
Durante um de seus freqüentes almoços no stande de pretzels, Alfredo lhe conta uma história sobre uma mulher de nome Hazel que traz sua filha
para tirar um retrato todo ano no mesmo dia. "Ela traz todas as fotos dos anos anteriores junto" lhe conta o Alfredo.
"Ela gosta de recordar como sua filha era em cada fase diferente, como se aquelas fotocópias pudessem realmente fazê-la mover o tempo até o instante daquelas memórias."
Repentinamente, a colocação inocente do Alfredo age como um catalisador para você enxergar um solução ideal para seu dilema de controle de versão.
Fotocópias ou snapshots, como pontos de salvamento em um vide-o game, são realmente o que lhe importa quando precisa interagir com um sistema de controle de versão.
E se você pudesse tirar fotocópias do seu código a qualquer momento e ressuscitar esse código quando quizese?
Alfredo percebe o sentimento de realização espalhado por todo seu rosto e sabe que você está a ponto de deixá-lo sem dizer uma palavra e vai implementar qualquer idéia genial que ele acabou de lhe causar. Você não o desaponta.
Você começa seu projeto em um diretório chamado working. Conforme você programa, tenta implementar uma característica por vez.
Quando completa uma parte auto-contida da característica, certifica-se que todos os arquivos foram salvos e faz então uma cópia do diretório de trabalho, dando a ele o nome snapshot-0. Depois de efetuar a cópia, garante que ela nunca será alterada.
Após a próxima parte do trabalho, você faz outra cópia, mas desta vez o novo diretório é denominado snapshot-1, e assim por diante.
Para recordar quais mudanças você fez em cada snapshot, você adiciona um arquivo especial chamado mensagem a cada diretório de snapshot que contém
um resumo do trabalho efetuado e a data de realização. Verificando o conteúdo de cada arquivo mensagem fica fácil de encontrar uma mudança específica feita no passado, caso você tenha que ressuscitar algum código antigo.
Galhos ( Branches )
Após algum tempo no projeto, uma versão candidata à publicação começa a surgir. Madrugadas no teclado finalmente geraram o snapshot-99, a forma
nascente do que será a Versão de Publicação 1.0. Então esta cópia é empacotada e distribuída às massas sedentas que aguardam.
Tocado pela excelente resposta ao seu software, você se esforça, determinado a fazer com que a próxima versão seja um sucesso ainda maior.
Seu VCS até então tem sido um companheiro a prova de falhas.
Versões antigas de seu código estão lá e quando você precisa delas podem ser encontradas facilmente. Mas, não muito após sua publicação, relatórios
de erro começam a chegar. Ninguém é perfeito, você afirma a si mesmo, e o snapshot-99 está pronto para ser recuperado, orgulhoso por ser trazido
de volta para serem aplicadas as correções dos problemas.
Desde a publicação, você criou outros 10 snapshots. Este novo trabalho não deve ser incluído na versão correção de erros 1.0.1 que você vai criar.
Para resolver isso, você copia o snapshot-99 para working, deixando seu diretório de trabalho exatamente no ponto onde a versão 1.0 foi publicada.
Algumas linhas de código são tratadas e o problema está corrigido.
É neste momento que um problema torna-se aparente. o VCS trata muito bem com desenvolvimento linear, mas pela primeira vez, você precisa criar
um novo snapshot que não é descendente direto do último. Se você criar o snapshot-110 ( lembre-se que você já criou 10 cópias desde a publicação),
vai interromper o fluxo linear e não terá como determinar o ancestral da cópia. Claramente você precisa de algo mais poderoso que este sistema linear.
Estudos demonstram que mesmo pequenas exposições à natureza podem ajudar a recarregar o potencial criativo da mente.
Você esteve atrás da luz polarizada artificialmente de seu monitor por dias. Uma caminhada através das árvores sob o ar de outono vão lhe fazer bem
e com alguma sorte, ajudá-lo a encontrar uma solução ideal para seu problema.
Os grandes carvalhos que margeiam a trilha sempre lhe agradaram. Eles parecem repousar rígidos e orgulhosos do perfeito céu azul.
Metade das folhas avermelhadas caíram de seus galhos, deixando um intrincado padrão de galhos como rastro.
Fixando em um dos milhares de tipos de galhos você pacientemente tenta seguí-lo de volta até o solitário tronco.
Esta estrutura organicamente produzida permite esta grande complexidade, mas as regras para encontrar o caminho de volta para o tronco são tão
simples e perfeitas para manter o caminho de múltiplas linhas de desenvolvimento!
Vem a tona que é verdadeiro o que dizem a respeito da natureza e criatividade.
Observando seu histórico de desenvolvimento como uma árvore, resolver o problema de ancestralidade torna-se trivial.
Tudo que você precisa é incluir o nome da snapshot pai no arquivo de mensagem que você escreve para cada snapshot.
Adicionando apenas um único ponteiro para o antecessor possibilitará, fácil e precisamente, traçar o histórico de um snapshot, todo o caminho
de volta até sua raiz.
Nomes de Galhos ( Branch Names )
Seu histórico de código é agora uma árvore. Ao invés de ter apenas um único último snapshot, você tem duas: uma para cada galho ou 'branch'.
Com um sistema linear, seu sistema de numeração seqüencial permite facilmente identificar o último snapshot. Agora esta habilidade foi perdida.
Criar novos branchs de desenvolvimento tornou-se tão simples que você vai tirar vantagem disso o tempo todo.
Você criará branchs para fixar as antigas publicações, para experimentos que podem não serem concluídos; de fato torna-se possível criar um novo
branch para cada nova característica iniciada!
Mas como tudo bom na vida, existe um preço a ser pago. Cada vez que você cria um snapshot, você precisa lembrar que ele se torna o último no
seu próprio branch. Sem esta informação, alternar para um novo branch seria um trabalho extremamente penoso.
Toda vez que cria um novo branch você provavelmente da uma nome para ele em sua cabeça. Você provavelmente diz: "Este será o branch de manutenção da Versão 1.0".
Talvez prefira referir ao formal branch de desenvolvimento linear como o branch "mestre" ou "master".
Pense nisso um pouco mais profundamente. Da perspectiva de uma árvore, o que significa dar nome a um galho?
Nomear cada snapshot que aparecer no histórico de um branch resolveria seu problema, mas iria requirir o armazenamento de uma potencialmente grande
quantia de dados. Adicionalmente, isso ainda não ajudaria a eficientemente encontrar o último snapshot de um branch.
A única informação necessária para identificar um branch é a localização do último snapshot daquele branch.
Se precisar saber a lista de snapshots que são parte de um branch pode facilmente traçar pelo parentesco.
Guardar o nome do branch é trivial. Em um arquivo chamado branchs, armazenado fora de qualquer snapshot específico, você simplesmente
lista os pares nome/snapshot que representaram os branchs. Para alternar para um branch nomeado precisa apenas procurar pelo snapshot
correspondente neste arquivo de referencia.
Como você está armazenando apenas o último snapshot de cada branch, criar um novo snapshot contêm um passo adicional.
Se seu novo snapshot estiver sendo criado como parte de um branch, o arquivo de branchs precisa ser atualizado para que o nome do mesmo fique
associado com o último snapshot. Um pequeno preço a pagar pelo benefício.
Etiquetas ( Tags )
Após utilizar branchs por um tempo, você percebe que eles podem servir para dois propósitos. Primeiro, ele podem atuar com ponteiros de snapshots
para que você possa manter o rastro dos branchs. Segundo, podem apontar para um snapshot específica e nunca mudar.
O primeiro caso permite manter o desenvolvimento em execução, coisas como "Manutenção de Publicação". O segundo caso é útil para marcar pontos de
interesse, tais como "Versão 1.0" ou "Versão 1.1".
Misturar ambos os usos em apenas um arquivo parece bagunçado. Ambos os tipos são ponteiros para snapshots mas um muda e outro não.
Para clarificar e manter a solução elegante, você decide criar outro arquivo chamado etiquetas (tags) para manter ponteiros do segundo tipo.
Manter estes ponteiros, dois parentes comuns, em arquivos separados ira ajudá-lo a não tratar acidentalmente um branch como tag e vice-versa.
Distribuído ( Distributed )
Trabalhar por si mesmo acaba tornando-se bastante solitário. Não seria legal se pude-se convidar um amigo para trabalhar em seu projeto com você?
Bem, você está com sorte. Sua amiga Zoe tem um sistema de computador como o seu e quer ajudar no projeto. Como você criou um ótimo sistema de
controle de versão, acaba contando a ela sobre o mesmo e envia uma cópia de todos seus snapshots, branchs e tags para que ela possa aproveitar
dos mesmos benefícios do histórico de versões.
É ótimo ter Zoe no time mas ela tem o hábito de fazer longas viagens para lugares distantes e sem acesso à internet.
Assim que ela pegou o código fonte, saiu em um vôo para a Patagônia e você não ouviu falar dela por uma semana.
Neste meio tempo, ambos programaram uma tempestade. Quando ela finalmente voltou, você descobriu uma falha critica no seu VCS.
Como ambos estavam utilizando o mesmo sistema de numeração, cada um possui diretórios chamados 'snapshot-114', 'snapshot-115' e assim por diante,
mas com diferentes conteúdos.
Para piorar as coisas, você nem sabe quem produziu as alterações daquelas novas fotocópias. Juntos, concebem então um plano para tratar destes problemas.
Primeiro, as mensagens dos snapshots irão obrigatoriamente conter o nome do autor e seu e-mail. Segundo, snapshots não serão mais nomeadas por
simples números. No lugar disto, vocês utilizarão o conteúdo do arquivo de mensagem para produzir uma cifra ou hash. Este hash será garantidamente única
para o snapshot já que duas mensagens nunca terão a mesma data, mensagem, antecessor e autor.
Para fazer as coisas correrem suavemente, ambos concordam em utilizar o algoritmo de hash SHA1 que pega o conteúdo de um arquivo e produz uma
palavra de 40 caracteres hexadecimais. Ambos atualizam seus históricos com a nova técnica no lugar dos colisivos diretórios 'snapshot-114' e agora
possuem diretórios distintos nomeados ‘8ba3441b6b89cad23387ee875f2ae55069291f4b’ e ‘db9ecb5b5a6294a8733503ab57577db96ff2249e’.
Com a atualização do esquema de nomenclatura, torna-se trivial recuperar todas as cópias do computador de Zoe e colocá-las no seu repositório.
Como cada cópia especifica sua antecessora, quando possuem mensagens idênticas ( e também cópias idênticas ) possuirão os mesmos nomes não importa
onde são criados, o histórico da base de código pode continuar a ser mantido como uma árvore. A diferença é que a árvore é construída por snapshots
criadas por Zoe e você.
Este ponto é importante o bastante para garantir repetição. Uma cópia é identificada pelo SHA1 que a identifica unicamente ( e seu antecessor ).
Estes snapshots podem ser criadas e movidas entre computadores sem perder sua identidade ou onde pertencem no histórico da árvore do projeto.
Ainda mais, cópias podem ser compartilhadas ou mantidas privadas de acordo com sua vontade. Se você possuir alguns snapshots experimentais
que deseja manter para si mesmo, pode fazê-lo facilmente. Apenas não as torne disponíveis para Zoe!
Desconectado ( Offline )
O hábito de viajar de Zoe a faz passar incontáveis horas em aviões e barcos. A maior parte dos lugares que visita não possui acesso à internet disponível.
No fim do dia, ela passou mais tempo desconectada que conectada.
Não é surpresa, no entanto, que Zoe adora seu VCS. As operações do dia a dia que ela precisa fazer podem ser efetuadas localmente. O único momento
que ela precisa de uma conexão de rede é quando está pronta para compartilhar seus snapshots com você.
Junções ( Merges )
Antes de Zoe sair para sua viagem, você pediu-a para trabalhar em um branch chamado 'math' e para implementar uma função que gera números primos.
Neste meio tempo, você esteve também esteve desenvolvendo algo no branch 'math', só que esteve trabalhando na função que gera números mágicos;
Agora Zoe retornou e você está encarando a tarefa de unir estes dois diferentes branchs de desenvolvimento em uma único snapshot.
Como ambos trabalharam em tarefas distintas, a junção é simples. Enquanto construía a mensagem do snapshot para a junção, você percebe que esta
snapshot é especial. No lugar de ter apenas um antecessor, esta junção, ou merge, possui dois! O primeiro é a sua última snapshot no branch 'math'
e o segundo é o branch 'math' de Zoe. O snapshot de junção contêm todas as mudanças além daquelas necessárias para unir os dois antecessores em uma
única base de código;
Completado o merge, Zoe pega todos os snapshots que você possui e ela não, o que inclui seu desenvolvimento no branch 'math' e sua snapshot de merge.
Uma vez que ela fez isso, ambos os históricos casam perfeitamente!
Reescrevendo o Histórico ( Rewriting History )
Como muitos desenvolvedores de software, você tem a compulsão de manter seu código limpo e muito bem organizado. Isso leva ao desejo de manter seu
histórico de código com esmero. Noite passada você chegou após tomar alguns goles de Guinness em um bar e começou a programar, produzindo um monte
de snapshots no caminho. Hoje de manhã, uma revisão da produção da noite passada fez você se assustar um pouco. No geral, o código está bom, mas
você teve uma série de enganos que foram corrigidos nos snapshots posteriores.
Vamos dizer que a branch na qual você realizou o desenvolvimento bêbado é chamada 'drunk' e você fez três snapshots após ter chegado do bar.
Se o nome 'drunk' aponta para o último snapshot neste branch, então você pode utilizar uma notação útil para referenciar o antecessor deste snapshot.
A notação 'drunk^' significa o pai do snapshot apontada pelo branch 'drunk'. Similarmente 'drunk^^' significa o avô do snapshot 'drunk'.
Assim os snapshots em ordem cronológica são 'drunk^^', 'drunk^' e 'drunk'.
Você realmente gostaria que estes três snapshots fossem dois snapshots limpos. Uma que muda uma função existente, e uma que adiciona um novo arquivo.
Para realizar esta revisão do histórico você copia 'drunk' para working e exclui o arquivo que é novo na série. Agora working representa as
modificações corretas na função existente. Você pode criar um novo snapshot de working e escrever a mensagem apropriada para as mudanças.
Como antecessor você especifica o SHA1 do snapshot 'drunk^^^', essencialmente criando um novo branch do mesmo snapshot da noite passada.
Agora pode copiar 'drunk' para working e executar um novo snapshot com a adição daquele novo arquivo. Como antecessor você especifica aquele
snapshot que acabamos de criar antes deste.
Como último passo, você muda o branch 'drunk' e aponta para o último snapshot que acabou de fazer.
O histórico do branch 'drunk' agora representa uma versão melhor daquilo que fez na noite passada. Os outros snapshots que você substituiu não
são mais necessários e podem ser excluídos ou deixados para a posteridade. Nenhum nome da branch está apontando para eles portanto será difícil
de encontrá-los depois, mas se você não os excluir estarão lá.
Colocando no palco ( Staging Area )
Mesmo tentando manter suas modificações relacionadas a uma única característica ou pedaço lógico, algumas vezes você precisa começar a trabalhar
em algo totalmente não relacionado. No meio do caminho percebe que seu diretório de trabalho contêm algo que deveria ser separado em dois
snapshots discretos.
Para ajudá-lo com essa situação chata, o conceito área do palco é útil. Esta área atua como um intermediário entre seu trabalho e o snapshot
final. Cada vez que você termina um snapshot, você também copia aquele snapshot para o diretório do palco.
Agora, toda vez que terminar uma edição de um novo arquivo, criar um novo arquivo, ou remover um arquivo, você decide quando esta mudança deve
ser parte de seu próximo snapshot. Caso deseje, você replica a mudança dentro do palco. Senão, pode deixá-la e aplicar em um snapshot futuro.
De agora em diante, snapshot são criados diretamente do diretório do palco.
Esta separação entre programar e preparar o palco torna fácil especificar o que é e o que não é incluído no próximo snapshot.
Você não precisa mais se preocupar muito sobre causar uma mudança acidental não relacionada no seu diretório de trabalho.
No entanto, você precisa ser um pouco cuidadoso. Considere um arquivo de nome LEIAME. Você edita este arquivo e então replica o mesmo no palco.
Você continua com o que tem que fazer, editando outros arquivos. Depois de um tempinho, você faz outra alteração no LEIAME. Agora você tem duas
alterações neste arquivo, mas apenas uma está no palco! Se criar um snapshot agora, sua segunda alteração estará ausente.
A lição nisso: cada nova edição precisa ser adicionada ao palco se é para ser parte do próximo snapshot.
Diferenças ( Diffs )
Com um diretório de trabalho, um palco e um monte de snapshots armazenados, começa a ficar confuso notar quais diferenças existem entre eles. Uma mensagem de snapshot apenas fornece um resumo do que mudou, não exatamente quais linhas foram mudadas entre dois arquivos.
Utilizando um algoritmo de diferenciação, você pode implementar um pequeno programa que mostra as diferenças em duas bases de código.
Conforme for desenvolvendo e copiando coisas do seu diretório de trabalho para o palco, tudo que vai realmente querer é uma maneira fácil de visualizar as diferenças entre os dois, assim pode determinar o que mais precisa ser colocado no palco. É também importante notar como o palco está diferente do último snapshot, uma vez que estas mudanças são o que irá se tornar parte do próximo snapshot que produzir.
Existem muitas outras diferenças que você pode querer ver. Entre um snapshot e seu antecessor iria mostrar um conjunto de mudanças que foram introduzidas pelo snapshot. A diferenciação entre dois branchs poderia ser um auxílio para garantir que seu desenvolvimento não vagou muito distante da linha principal.
Eliminando Duplicação ( Eliminating Duplication )
Depois de algumas viagens para a Namibia, Istanbul e Galapagos, Zoe começa a reclamar que seu disco rígido está cheio com milhares de arquivos com
cópias praticamente iguais do software. Você também tem sentido que todas aquelas duplicações são desperdícios.
Depois de pensar um pouco, você chega a uma solução bastante inteligente.
Você recorda que aquele hash SHA1 produz uma palavra curta que é única para o conteúdo de um arquivo. Iniciando com o primeiríssimo snapshot no
histórico do projeto, você começa o processo de conversão. Primeiro, você cria um diretório chamado objetos fora do histórico de código. Depois, encontra o mais profundo diretório aninhando em um snapshot. Adicionalmente, você abre um arquivo temporário para escrita.
Para cada arquivo neste diretório você executa três passos.
Passo 1: Calcular o SHA1 de seu conteúdo.
Passo 2: Adicionar uma entrada no arquivo temporário que contêm a palavra blob ( grande objeto binário ), o SHA1 do Passo 1 e o nome do arquivo.
Passo 3: Copia o arquivo para o diretório de objetos e renomeia-o para o SHA1 do Passo 1.
Uma vez concluído todos os arquivos, calcule o SHA1 do arquivo temporário e use-o para nomeá-lo, também o colocando no diretório de objetos.
Se em algum momento o diretório de objetos possuir um arquivo com o dado nome, então você já armazenou o conteúdo daquele arquivo e não existe
necessidade de fazê-lo novamente.
Agora, suba um diretório e comece novamente. Mas desta vez, quando você chegar ao diretório que acabou de processar, inclua a palavra 'tree', o SHA1
do arquivo temporário da última vez, e o nome do diretório no novo arquivo temporário. Nesse esquema, você pode fazer uma árvore de objetos
arquivo e diretórios que contêm os SHA1s e nomes dos arquivos e objetos de diretório que eles contêm.
Quando aplicar o processo em cada diretório e arquivo no snapshot, você terá um único "arquivo objeto diretório raiz" e seu correspondente SHA1.
Como nada contém o diretório raiz, você precisa gravar o SHA1 da árvore raiz em algum outro lugar. Um local ideal para armazenar é no arquivo de
mensagem do snapshot. Assim, a unicidade do SHA1 da mensagem também depende de todo o conteúdo do snapshot, e você pode garantir com absoluta
certeza que duas mensagens de snapshots com SHA1s idênticas contêm os mesmos arquivos!
É também conveniente criar um objeto da mensagem do snapshot da mesma maneira que fez para blobs e árvores. Como você está mantendo uma lista de branchs e nomes de tags que apontam para o SHA1 da mensagem você não precisa se preocupar com perder o rastro de quais snapshots são importantes para você.
Com todas estas informações armazenadas no diretório de objetos, você pode seguramente excluir o diretório de snapshots que usou como fonte desta operação. Se desejar reconstituir o snapshot no futuro é simples como seguir o SHA1 da branch armazenada no arquivo de mensagem e extrair cada árvore e blob no seu diretório e arquivo correspondente.
Para um simples snapshot, este processo de transformação não lhe provê muito. Você basicamente converteu um sistema de arquivos em outro e criou um monte de trabalho a ser feito. Os benefícios reais deste sistema aparecem com o reuso de árvores e blobs entre os snapshots.
Imagine dois snapshots seqüenciais nos quais apenas um arquivo no diretório raiz foi alterado. Se ambos os snapshots contêm 10 diretórios e 100 arquivos, o processo de transformação criará 10 árvores e 100 blobs do primeiro snapshot porem apenas 1 novo blob e 1 nova árvore do segundo snapshot!
Por converter todo o diretório de snapshots do sistema antigo para os arquivos objeto do novo, você drasticamente reduz o número de arquivos que são armazenados no disco. Agora, no lugar de talvez armazenar 50 cópias de arquivos idênticos de um arquivo que raramente muda, você mantém apenas um.
Comprimindo Blobs ( Compressing Blobs )
Eliminando duplicações de blobs e árvores reduz significativamente o total de armazenamento do histórico do projeto, mas não é a única coisa que você pode fazer para salvar espaço. Código fonte é apenas texto e pode ser eficientemente compactado utilizando um algoritmo como LZW ou DEFLATE.
Se você compactar cada blob antes de computar seu SHA1 e salvá-lo no disco pode reduzir o total de armazenamento do histórico para outra quantia bastante admirável.
O verdadeiro Git ( The True Git )
O VCS que você construiu é agora quase uma cópia do Git. A principal diferença é que o Git lhe fornece belas ferramentas de linha de comando para tratar tais coisas como criar snapshots e trocar para antigos ( o Git usa o termo "commit" no lugar de "snapshot"), traçar histórico, manter branchs atualizados, replicar mudanças de outras pessoas, unir e analisar diferenças de branchs, a centenas de outras tarefas comuns (e outras nem tão comuns).
Conforme você continuar a aprender o Git, mantenha está parábola em mente. Git é realmente simples nos bastidores, e é sua simplicidade que o faz tão flexível e poderoso. Uma última coisa antes de correr e começar a aprender os comandos do Git: lembre-se que é quase impossível perder um trabalho que foi comitado. Até mesmo quando excluir um branch, tudo que ocorre é que o ponteiro para aquele commit será removido.
Todos os snapshots continuarão no diretório de objetos, você apenas precisa escavar o SHA1 do commit. Nestes casos, procure com o "git reflog.
Ele contêm o histórico do que cada branch apontou para e em tempos de crise, vai salvar o dia.
Boa leitura....
A parábola do Git
Git é um sistema simples mas extremamente poderoso.
A maioria das pessoas tentam ensinar Git pela demonstração de alguns comandos chegando a um "tadaaaaa.", acredito que este método seja ineficiente. Esta abordagem vai ensiná-lo a usar Git para realizar tarefas simples, mas os comandos irão sempre parecer encantamentos mágicos.
Fazer qualquer coisa além do trivial será aterrorizador. Enquanto não compreender os conceitos nos quais o Git é feito você se sentirá perdido.
A seguinte parábola vai levá-lo através da criação de um sistema similar ao Git a partir de sua base. Entender os conceitos apresentados aqui é a melhor preparação para alcançar o poder total do Git.
Os conceitos em si são bastante simples, mas criam uma imensa gama de funcionalidades. Leia esta parábola do começo ao fim e deverá ter poucos problemas para tornar-se um mestre nos vários comandos do Git, liberando o maravilhoso poder que o mesmo disponibiliza.
A parábola
Imagine que você tem um computador que não tem nada além de um editor de textos e alguns comandos de sistema de arquivos.
Agora, imagine que você decidiu escrever um grande programa neste sistema. Como você é um desenvolvedor responsável, decidiu que precisa inventar alguma forma de manter o registro das versões de seu programa de forma que possa recuperar códigos que tenham sido alterados ou excluídos.
O que segue é uma história sobre como você pode arquitetar tal sistema de controle de versão (VCS) e a razão por traz das escolhas da arquitetura.
Fotocópias ( Snapshots )
Alfredo é um amigo seu que trabalha no supermercado como fotógrafo em uma boutique tipo "Momentos Especiais".
Fica o dia inteiro tirando fotos de garotinhos com poses esquisitas com um fundo de selva ou oceano.
Durante um de seus freqüentes almoços no stande de pretzels, Alfredo lhe conta uma história sobre uma mulher de nome Hazel que traz sua filha
para tirar um retrato todo ano no mesmo dia. "Ela traz todas as fotos dos anos anteriores junto" lhe conta o Alfredo.
"Ela gosta de recordar como sua filha era em cada fase diferente, como se aquelas fotocópias pudessem realmente fazê-la mover o tempo até o instante daquelas memórias."
Repentinamente, a colocação inocente do Alfredo age como um catalisador para você enxergar um solução ideal para seu dilema de controle de versão.
Fotocópias ou snapshots, como pontos de salvamento em um vide-o game, são realmente o que lhe importa quando precisa interagir com um sistema de controle de versão.
E se você pudesse tirar fotocópias do seu código a qualquer momento e ressuscitar esse código quando quizese?
Alfredo percebe o sentimento de realização espalhado por todo seu rosto e sabe que você está a ponto de deixá-lo sem dizer uma palavra e vai implementar qualquer idéia genial que ele acabou de lhe causar. Você não o desaponta.
Você começa seu projeto em um diretório chamado working. Conforme você programa, tenta implementar uma característica por vez.
Quando completa uma parte auto-contida da característica, certifica-se que todos os arquivos foram salvos e faz então uma cópia do diretório de trabalho, dando a ele o nome snapshot-0. Depois de efetuar a cópia, garante que ela nunca será alterada.
Após a próxima parte do trabalho, você faz outra cópia, mas desta vez o novo diretório é denominado snapshot-1, e assim por diante.
Para recordar quais mudanças você fez em cada snapshot, você adiciona um arquivo especial chamado mensagem a cada diretório de snapshot que contém
um resumo do trabalho efetuado e a data de realização. Verificando o conteúdo de cada arquivo mensagem fica fácil de encontrar uma mudança específica feita no passado, caso você tenha que ressuscitar algum código antigo.
Galhos ( Branches )
Após algum tempo no projeto, uma versão candidata à publicação começa a surgir. Madrugadas no teclado finalmente geraram o snapshot-99, a forma
nascente do que será a Versão de Publicação 1.0. Então esta cópia é empacotada e distribuída às massas sedentas que aguardam.
Tocado pela excelente resposta ao seu software, você se esforça, determinado a fazer com que a próxima versão seja um sucesso ainda maior.
Seu VCS até então tem sido um companheiro a prova de falhas.
Versões antigas de seu código estão lá e quando você precisa delas podem ser encontradas facilmente. Mas, não muito após sua publicação, relatórios
de erro começam a chegar. Ninguém é perfeito, você afirma a si mesmo, e o snapshot-99 está pronto para ser recuperado, orgulhoso por ser trazido
de volta para serem aplicadas as correções dos problemas.
Desde a publicação, você criou outros 10 snapshots. Este novo trabalho não deve ser incluído na versão correção de erros 1.0.1 que você vai criar.
Para resolver isso, você copia o snapshot-99 para working, deixando seu diretório de trabalho exatamente no ponto onde a versão 1.0 foi publicada.
Algumas linhas de código são tratadas e o problema está corrigido.
É neste momento que um problema torna-se aparente. o VCS trata muito bem com desenvolvimento linear, mas pela primeira vez, você precisa criar
um novo snapshot que não é descendente direto do último. Se você criar o snapshot-110 ( lembre-se que você já criou 10 cópias desde a publicação),
vai interromper o fluxo linear e não terá como determinar o ancestral da cópia. Claramente você precisa de algo mais poderoso que este sistema linear.
Estudos demonstram que mesmo pequenas exposições à natureza podem ajudar a recarregar o potencial criativo da mente.
Você esteve atrás da luz polarizada artificialmente de seu monitor por dias. Uma caminhada através das árvores sob o ar de outono vão lhe fazer bem
e com alguma sorte, ajudá-lo a encontrar uma solução ideal para seu problema.
Os grandes carvalhos que margeiam a trilha sempre lhe agradaram. Eles parecem repousar rígidos e orgulhosos do perfeito céu azul.
Metade das folhas avermelhadas caíram de seus galhos, deixando um intrincado padrão de galhos como rastro.
Fixando em um dos milhares de tipos de galhos você pacientemente tenta seguí-lo de volta até o solitário tronco.
Esta estrutura organicamente produzida permite esta grande complexidade, mas as regras para encontrar o caminho de volta para o tronco são tão
simples e perfeitas para manter o caminho de múltiplas linhas de desenvolvimento!
Vem a tona que é verdadeiro o que dizem a respeito da natureza e criatividade.
Observando seu histórico de desenvolvimento como uma árvore, resolver o problema de ancestralidade torna-se trivial.
Tudo que você precisa é incluir o nome da snapshot pai no arquivo de mensagem que você escreve para cada snapshot.
Adicionando apenas um único ponteiro para o antecessor possibilitará, fácil e precisamente, traçar o histórico de um snapshot, todo o caminho
de volta até sua raiz.
Nomes de Galhos ( Branch Names )
Seu histórico de código é agora uma árvore. Ao invés de ter apenas um único último snapshot, você tem duas: uma para cada galho ou 'branch'.
Com um sistema linear, seu sistema de numeração seqüencial permite facilmente identificar o último snapshot. Agora esta habilidade foi perdida.
Criar novos branchs de desenvolvimento tornou-se tão simples que você vai tirar vantagem disso o tempo todo.
Você criará branchs para fixar as antigas publicações, para experimentos que podem não serem concluídos; de fato torna-se possível criar um novo
branch para cada nova característica iniciada!
Mas como tudo bom na vida, existe um preço a ser pago. Cada vez que você cria um snapshot, você precisa lembrar que ele se torna o último no
seu próprio branch. Sem esta informação, alternar para um novo branch seria um trabalho extremamente penoso.
Toda vez que cria um novo branch você provavelmente da uma nome para ele em sua cabeça. Você provavelmente diz: "Este será o branch de manutenção da Versão 1.0".
Talvez prefira referir ao formal branch de desenvolvimento linear como o branch "mestre" ou "master".
Pense nisso um pouco mais profundamente. Da perspectiva de uma árvore, o que significa dar nome a um galho?
Nomear cada snapshot que aparecer no histórico de um branch resolveria seu problema, mas iria requirir o armazenamento de uma potencialmente grande
quantia de dados. Adicionalmente, isso ainda não ajudaria a eficientemente encontrar o último snapshot de um branch.
A única informação necessária para identificar um branch é a localização do último snapshot daquele branch.
Se precisar saber a lista de snapshots que são parte de um branch pode facilmente traçar pelo parentesco.
Guardar o nome do branch é trivial. Em um arquivo chamado branchs, armazenado fora de qualquer snapshot específico, você simplesmente
lista os pares nome/snapshot que representaram os branchs. Para alternar para um branch nomeado precisa apenas procurar pelo snapshot
correspondente neste arquivo de referencia.
Como você está armazenando apenas o último snapshot de cada branch, criar um novo snapshot contêm um passo adicional.
Se seu novo snapshot estiver sendo criado como parte de um branch, o arquivo de branchs precisa ser atualizado para que o nome do mesmo fique
associado com o último snapshot. Um pequeno preço a pagar pelo benefício.
Etiquetas ( Tags )
Após utilizar branchs por um tempo, você percebe que eles podem servir para dois propósitos. Primeiro, ele podem atuar com ponteiros de snapshots
para que você possa manter o rastro dos branchs. Segundo, podem apontar para um snapshot específica e nunca mudar.
O primeiro caso permite manter o desenvolvimento em execução, coisas como "Manutenção de Publicação". O segundo caso é útil para marcar pontos de
interesse, tais como "Versão 1.0" ou "Versão 1.1".
Misturar ambos os usos em apenas um arquivo parece bagunçado. Ambos os tipos são ponteiros para snapshots mas um muda e outro não.
Para clarificar e manter a solução elegante, você decide criar outro arquivo chamado etiquetas (tags) para manter ponteiros do segundo tipo.
Manter estes ponteiros, dois parentes comuns, em arquivos separados ira ajudá-lo a não tratar acidentalmente um branch como tag e vice-versa.
Distribuído ( Distributed )
Trabalhar por si mesmo acaba tornando-se bastante solitário. Não seria legal se pude-se convidar um amigo para trabalhar em seu projeto com você?
Bem, você está com sorte. Sua amiga Zoe tem um sistema de computador como o seu e quer ajudar no projeto. Como você criou um ótimo sistema de
controle de versão, acaba contando a ela sobre o mesmo e envia uma cópia de todos seus snapshots, branchs e tags para que ela possa aproveitar
dos mesmos benefícios do histórico de versões.
É ótimo ter Zoe no time mas ela tem o hábito de fazer longas viagens para lugares distantes e sem acesso à internet.
Assim que ela pegou o código fonte, saiu em um vôo para a Patagônia e você não ouviu falar dela por uma semana.
Neste meio tempo, ambos programaram uma tempestade. Quando ela finalmente voltou, você descobriu uma falha critica no seu VCS.
Como ambos estavam utilizando o mesmo sistema de numeração, cada um possui diretórios chamados 'snapshot-114', 'snapshot-115' e assim por diante,
mas com diferentes conteúdos.
Para piorar as coisas, você nem sabe quem produziu as alterações daquelas novas fotocópias. Juntos, concebem então um plano para tratar destes problemas.
Primeiro, as mensagens dos snapshots irão obrigatoriamente conter o nome do autor e seu e-mail. Segundo, snapshots não serão mais nomeadas por
simples números. No lugar disto, vocês utilizarão o conteúdo do arquivo de mensagem para produzir uma cifra ou hash. Este hash será garantidamente única
para o snapshot já que duas mensagens nunca terão a mesma data, mensagem, antecessor e autor.
Para fazer as coisas correrem suavemente, ambos concordam em utilizar o algoritmo de hash SHA1 que pega o conteúdo de um arquivo e produz uma
palavra de 40 caracteres hexadecimais. Ambos atualizam seus históricos com a nova técnica no lugar dos colisivos diretórios 'snapshot-114' e agora
possuem diretórios distintos nomeados ‘8ba3441b6b89cad23387ee875f2ae55069291f4b’ e ‘db9ecb5b5a6294a8733503ab57577db96ff2249e’.
Com a atualização do esquema de nomenclatura, torna-se trivial recuperar todas as cópias do computador de Zoe e colocá-las no seu repositório.
Como cada cópia especifica sua antecessora, quando possuem mensagens idênticas ( e também cópias idênticas ) possuirão os mesmos nomes não importa
onde são criados, o histórico da base de código pode continuar a ser mantido como uma árvore. A diferença é que a árvore é construída por snapshots
criadas por Zoe e você.
Este ponto é importante o bastante para garantir repetição. Uma cópia é identificada pelo SHA1 que a identifica unicamente ( e seu antecessor ).
Estes snapshots podem ser criadas e movidas entre computadores sem perder sua identidade ou onde pertencem no histórico da árvore do projeto.
Ainda mais, cópias podem ser compartilhadas ou mantidas privadas de acordo com sua vontade. Se você possuir alguns snapshots experimentais
que deseja manter para si mesmo, pode fazê-lo facilmente. Apenas não as torne disponíveis para Zoe!
Desconectado ( Offline )
O hábito de viajar de Zoe a faz passar incontáveis horas em aviões e barcos. A maior parte dos lugares que visita não possui acesso à internet disponível.
No fim do dia, ela passou mais tempo desconectada que conectada.
Não é surpresa, no entanto, que Zoe adora seu VCS. As operações do dia a dia que ela precisa fazer podem ser efetuadas localmente. O único momento
que ela precisa de uma conexão de rede é quando está pronta para compartilhar seus snapshots com você.
Junções ( Merges )
Antes de Zoe sair para sua viagem, você pediu-a para trabalhar em um branch chamado 'math' e para implementar uma função que gera números primos.
Neste meio tempo, você esteve também esteve desenvolvendo algo no branch 'math', só que esteve trabalhando na função que gera números mágicos;
Agora Zoe retornou e você está encarando a tarefa de unir estes dois diferentes branchs de desenvolvimento em uma único snapshot.
Como ambos trabalharam em tarefas distintas, a junção é simples. Enquanto construía a mensagem do snapshot para a junção, você percebe que esta
snapshot é especial. No lugar de ter apenas um antecessor, esta junção, ou merge, possui dois! O primeiro é a sua última snapshot no branch 'math'
e o segundo é o branch 'math' de Zoe. O snapshot de junção contêm todas as mudanças além daquelas necessárias para unir os dois antecessores em uma
única base de código;
Completado o merge, Zoe pega todos os snapshots que você possui e ela não, o que inclui seu desenvolvimento no branch 'math' e sua snapshot de merge.
Uma vez que ela fez isso, ambos os históricos casam perfeitamente!
Reescrevendo o Histórico ( Rewriting History )
Como muitos desenvolvedores de software, você tem a compulsão de manter seu código limpo e muito bem organizado. Isso leva ao desejo de manter seu
histórico de código com esmero. Noite passada você chegou após tomar alguns goles de Guinness em um bar e começou a programar, produzindo um monte
de snapshots no caminho. Hoje de manhã, uma revisão da produção da noite passada fez você se assustar um pouco. No geral, o código está bom, mas
você teve uma série de enganos que foram corrigidos nos snapshots posteriores.
Vamos dizer que a branch na qual você realizou o desenvolvimento bêbado é chamada 'drunk' e você fez três snapshots após ter chegado do bar.
Se o nome 'drunk' aponta para o último snapshot neste branch, então você pode utilizar uma notação útil para referenciar o antecessor deste snapshot.
A notação 'drunk^' significa o pai do snapshot apontada pelo branch 'drunk'. Similarmente 'drunk^^' significa o avô do snapshot 'drunk'.
Assim os snapshots em ordem cronológica são 'drunk^^', 'drunk^' e 'drunk'.
Você realmente gostaria que estes três snapshots fossem dois snapshots limpos. Uma que muda uma função existente, e uma que adiciona um novo arquivo.
Para realizar esta revisão do histórico você copia 'drunk' para working e exclui o arquivo que é novo na série. Agora working representa as
modificações corretas na função existente. Você pode criar um novo snapshot de working e escrever a mensagem apropriada para as mudanças.
Como antecessor você especifica o SHA1 do snapshot 'drunk^^^', essencialmente criando um novo branch do mesmo snapshot da noite passada.
Agora pode copiar 'drunk' para working e executar um novo snapshot com a adição daquele novo arquivo. Como antecessor você especifica aquele
snapshot que acabamos de criar antes deste.
Como último passo, você muda o branch 'drunk' e aponta para o último snapshot que acabou de fazer.
O histórico do branch 'drunk' agora representa uma versão melhor daquilo que fez na noite passada. Os outros snapshots que você substituiu não
são mais necessários e podem ser excluídos ou deixados para a posteridade. Nenhum nome da branch está apontando para eles portanto será difícil
de encontrá-los depois, mas se você não os excluir estarão lá.
Colocando no palco ( Staging Area )
Mesmo tentando manter suas modificações relacionadas a uma única característica ou pedaço lógico, algumas vezes você precisa começar a trabalhar
em algo totalmente não relacionado. No meio do caminho percebe que seu diretório de trabalho contêm algo que deveria ser separado em dois
snapshots discretos.
Para ajudá-lo com essa situação chata, o conceito área do palco é útil. Esta área atua como um intermediário entre seu trabalho e o snapshot
final. Cada vez que você termina um snapshot, você também copia aquele snapshot para o diretório do palco.
Agora, toda vez que terminar uma edição de um novo arquivo, criar um novo arquivo, ou remover um arquivo, você decide quando esta mudança deve
ser parte de seu próximo snapshot. Caso deseje, você replica a mudança dentro do palco. Senão, pode deixá-la e aplicar em um snapshot futuro.
De agora em diante, snapshot são criados diretamente do diretório do palco.
Esta separação entre programar e preparar o palco torna fácil especificar o que é e o que não é incluído no próximo snapshot.
Você não precisa mais se preocupar muito sobre causar uma mudança acidental não relacionada no seu diretório de trabalho.
No entanto, você precisa ser um pouco cuidadoso. Considere um arquivo de nome LEIAME. Você edita este arquivo e então replica o mesmo no palco.
Você continua com o que tem que fazer, editando outros arquivos. Depois de um tempinho, você faz outra alteração no LEIAME. Agora você tem duas
alterações neste arquivo, mas apenas uma está no palco! Se criar um snapshot agora, sua segunda alteração estará ausente.
A lição nisso: cada nova edição precisa ser adicionada ao palco se é para ser parte do próximo snapshot.
Diferenças ( Diffs )
Com um diretório de trabalho, um palco e um monte de snapshots armazenados, começa a ficar confuso notar quais diferenças existem entre eles. Uma mensagem de snapshot apenas fornece um resumo do que mudou, não exatamente quais linhas foram mudadas entre dois arquivos.
Utilizando um algoritmo de diferenciação, você pode implementar um pequeno programa que mostra as diferenças em duas bases de código.
Conforme for desenvolvendo e copiando coisas do seu diretório de trabalho para o palco, tudo que vai realmente querer é uma maneira fácil de visualizar as diferenças entre os dois, assim pode determinar o que mais precisa ser colocado no palco. É também importante notar como o palco está diferente do último snapshot, uma vez que estas mudanças são o que irá se tornar parte do próximo snapshot que produzir.
Existem muitas outras diferenças que você pode querer ver. Entre um snapshot e seu antecessor iria mostrar um conjunto de mudanças que foram introduzidas pelo snapshot. A diferenciação entre dois branchs poderia ser um auxílio para garantir que seu desenvolvimento não vagou muito distante da linha principal.
Eliminando Duplicação ( Eliminating Duplication )
Depois de algumas viagens para a Namibia, Istanbul e Galapagos, Zoe começa a reclamar que seu disco rígido está cheio com milhares de arquivos com
cópias praticamente iguais do software. Você também tem sentido que todas aquelas duplicações são desperdícios.
Depois de pensar um pouco, você chega a uma solução bastante inteligente.
Você recorda que aquele hash SHA1 produz uma palavra curta que é única para o conteúdo de um arquivo. Iniciando com o primeiríssimo snapshot no
histórico do projeto, você começa o processo de conversão. Primeiro, você cria um diretório chamado objetos fora do histórico de código. Depois, encontra o mais profundo diretório aninhando em um snapshot. Adicionalmente, você abre um arquivo temporário para escrita.
Para cada arquivo neste diretório você executa três passos.
Passo 1: Calcular o SHA1 de seu conteúdo.
Passo 2: Adicionar uma entrada no arquivo temporário que contêm a palavra blob ( grande objeto binário ), o SHA1 do Passo 1 e o nome do arquivo.
Passo 3: Copia o arquivo para o diretório de objetos e renomeia-o para o SHA1 do Passo 1.
Uma vez concluído todos os arquivos, calcule o SHA1 do arquivo temporário e use-o para nomeá-lo, também o colocando no diretório de objetos.
Se em algum momento o diretório de objetos possuir um arquivo com o dado nome, então você já armazenou o conteúdo daquele arquivo e não existe
necessidade de fazê-lo novamente.
Agora, suba um diretório e comece novamente. Mas desta vez, quando você chegar ao diretório que acabou de processar, inclua a palavra 'tree', o SHA1
do arquivo temporário da última vez, e o nome do diretório no novo arquivo temporário. Nesse esquema, você pode fazer uma árvore de objetos
arquivo e diretórios que contêm os SHA1s e nomes dos arquivos e objetos de diretório que eles contêm.
Quando aplicar o processo em cada diretório e arquivo no snapshot, você terá um único "arquivo objeto diretório raiz" e seu correspondente SHA1.
Como nada contém o diretório raiz, você precisa gravar o SHA1 da árvore raiz em algum outro lugar. Um local ideal para armazenar é no arquivo de
mensagem do snapshot. Assim, a unicidade do SHA1 da mensagem também depende de todo o conteúdo do snapshot, e você pode garantir com absoluta
certeza que duas mensagens de snapshots com SHA1s idênticas contêm os mesmos arquivos!
É também conveniente criar um objeto da mensagem do snapshot da mesma maneira que fez para blobs e árvores. Como você está mantendo uma lista de branchs e nomes de tags que apontam para o SHA1 da mensagem você não precisa se preocupar com perder o rastro de quais snapshots são importantes para você.
Com todas estas informações armazenadas no diretório de objetos, você pode seguramente excluir o diretório de snapshots que usou como fonte desta operação. Se desejar reconstituir o snapshot no futuro é simples como seguir o SHA1 da branch armazenada no arquivo de mensagem e extrair cada árvore e blob no seu diretório e arquivo correspondente.
Para um simples snapshot, este processo de transformação não lhe provê muito. Você basicamente converteu um sistema de arquivos em outro e criou um monte de trabalho a ser feito. Os benefícios reais deste sistema aparecem com o reuso de árvores e blobs entre os snapshots.
Imagine dois snapshots seqüenciais nos quais apenas um arquivo no diretório raiz foi alterado. Se ambos os snapshots contêm 10 diretórios e 100 arquivos, o processo de transformação criará 10 árvores e 100 blobs do primeiro snapshot porem apenas 1 novo blob e 1 nova árvore do segundo snapshot!
Por converter todo o diretório de snapshots do sistema antigo para os arquivos objeto do novo, você drasticamente reduz o número de arquivos que são armazenados no disco. Agora, no lugar de talvez armazenar 50 cópias de arquivos idênticos de um arquivo que raramente muda, você mantém apenas um.
Comprimindo Blobs ( Compressing Blobs )
Eliminando duplicações de blobs e árvores reduz significativamente o total de armazenamento do histórico do projeto, mas não é a única coisa que você pode fazer para salvar espaço. Código fonte é apenas texto e pode ser eficientemente compactado utilizando um algoritmo como LZW ou DEFLATE.
Se você compactar cada blob antes de computar seu SHA1 e salvá-lo no disco pode reduzir o total de armazenamento do histórico para outra quantia bastante admirável.
O verdadeiro Git ( The True Git )
O VCS que você construiu é agora quase uma cópia do Git. A principal diferença é que o Git lhe fornece belas ferramentas de linha de comando para tratar tais coisas como criar snapshots e trocar para antigos ( o Git usa o termo "commit" no lugar de "snapshot"), traçar histórico, manter branchs atualizados, replicar mudanças de outras pessoas, unir e analisar diferenças de branchs, a centenas de outras tarefas comuns (e outras nem tão comuns).
Conforme você continuar a aprender o Git, mantenha está parábola em mente. Git é realmente simples nos bastidores, e é sua simplicidade que o faz tão flexível e poderoso. Uma última coisa antes de correr e começar a aprender os comandos do Git: lembre-se que é quase impossível perder um trabalho que foi comitado. Até mesmo quando excluir um branch, tudo que ocorre é que o ponteiro para aquele commit será removido.
Todos os snapshots continuarão no diretório de objetos, você apenas precisa escavar o SHA1 do commit. Nestes casos, procure com o "git reflog.
Ele contêm o histórico do que cada branch apontou para e em tempos de crise, vai salvar o dia.
Comentários