Operando o git, em poucas palavras (imagem aqui)

Git para dummies

Bernardo Soares
TechRebels

--

Desenvolver código, hoje em dia, se tornou uma habilidade tão importante quanto falar uma língua estrangeira. Cada vez mais os softwares fazem parte da nossa realidade, e a capacidade de customizar uma determinada experiência ou a forma como interagimos com o nosso meio nos permite criar uma série de elementos para nos ajudar no dia a dia. Praticar o desenvolvimento de software de uma maneira bem elaborada traz uma série de benefícios — quer você trabalhe sozinho ou seja parte de uma equipe — uma vez que a qualidade do software que é desenvolvido afeta cada vez mais a rotina das pessoas, seja positiva ou negativamente.

No post de hoje vamos falar sobre o Git — uma ferramenta essencial para controlar o versionamento e revisão de código.

Ao organizar seu código através de repositórios contendo todo o histórico de alteração e dependências (junto à um processo de revisão, testes, etc), os ganhos em qualidade e agilidade são imensuráveis!

Por quê ter um sistema de controle de versão?

Um sistema de controle de versão permite a colaboração de vários desenvolvedores em uma base de código compartilhada, onde as ações podem ou não ocorrer de forma simultânea. Devido à esta natureza dinâmica, é vital que mais de uma pessoa possa trabalhar na mesma base sem afetar o trabalho de outro indivíduo.

Os sistemas que permitem tal forma de trabalho são conhecidos como VCS, ou Version Control System. Existem diferentes tipos de VCS disponíveis: Local VCS, Centralized VCS e Distributed VCS. Apesar das diferenças de design de cada um destes tipos, o importante é saber que com um VCS, é possível reverter arquivos à um estado anterior, reverter o projeto inteiro à um estado anterior, revisar o histórico de alterações, ver quem efetuou uma determinada mudança, e quando essa mudança foi inserida. Isso se aplica a praticamente qualquer tipo de arquivo.

Git

O Git é um sistema que foi desenvolvido em 2005 pelo Linus Torvalds (isso mesmo, o cara do Linux) com o principal objetivo de oferecer um controle de versionamento distribuído e que não tenha sérios problemas de performance ao lidar com uma base de código muito grande. Afinal, o kernel Linux é uma das maiores bases de códigos que existe.

VCS Centralizado (imagem aqui)

O problema maior com o versionamento Centralizado é que o banco de dados de versões do código está localizada no servidor, fazendo com que que boa parte das operações (listar índices, histórico, etc) necessite uma interação com o servidor — o que afeta severamente a performance destas soluções.

VCS Distribuído (imagem aqui)

Já no modelo distribuído, a cópia local dos repositórios possui todas essas informações e referências, trazendo ganhos astronômicos em performance. Não é nenhuma surpresa: este sistema se tornou extremamente popular — tanto que em uma pesquisa em 2018 pela stack overflow reportou que cerca de 88% dos desenvolvedores profissionais utilizam o Git como VCS.

Os Estados — Operações locais

No Git, quase toda operação é feita localmente. Cada arquivo local pode estar em um dos três estados: modified, staged e committed. Um arquivo no estado modified foi recém alterado, porém ainda não foi adicionado na lista de alterações que entrarão no próximo commit. Ao utilizar o comando “git add”, o arquivo no estado modified é adicionado à lista para o próximo commit, e entra em estado staged. Por fim, todas as alterações que estejam em estado staged fazem parte do snapshot do commit individual ao se usar o comando “git commit” — movendo os arquivos para o estado committed.

Estas mudanças no estado dos arquivos também têm relação com o estado do repositório local, onde cada alteração deste estado vai refletir a versão do repositório em que estamos trabalhando.

Estados do repositório local (aqui)

A partir do repositório principal, pode-se selecionar em qual versão do repositório vamos trabalhar em um determinado momento — essa operação é conhecida como “checkout”. O processo de checkout carrega todos os estados de arquivos correspondentes àquele commit e sincroniza o nosso “working directory”. Todas as alterações adicionadas do nosso “working directory” são listadas na área de “staging”, e o repositório principal é sincronizado com o que tínhamos na área de “staging” após efetuarmos o commit.

Operações remotas

Até agora, vimos que boa parte do trabalho é feita local. Mas se quisermos compartilhar o trabalho ou contribuir com algum projeto, algumas operações remotas são necessárias. Essas operações são o que tornam a colaboração possível — por meio da capacidade de sincronizar os snapshots criados localmente com o repositório em algum servidor. Como vimos na seção anterior, boa parte da complexidade é na parte local do repositório — fazendo com que a interação com as partes remotas sejam simplificadas. Em outras palavras, operações remotas se resumem à sincronizar os snapshots locais e remotos, através das operações “fetch”, “pull”, e “push”.

Workflows

São, literalmente, o método utilizado para interagir com o código. Em outras palavras, os workflows vão refletir a forma como o código é gerenciado e como seu ciclo de vida funciona. Dois conceitos fundamentais de workflow são o Branching e o Forking. Vamos falar um pouco destas duas funcionalidades sem entrar muito em detalhe na parte processual de workflows. Caso tenha interesse em alguns modelos, este link se refere aos mais comuns.

Branching

Em um determinado repositório, a linha principal de código é conhecida como “master branch”, ou somente “master”. Uma das maneiras de divergir desta linha principal de código é criando um “branch”. Este branch encapsula todas as alterações em uma linha independente do master, que pode ser testada e revisada de forma totalmente isolada (mas ainda sim, sob o mesmo repositório). Após os devidos testes e revisões, este branch pode ser incorporado (ou merge) ao master.

Feature Branch Workflow (imagem aqui)

Na imagem acima, podemos ver que cada feature inserida na linha principal (master) possui um branch correspondente, originado a partir de diferentes estados do branch master.

Forking

O Forking permite divergir do master a partir da criação de uma cópia independente do repositório a ser clonado. Essa prática é mais utilizada quando o organizações distintas trabalham no mesmo repositório — como em projetos open source. Desta forma, cada alteração é feita em um repositório independente, e possivelmente submetido “upstream” para aprovação e merge. A partir daí, basta cada desenvolvedor sincronizar fork com o repositório principal para atualizar o estado do fork.

Vários usuários interagindo com seus respectivos forks, e submetendo alterações upstream (imagem aqui)

Ainda sob o mesmo fork, podemos criar vários branches para trabalhar simultaneamente. Ou seja, trabalhar em um fork não te impede de usar branches para gerenciar melhor as alterações.

Na prática

Para ilustrar alguns dos conceitos acima, vamos criar um repositório e simular algumas ações. Localmente, tenho esse programinha chamado “pingmcast.py” que envia ou escuta pacotes multicast para um determinado grupo. O detalhamento do código não é o foco aqui, e sim o processo de fazer com que esse simples diretório se torne um repositório.

Primeiro, vamos criar o diretório “pingmcast” que irá representar o nosso repositório. Esse diretório será como a raiz (root) do projeto — e toda a estrutura necessária para suportar este projeto (como bibliotecas, requisitos, manuais, exemplos, etc) vão ser organizados a partir daí.

bersoare➜/opt/localsrc/git» mkdir pingmcast
bersoare➜/opt/localsrc/git» cd pingmcast
bersoare➜localsrc/git/pingmcast»
bersoare➜localsrc/git/pingmcast» tree
.
├── README.md
└── pingmcast.py
0 directories, 2 files

Perfeito! Agora que já temos o diretório e os arquivos básicos, podemos inicializar o repositório localmente com o comando “git init” dentro do diretório atual. Como não passei o diretório como um argumento para o comando, ele vai por default usar o diretório atual para criar o repositório.

bersoare➜localsrc/git/pingmcast» git init
Initialized empty Git repository in /opt/localsrc/git/pingmcast/.git/
bersoare➜localsrc/git/pingmcast(master✗)» ls -alh
total 16
drwxr-xr-x 5 bersoare staff 160B Nov 7 14:54 .
drwxr-xr-x 5 bersoare staff 160B Jun 4 07:55 ..
drwxr-xr-x 9 bersoare staff 288B Nov 7 14:54 .git
-rw-r--r-- 1 bersoare staff 471B Jun 4 08:05 README.md
-rw-r--r--@ 1 bersoare staff 2.3K Jun 4 07:56 pingmcast.py

O comando “git init” inicializou um repositório criando o diretório “.git”. Este diretório contém todas as informações iniciais de tracking para o nosso repositório. À medida que o projeto vai sendo alterado, todos os meta dados referentes à commits e às mudanças são armazenados neste diretório.

Os arquivos que estavam no diretório ainda não são parte do repositório, pois o que fizemos até o momento foi apenas a inicialização do repositório. Quando ainda não existem referências à um arquivo (isto é, ele ainda não foi adicionado ao repositório), ele fica em estado Untracked. Podemos comprovar isto com o comando “git status”:

bersoare➜localsrc/git/pingmcast(master✗)» git status
On branch master
No commits yetUntracked files:
(use "git add <file>..." to include in what will be committed)
README.md
pingmcast.py
nothing added to commit but untracked files present (use "git add" to track)

Agora podemos adicionar os arquivos e criar o nosso primeiro commit.

bersoare➜localsrc/git/pingmcast(master✗)» git add README.md
bersoare➜localsrc/git/pingmcast(master✗)» git add pingmcast.py
bersoare➜localsrc/git/pingmcast(master✗)» git commit -m "initial commit, adding readme and script"
[master (root-commit) 7b0fd70] initial commit, adding readme and script
2 files changed, 109 insertions(+)
create mode 100644 README.md
create mode 100644 pingmcast.py

Após o comando “git commit”, vemos que o nosso repositório está com o HEAD no commit que fizemos acima, efetivamente inicializando a estrutura do repositório. Se usarmos o comando “git show”, podemos ver o commit e os detalhes das alterações:

bersoare➜localsrc/git/pingmcast(master)» git show
commit 7b0fd70ed157c45166937bdf29e90cb759a9a618 (HEAD -> master)
Author: Bernardo
Date: Thu Nov 7 14:58:29 2019 -0300
initial commit, adding readme and scriptdiff --git a/README.md b/README.md
new file mode 100644
index 0000000..6507f5c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,16 @@
<snip>
diff --git a/pingmcast.py b/pingmcast.py
new file mode 100644
index 0000000..6c19be4
--- /dev/null
+++ b/pingmcast.py
<snip>

Note que tudo o que fizemos até o momento foi executado localmente. Isso é, não associamos o nosso repositório local à um repositório remoto. Após criar o repositório remotamente*, vamos associar o repositório remoto ao nome “origin” — para facilitar na hora de referenciar o remoto. Podemos usar o comando “git remote add” para adicionar a referência, e visualizar o resultado com o comando “git remote -v”.

*Para o exemplo, foi utilizado o GitHub, e criado apenas um repositório vazio com o mesmo nome . Para mais detalhes, veja aqui.

bersoare➜localsrc/git/pingmcast(master)» git remote add origin git@github.com:bersoare/pingmcast.git
bersoare➜localsrc/git/pingmcast(master)» git remote -v
origin git@github.com:bersoare/pingmcast.git (fetch)
origin
git@github.com:bersoare/pingmcast.git (push)

Excelente! Agora que já associamos o nosso local com o remoto no GitHub, podemos fazer um “push”, para que nosso commit esteja disponível no servidor (nota: a parte de autenticação e permissões já tinham sido configuradas anteriormente!):

bersoare➜localsrc/git/pingmcast(master)» git push origin master
Warning: Permanently added the RSA host key for IP address '140.82.114.3' to the list of known hosts.
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 8 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 1.41 KiB | 1.41 MiB/s, done.
Total 4 (delta 0), reused 0 (delta 0)
To github.com:bersoare/pingmcast.git

Um detalhe que podemos ver é que no comando “git log” o HEAD de ambos os repositórios — tanto local quanto remoto — apontam para o mesmo commit:

bersoare➜localsrc/git/pingmcast(master)» git log
commit 7b0fd70ed157c45166937bdf29e90cb759a9a618 (HEAD -> master, origin/master)
Author: Bernardo
Date: Thu Nov 7 14:58:29 2019 -0300
initial commit, adding readme and script
(END)

Agora, acabei de perceber que não tenho o arquivo de requirements :(. Para tornar mais fácil a vida de quem for utilizar este script no futuro, vamos adicionar o arquivo de requirements no repositório. Caso não tenha familiaridade com Python, o arquivo de requirements é a lista de bibliotecas que nosso projeto usa.

bersoare➜localsrc/git/pingmcast(master)» echo "ipaddress==1.0.18" > requirements.txt
bersoare➜localsrc/git/pingmcast(master✗)» tree
.
├── README.md
├── pingmcast.py
└── requirements.txt
0 directories, 3 files

Maravilha! Agora, com nosso arquivo em mãos, podemos criar um branch para encapsular toda essa alteração:

bersoare➜localsrc/git/pingmcast(master✗)» git checkout -b "requirements_file"
Switched to a new branch 'requirements_file'

bersoare➜localsrc/git/pingmcast(requirements_file✗)» git status
On branch requirements_file
Untracked files:
(use "git add <file>..." to include in what will be committed)
requirements.txtnothing added to commit but untracked files present (use "git add" to track)bersoare➜localsrc/git/pingmcast(requirements_file✗)» git add requirements.txtbersoare➜localsrc/git/pingmcast(requirements_file✗)» git status
On branch requirements_file
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: requirements.txt

Após criar o branch, podemos ver com o comando “git status” o nome do branch atual, e também quais as alterações foram efetuadas no nosso repositório local . Com o comando “git add”, podemos fazer com que um arquivo em estado modified (ou new) entre em estado staged — para que possa ser incluído no nosso commit. Após efetuar o “commit”, vemos agora que o HEAD do nosso branch “requirements_file” corresponde à este novo commit:

bersoare➜localsrc/git/pingmcast(requirements_file✗)» git commit -m "adding requirements file"
[requirements_file 5afbda5] adding requirements file
1 file changed, 1 insertion(+)
create mode 100644 requirements.txt
bersoare➜localsrc/git/pingmcast(requirements_file)» git log
commit 5afbda5a69ffa8ecc64249e0e86c90fa986be952 (HEAD -> requirements_file)
Author: Bernardo
Date: Thu Nov 7 15:17:42 2019 -0300
adding requirements filecommit 7b0fd70ed157c45166937bdf29e90cb759a9a618 (origin/master, master)
Author: Bernardo <bersoare@cisco.com>
Date: Thu Nov 7 14:58:29 2019 -0300
initial commit, adding readme and script

Finalmente, podemos fazer o push deste branch para o nosso “origin” remoto.

bersoare➜localsrc/git/pingmcast(requirements_file)» git push origin requirements_file
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 343 bytes | 343.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
remote:
remote: Create a pull request for 'requirements_file' on GitHub by visiting:
remote:
https://github.com/bersoare/pingmcast/pull/new/requirements_file
remote:
To github.com:bersoare/pingmcast.git
* [new branch] requirements_file -> requirements_file

Bem, vamos fazer o que o git nos mandou e vamos ao GitHub. Ao ir para o repositório, podemos criar um “pull request” usando o Branch — que contém o nosso arquivo recém adicionado:

Dessa forma, permitimos uma revisão das alterações, ou mesmo um pipeline de CI para testar as alterações e reduzir a probabilidade de algum negativo no projeto. Após efetuar o merge desta pull request, atualizamos o nosso repositório local com o comando “git pull”, onde podemos ver os novos branches que foram incorporados ao master — e também vemos HEAD do origin/master e do master local apontando para o commit correspondente à ação de merge da Pull Request criada acima:

bersoare➜localsrc/git/pingmcast(master)» git status
On branch master
nothing to commit, working tree clean
bersoare➜localsrc/git/pingmcast(master)» git pull origin master
From github.com:bersoare/pingmcast
* branch master -> FETCH_HEAD
Updating 7b0fd70..028e7c9
Fast-forward
requirements.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 requirements.txt
bersoare➜localsrc/git/pingmcast(master)» git log
commit 028e7c9e9e13eb4a51b45a56b94cafb02cd235b8 (HEAD -> master, origin/master)
Merge: 7b0fd70 5afbda5
Author: bersoare <bsoares.it@gmail.com>
Date: Thu Nov 7 17:15:00 2019 -0300
Merge pull request #1 from bersoare/requirements_fileadding requirements filecommit 5afbda5a69ffa8ecc64249e0e86c90fa986be952 (origin/requirements_file, requirements_file)
Author: Bernardo <bersoare@cisco.com>
Date: Thu Nov 7 15:17:42 2019 -0300
adding requirements filecommit 7b0fd70ed157c45166937bdf29e90cb759a9a618
Author: Bernardo <bersoare@cisco.com>
Date: Thu Nov 7 14:58:29 2019 -0300
initial commit, adding readme and script

Nota: Tive que especificar o branch e repositório remotos pois ainda não tinha associado o meu master local com o master remoto. Para evitar que isto ocorra, pode-se utilizar o comando git branch — set-upstream-to=origin/master master.

Simples, não é!? Agora você está pronto para criar um repositório para seus projetos e ter aquele ganho em agilidade!

Se gostou do conteúdo, peço para compartilhar com outros do ramo. Não se esqueça de seguir a mim e ao TechRebels clicando follow aí embaixo :)

Sobre o autor:

Bernardo, CCIE #57862

Cloud Network Engineer

linkedin.com/in/bernardosoares/

--

--