Um Sistema de Controle de Versão (ou Version Control System - VCS) é uma metodologia ou ferramenta que ajuda a rastrear alterações nos arquivos do projeto. O VCS é como se fosse criada uma cópia de um arquivo que estivesse sendo trabalhado e de forma manual esse arquivo de tempos em tempos fosse salvo atribuindo ao seu nome diferentes versões ou uma data e um tempo como Arquivo1, Arquivo2, etc., ou ainda Arquivo-15-10-11-41, Arquivo-15-19-11-45, etc.

Um Sistema de Controle de Versão Distribuído (ou Distributed Version Control Systems - DVCSs) não é diferente do Sistema de Controle de Versão nesse sentido. O principal objetivo ainda é rastrear alterações realizadas no projeto que está sendo trabalhado. A diferença entre VCSs e DVCSs é como os desenvolvedores comunicam as alterações entre eles.

No restante do artigo será visto as diversas diferenças entre os dois sistemas de controle de versão e como podemos utilizar na prática um sistema de controle de versão distribuído. Também será visto as melhores práticas adotadas nas organizações quando se utiliza um sistema de controle de versão distribuído como o GIT.

Repositório

O repositório é o local onde o sistema de controle de versão mantém o rastreamento de todas as alterações realizadas no projeto. A maioria dos VCSs apenas armazenam o estado atual do código, além de quando essa alteração foi realizada, quem a fez, e uma mensagem de texto em um log que explica o porquê da alteração ter sido realizada.

Nos VCSs esses repositórios eram acessíveis apenas se o desenvolvedor estivesse logado diretamente na máquina onde o repositório estivesse armazenado. Como pode ser visto isto não é escalável. Entre os tipos de VCSs mais conhecidos tem-se o CVS e o SVN. Esses sistemas permitem que os desenvolvedores trabalhem remotamente com o repositório enviando as suas alterações e recebendo-as utilizando uma conexão com a rede.

Como pode-se notar esses sistemas utilizam um modelo de repositório centralizado, em que existe um repositório central que todos enviam as suas alterações. Assim, cada desenvolvedor mantém uma cópia da última versão do repositório e sempre que uma alteração é realizada o desenvolvedor a envia para o repositório, principalmente para manter uma cópia daquilo que ele já fez e não a perca se algo de errado ocorrer com as próximas modificações no código.

Essa abordagem tem muitas limitações, pois é preciso manter a última versão do código, muitas vezes enviar alterações incompletas para o repositório, e ter acesso permanente ao repositório remoto, sendo nem sempre se tem acesso a ele. Através disto é que surgem as grandes vantagens em utilizar DVCS, que é o modelo seguido pelo GIT. Ao invés de precisar de um repositório central que os desenvolvedores enviam suas alterações, é preciso apenas de um repositório próprio em que se tem um histórico inteiro do projeto. Efetuar commits não envolve uma conexão com um repositório remoto, a alteração é armazenada localmente.

Armazenamento no Repositório

De forma geral o repositório precisa ter tudo que é essencial para o projeto, tanto para codificar, modificar quanto para construir as versões. Esses artefatos armazenados podem incluir Makefiles, Rakefiles, código-fonte, documentos do projeto, build.xml do Ant, imagens que são usadas pela aplicação, testes de unidade, entre outros.

Working Trees

Um Working Tree é onde as alterações são realizadas. Assim, o working tree é a visão atual do repositório e pode incluir código-fonte, documentos, arquivos build, testes de unidades, etc. Os VCSs tradicionais se referem ao working tree como o working copy.

É importante diferenciar um working tree de um repositório nos DVCS. No Git o diretório “.git/” é o repositório local do desenvolvedor, ele está dentro do diretório do projeto na máquina local. Ou seja, pode-se visualizar o histórico completo do repositório e visualizar alterações sem ser necessário se comunicar com um repositório em outro servidor.

Para iniciar um working tree basta inicializar um repositório no Git ou ainda pode-se clonar um repositório existente. O clone faz uma cópia de outro repositório e então faz um check out do branch master (o master é a linha principal de desenvolvimento). O checking out é o processo que o Git usa para alterar o working tree para um certo ponto do repositório.

Manipulando Arquivos e Sincronizando

O committ das alterações adiciona uma nova revisão ao repositório e armazena um log de mensagem que explica o que a alteração fez. Isto permite que se tenha um registro para voltar atrás se for preciso saber, por exemplo, quando um bug foi introduzido.

Um DVCS como o Git requer que alterações sejam compartilhadas com outros desenvolvedores também, além de dar acesso a outras alterações já realizadas por outros desenvolvedores. Isto é realizado através de um push para um repositório remoto. Esse repositório possibilita que todos desenvolvedores façam também um push. Um push nada mais é do que enviar as informações para outro repositório que seja compartilhado com outros desenvolvedores.

Para manter o projeto como um todo sincronizado, apenas o push não basta, também deve-se realizar um fetch que é quando solicitamos ao repositório as últimas atualizações dos outros membros do time.

Assim, existem duas formas de receber as alterações de um repositório remoto. A primeira delas é através do fetch, que se dá através de uma cópia das alterações do repositório remoto para o nosso repositório local. A outra forma é através do merge em que se mescla as alterações do repositório remoto com o local. O Git fornece ferramentas que facilitam esse processo.

Na maioria das vezes o melhor é fazer o fetch (buscar alterações) e o merge (mesclar as alterações) de uma vez, como o update no SVN ou no CVS. Para isso basta utilizar o pulling que faz ambas as coisas de uma vez.

Rastreando Projetos, Diretórios e Arquivos

O Git rastreia os arquivos armazenados no repositório como “conteúdo”. Esta abordagem é diferente da maioria dos sistemas de controle de versão que rastreavam arquivos. Assim, ao invés de rastrear um arquivo como "modelos.py", o Git rastreia o seu conteúdo, ou seja, caracteres individuais e linhas, então o Git adiciona metadados para complementar as informações. Esta diferença é sutil, mas bastante importante.

Tecnicamente esta diferença tem várias vantagens como a redução na quantidade de espaço necessário para armazenar todo o histórico do repositório e também torna viável e rápido para fazer as coisas, tais como a detecção de funções ou classes que se deslocam entre dois arquivos ou determinar de onde o código copiado veio.

Todo trabalho ocorre no working tree que é um conjunto de diretórios e arquivos normais que representam a visão atual do repositório.

O Git também permite que o repositório seja estruturado como o desenvolvedor quiser. Pode-se criar um novo diretório no repositório para cada projeto para que esses projetos compartilhem um histórico comum, ou pode-se criar um novo repositório para cada projeto.

Tags

Assim que os projetos progridem, eles atingem marcos. Em um projeto que utiliza uma metodologia ágil e tem o desenvolvimento em ciclos semanais, as funcionalidades são adicionadas a cada semana, já em um projeto que segue uma metodologia tradicional as funcionalidades são lançadas em poucos meses.

Em qualquer uma das metodologias, faz-se necessário acompanhar e marcar o estado do repositório quando um marco importante for atingido, como por exemplo, uma entrega.

As Tags fornecem essa importante ferramenta. As tags marcam um certo ponto no histórico do repositório tal que elas podem facilmente ser referenciadas mais tarde.

Assim, uma tag é simplesmente um nome que se pode usar para marcar algum ponto específico na história do repositório, como por exemplo, uma entrega ou algum ponto de correção de algum bug.

As tags são diferentes dos branches, que será visto na próxima seção.

Branches

O Git permite a criação de branches que marcam um ponto onde os arquivos do repositório divergem. Cada branch mantém um registro das alterações feitas ao seu conteúdo separadamente de outros branches para que seja permitido criar histórias alternativas no projeto.

O branch master é a linha principal de desenvolvimento, também conhecido como trunk nos VCSs. O branch quebra essa linha em outros "ramos" (como uma árvore que tem o tronco e vários ramos a partir daquele tronco principal).

As boas práticas indicam que um branch pode existir durante toda a vida de um projeto ou por apenas algumas horas, como um branch que foi criado para realizar algum experimento no projeto. Um branch também pode ser mesclado (merged) com outro branch, mas também isto não é uma regra, ou seja, um branch não precisa ser sempre mesclado com outro branch.

Como tudo no Git um branch pode ser criado localmente sem precisar compartilhá-lo no repositório remoto. Isto pode ser usado para o desenvolvedor criar experimentos com algumas alterações e deletá-lo se não funcionar.

De fato, a maioria dos branches não precisam ser mescladas com outros branches para mantê-lo atualizado. Na parte prática do artigo isso será melhor exemplificado.

Merging

Algumas vezes é necessário mesclar os branches, nesse caso o merge entra para fazer o serviço.

O merge nada mais é do que combinar dois ou mais históricos de branches em um único. O Git compara dois ou mais conjuntos de alterações e tenta determinar onde essas alterações ocorreram.

Quando as alterações ocorrem em diferentes partes de um arquivo, o Git pode fazer um merge automático deles. Algumas vezes ocorre um conflito, que é quando existem alterações em partes iguais do código, nesse caso o Git pede que o desenvolvedor interfira no processo e faça o merge de forma manual.

O Git possui vários métodos para gerenciar conflitos.

Outra situação interessante do Git é que ele rastreia os merges, ou seja, quais commits sofreram um merge e quais não foram. Esta abordagem não é encontrada em outros sistemas de controle de versão como CVS e SVN.

Opções de Bloqueio

Quando um livro na biblioteca tem apenas uma cópia, todos que tiverem interesse em ler o livro deverão aguardar ele retornar à estante para que possam lê-lo.

O mesmo ocorre com os VCSs, em que o desenvolvedor solicita ao repositório um arquivo para que possa fazer algumas alterações, então o repositório confirma para o usuário e previne qualquer um de realizar alterações até que se faça um check-in.

Este é chamado é strict locking e bloqueia completamente o trabalho.

Assim como a biblioteca apenas uma pessoa pode ter uma cópia do trabalho em um dado momento. Isto não é eficiente para um time que trabalha junto e não é um modelo utilizado pelos DVCSs em que todos desenvolvedores são fracamente acoplados a todos os outros. Assim, no DVCS utiliza-se o optimistic locking que permite múltiplos desenvolvedores trabalhar no mesmo código e nos mesmos arquivos assumindo que na maior parte do tempo as suas alterações não conflitam.

Um exemplo dessa situação são dois desenvolvedores que realizaram um clone do repositório remoto cada um. O desenvolvedor A faz alterações no seu código e dá um push para o repositório. Se um desenvolvedor B estiver mexendo no mesmo arquivo e tentar realizar um push ele não conseguirá, o Git informará que existem alterações no repositório e ele deve atualizar o seu arquivo. Assim, após realizar um pull para receber as alterações e gerenciar algum conflito se necessário ele pode realizar um push normalmente.

Vale ressaltar que duas pessoas no mesmo time raramente editam o mesmo arquivo, assim esse problema não ocorre com frequência. Quando isto ocorre, o Git trata de gerenciar os merges automaticamente.

Criando um Projeto no Git

Para entender melhor como funciona o GIT nada melhor do que verificar na prática como criar um projeto.

Criando um Repositório

Criar um repositório no GIT é bastante simples, mas para quem está acostumado com SVN e CVS pode achar um pouco diferente.

Um repositório pessoal ou local é algo diferente do que havia na maioria dos VCSs. O repositório local no Git é armazenado em um diretório chamado ".git".

Para criar um repositório no Git, primeiramente deve-se decidir onde armazenar o código do projeto. Para o exemplo abaixo considera-se um arquivo HTML que pertence ao projeto "meusite". O diretório a ser criado precisa ter o mesmo nome do projeto. Assim, os passos executados abaixo criam o diretório do projeto e o repositório local:


  prompt> mkdir meusite
  prompt> cd meusite
  prompt> git init
  Initialized empty Git repository in /meusite/.git/

Com isso foi criado um repositório Git que está pronto para rastrear o projeto, e não precisa nada mais do que isso para ter um repositório criado.

O comando "get init" configura um diretório chamado “.git” que armazena todos metadados do repositório. O diretório “meusite” é o working tree e contém o código.

Realizando Alterações

Agora que possuímos um repositório vazio, pode-se adicionar arquivos a ele. Para isso podemos criar um arquivo index.html como exemplificado no código abaixo:

Listagem 1. Arquivo de exemplo HTML que será versionado no Git.


  <html>
  <body>
           <h1>Página Inicial GIT</h1>
  </body>
  </html>

Agora que já se tem um arquivo no repositório pode-se solicitar ao Git para rastrear este projeto.

Primeiramente é necessário solicitar ao Git para adicionar o arquivo ao "index". Isto é realizado usando o comando "get add" e, por fim, pode-se realizar um commit conforme os códigos da Listagem 2.

Listagem 2. Commit


  prompt> git add index.html
  prompt> git commit -m "adicionado arquivo HTML ao projeto"
           Created initial commit 7b1558c: adicionado arquivo HTML ao projeto
           1 files changed, 5 insertions(+), 0 deletions(-)
           create mode 100644 index.html

O comando "git add" espera um arquivo ou uma lista de arquivos como parâmetro.

O Git também possui outras formas de adicionar arquivos, mas isto será visto no próximo artigo.

O comando "git commit" faz o commit propriamente dito. Os commits são peças individuais da história armazenadas pelo repositório. Cada um desses commit marca a progressão do código.

A cada commit o Git armazena o nome e o e-mail de quem está comitando (o nome e o e-mail são solicitados quando instalamos o Git) e também uma mensagem para cada commit.

O atributo -m recebe como parâmetro uma mensagem que é utilizada para explicar a razão do commit. Essas mensagens são importantes quando precisarmos auditar os commits ou saber o que foi realizado em determinado commit. Para facilitar a escrita da mensagem, ela deve responder principalmente das perguntas: "O que faz esse novo arquivo sendo comitado?" e "Por que essa alteração foi realizada?".

Outro comando importante é o "git log". Se executarmos o comando neste momento pode-se visualizar o log da Listagem 3.

Listagem 3. Log de teste

 commit 7b1558c92a7f755d8343352d5051384d98f104e4
           Author: Higor Medeiros <higor@teste.com>
           Date: Sun Sep 21 14:20:21 2015 -0500
                     add in hello world HTML

A primeira linha mostra o nome do commit, que são hashes SHA-1 gerados pelo Git para manter o rastreamento de um commit. O Git usa esses hashes para certificar que cada identificador de commit é completamente único, isso é muito importante em ambientes distribuídos.

Uma situação interessante é que quando foi realizado um commit anteriormente, o seguinte log foi recebido:


           Created initial commit 7b1558c: adicionado arquivo HTML ao projeto
           1 files changed, 5 insertions(+), 0 deletions(-)
           create mode 100644 index.html

O nome do commit acima possui apenas os sete primeiros caracteres da saída do comando “git log”. Isto é normal, pois o Git apresenta apenas um nome abreviado quando o commit é realizado, ele não mostra o nome completo.

A segunda linha do comando "git log" mostra os dados do autor que realizou o commit e a terceira linha mostra as informações da data do commit e, por fim, a última linha mostra a mensagem utilizada no commit.

Pode-se notar que no VCS eram designados números para cada commit. Estes números até faziam sentido, mas agora que temos um ambiente distribuído seria impossível utilizar esses números simples.

Trabalhando com Alterações no Projeto

Alterando o código index.html anterior adicionando as tags <head> e <title>, tem-se o código da Listagem 4.

Listagem 4. Arquivo de exemplo HTML alterado com novas tags.


  <html>
  <head>
           <title>Testando o Git</title>
  </head>
  <body>
           <h1>Página Inicial GIT</h1>
  </body>
  </html>

Dessa forma, o arquivo foi alterado.

Utilizando o comando "git status" tem-se como saída como que o Git está visualizando o working tree, que é a visão atual do repositório, de acordo com a Listagem 5.

Listagem 5. Saída do comando git status

prompt> git status
  # On branch master
  # Changed but not updated:
  # (use "git add <file>..." to update what will be committed)
  #
  # modified: index.html
  #
  no changes added to commit (use "git add" and/or "git commit -a")

Esta saída mostra que o Git identificou modificações que foram realizadas, mas ele não sabe o que fazer com essas alterações. Este arquivo é listado como modificado e não atualizado.

É importante saber que existem três lugares no Git que o arquivo pode ser armazenado. O primeiro lugar é onde trabalha-se diretamente quando está se editando arquivos, é o working tree. O segundo é o index que é uma área temporária (staging). A área temporária é um buffer entre o working tree e o que está armazenado no repositório. A terceira, portanto, é a área final de armazenamento do Git. A área Staging ou temporária é utilizada para armazenar apenas as alterações a serem comitadas para o repositório.

Voltando ao projeto, vamos adicionar ao Stage as alterações realizadas no index.html e após isso verificar como ficou a estado do repositório. Para isso basta realizar o comando da Listagem 6.

Listagem 6. Alterações no Stage

prompt> git add index.html
  prompt> git status
  # On branch master
  # Changes to be committed:
  # (use "git reset HEAD <file>..." to unstage)
  #
  # modified: index.html
  #

Agora status mudou de "Changed but not updated" para "Changes to be committed".

Após isso deve-se executar um comando "git commit" conforme a Listagem 7.

Listagem 7. Comando git commit

prompt> git commit -m "adicionadas as tags para o arquivo index"            -m "Isto permite uma melhor semântica ao arquivo."
  Created commit a5dacab: adicionadas as tags <head> e <title> para o arquivo index
  1 files changed, 3 insertions(+), 0 deletions(-)

No comando passado foram utilizados dois parâmetros “–m”, isso é permitido no Git e equivale a um novo parágrafo.

Realizando um comando "git log" pode-se visualizar como fica o log da listagem 8.

Listagem 8. Saída do comando git log


  prompt> git log -1
  commit a5dacabde5a622ce8ed1d1aa1ef165c46708502d
  Author: Higor Medeiros <higor@teste.com>
  Date: Sun Sep 21 20:37:47 2015 -0500
           adicionadas as tags para o arquivo index

Isto permite uma melhor semântica ao arquivo. O parâmetro "-1" altera o número limite de saída do log do git.

Utilizando Branches

Os branches são utilizados para manter histórias alternadas no projeto que está sendo trabalhado.

Basicamente existem dois tipos mais usados de branches: utilizar diferentes versões de um projeto em diferentes branches e branches organizados por funcionalidades específicas. Como o primeiro tipo é o mais utilizado apenas o exemplificaremos.

Para criar um branch no Git basta executar o código abaixo:

prompt> git branch RB_1.0 master

O comando recebe dois parâmetros: o nome do branch que se quer criar e o nome do branch a partir do qual o novo será criado.

No exemplo acima o comando cria um branch chamado RB_1.0 a partir do branch master (o master é o mesmo que o trunk no CVS e SVN).

Assim, o que está atualmente no master vai para o branch criado.

O RB no nome é um acrônimo para "release branch". Agora pode-se realizar alterações no código sem afetar um código que está quase pronto para ser lançado em produção.

Agora vamos tentar adicionar um link à página antes da tag </body>, como mostra a Listagem 9.

Listagem 9. Arquivo de exemplo HTML alterado com uma nova tag de link.


  <ul>
  <li><a href="teste2.html">Teste com Link</a></li>
  </ul>

Agora pode-se comitar este código conforme os comandos da Listagem 10.

Listagem 10. Commit da Listagem 9


  prompt> git commit -a
           ... editor launch, create log message, save, and exit ...
           Created commit e993d25: adicionada tag de link
           1 files changed, 3 insertions(+), 0 deletions(-)

O parâmetro “-a” informa para o Git comitar todos os arquivos que ele conheça que possuem modificações.

Agora temos uma alteração no branch master que não está incluída no branch criado.

Para trocar de branch basta executar o comando a seguir:


  prompt> git checkout RB_1.0
  Switched to branch "RB_1.0"

Abrindo o arquivo index.html pode-se notar que não foi incluída a tag anterior.

O projeto original está intacto no branch criado.

Por fim, vamos realizar mais uma adição ao código dentro da tag <head>, como mostra a Listagem 11.

Listagem 11. Arquivo de exemplo HTML alterado com uma nova tag no head.


  <head>
           <title>Testando o Git</title>
           <meta name="description" content="testando o git" />
  </head>

Agora basta salvar o arquivo e realizar o commit:


  prompt> git commit -a
           ... editor launch, create log message, save, and exit ...
           Created commit 4b53779: Adicionado um elemento description no metadata
           1 files changed, 1 insertions(+), 0 deletions(-)

Agora o código está pronto para ser lançado em produção, dessa forma, agora pode-se criar uma Tag.

Gerenciamento de Lançamentos com Tags

Agora o código está pronto para ter a sua versão 1.0 lançada, portanto, é hora de criar uma tag para o projeto.

Criando uma Tag o Git marca um ponto específico no histórico do repositório tal que possa ser referenciado facilmente.

Para isso basta realizar o comando a seguir:

prompt> git tag 1.0 RB_1.0

Os dois parâmetros especificam o nome da tag e o ponto que se quer a tag, ou seja, 1.0 e o RB_1.0 respectivamente.

Para ver uma lista de tags existentes no repositório basta executar o comando a seguir:

prompt> git tag
  1.0

Nesse caso, foi apresentada a versão 1.0 que é a única tag criada.

Agora que o código está taggeado é preciso efetuar algumas limpezas no repositório, pois tem-se dois branches e cada um contém commits diferentes.

O branch master precisa receber as alterações adicionadas no branch RB_1.0 que possui a adição da tag <meta>.

Para isto basta utilizar o comando rebase que pega todas alterações de um branch e coloca em cima de outro branch.

Portanto, primeiramente é preciso voltar ao branch master:


  prompt> git checkout master
  Switched to branch "master"

E agora rodar o git rebase com o nome do branch que queremos efetuar o rebase:


  prompt> git rebase RB_1.0
  First, rewinding head to replay your work on top of it...
  Applying: adicionada tag de link

Agora o branch criado anteriormente já pode ser deletado conforme o comando a seguir:


  prompt> git branch -d RB_1.0
  Deleted branch RB_1.0. 

Nesse momento, se surgirem bugs podemos criar um branch a partir da tag que possui a versão lançada (1.0).

Para isso basta realizar o comando a seguir:


  prompt> git branch RB_1.0.1 1.0
  prompt> git checkout RB_1.0.1
  Switched to branch "RB_1.0.1"

Outro comando interessante é o git log para verificar tudo que foi comitado. Para isto basta realizar o comando abaixo:

prompt> git log --pretty=oneline
  4b53779 adicionada tag de link
  a5dacab adicionadas as tags para o arquivo index
  7b1558c adicionado arquivo HTML ao projeto

Até o momento trabalhamos com o repositório local, na próxima seção veremos como trabalhar no repositório remoto.

Clonando um Repositório Remoto

Para começar a trabalhar com o repositório remoto precisamos clonar o repositório com o comando git clone que cria uma cópia do repositório remoto na qual podemos começar a trabalhar.

Um exemplo disso é o código da Listagem 12, que faz um clone de um projeto alocado no repositório remoto github.

Listagem 12. Clone do projeto

prompt> cd /projetos
  prompt> git clone git://github.com/tswicegood/mysite.git meusite-remote
  Initialized empty Git repository in /work/mysite-remote/.git/
  remote: Counting objects: 12, done.
  remote: Compressing objects: 100% (8/8), done.
  remote: Total 12 (delta 2), reused 0 (delta 0)
  Receiving objects: 100% (12/12), done.
  Resolving deltas: 100% (2/2), done. 

O comando clone possui dois parâmetros: a localização do repositório que queremos clonar e o diretório onde queremos que o projeto seja armazenado. O segundo parâmetro é opcional.

Com isso já é possível trabalhar com o repositório remoto.

Existem outros comandos e parâmetros que serão melhores vistos no próximo artigo que falaremos mais sobre os comandos e como podemos realizar push e fetch no repositório para buscar alterações e realizá-las.

Bibliografia

[1] Travis Swicegood. Pragmatic Version Control Using Git. Bookshelf, 2008.

[2] Git Reference
https://git-scm.com/.

[3] GitHub
https://github.com/.