Curso de Linux - Parte III - Aula de Git

Olá gente, neste post veremos algumas noções básicas sobre o Git e o Git Lab. Uma pergunta que pode surgir é para que serve o git? Bom, o git é uma ferramenta de versionamento amplamente utilizada, que consegue lidar eficientemente com arquivos de textos “legíveis” para seres humanos. Como projeto, fazeremos um testador para os laboratórios de MC102, usando as linguagens python e bash script. Assumiremos que o leitor possui alguma familiaridade com essas duas linguagens.

Instalação dos pacotes necessários

Para esse curso precisaremos dos seguintes pacotes: git, vim, pip3 e gdown, que para o Ubuntu/ Debian podem ser instalados com:

sudo apt install git
sudo apt install vim
sudo apt install python3-pip
pip3 install gdown

Se você utilizar o Fedora, troque apt por dnf. Para contextualizar, o git é a ferramenta de versionamento, o vim é um editor de texto, o pip3 é um gerenciador de pacotes do python e o gdown é um pacote que utilizaremos para baixar arquivos do google drive. Pronto, agora podemos começar.

Organização

Este post está organizado da seguinte forma:

  • Configuração do SSH no GitLab
  • Criando o repositório git localmente
  • Enviando nosso repositório local para o GitLab
  • Criando uma branch
  • Baixando os arquivos e usando o git ignore
  • Fazendo um merge request
  • Executando nossa solução do lab
  • Guardando o output gerado pela nossa solução
  • Comparando a saída de nosso algoritmo com a saída esperada
  • Outros comandos úteis do git

Configuração do SSH no GitLab

Em nossa primeira etapa, precisamos de uma conta no GitLab, e configuraremos em nosso computador uma chave ssh para nos comunicar com o GitLab. Para aqueles que não possuem uma conta no gitlab, a mesma pode ser criada em https://gitlab.com/users/sign_up .

No linux, temos o padrão de usar uma pasta de configurações de ssh na home de cada usuário, chamada .ssh, note que está é uma pasta oculta. Podemos mudar para ela usando o comando:

cd ~/.ssh

Se você receber um erro que tal pasta não existe, então podemos usar a seguinte combinação de comandos, que criará a pasta antes de mudar para a mesma:

mkdir ~/.ssh; cd ~/.ssh

Agora criaremos uma chave ssh do tipo ed25519, usando o comando:

ssh-keygen -t ed25519

Então, será pedido um nome para o arquivo, neste projeto sugerimos que use gitlab_key. Em seguida, será pedido uma senha para a chave, que será exigida na hora de nos comunicarmos por ssh com o GitLab. Pronto, para checar se tudo deu certo podemos dar um dir -l, e devemos obter como saída algo como:

total 8
-rw------- 1 renan renan 464 jun  8 04:50 gitlab_key
-rw-r--r-- 1 renan renan 104 jun  8 04:50 gitlab_key.pub

Onde gitlab_key é uma chave privada, e gitlab_key.pub é uma chave pública. Resumidamente o ssh trabalha com um esquema de par de chaves pública e privada, onde com a chave pública conseguimos encriptografar mensagens, e com a chave privada conseguimos além encriptografar mensagens, também conseguimos desencriptografa-las (criptografia está que pode ser feita tanto pela chave pública como pela própria chave privada).

Para nos comunicar compartilharemos sempre a chave pública, e veremos como fazer isso com o GitLab.

Jamais compartilhe a sua chave privada com ninguém !!!

Com GitLab aberto no seu navegador, clique no seu usuário (botão no canto superior direito), então clique em preferências. Nesta nova página, terá uma barra lateral esquerda, então clique em SSH Keys. Deverá abrir uma tela assim:

Então copie o conteúdo da sua chave pública, isso pode ser realizado com o comando cat gitlab_key.pub e copiando o conteúdo exibido no terminal. Em seguida, cole o conteúdo na caixa keys do GitLab, coloque um nome para sua chave na caixa Title, e clique em AddKey.

Para testar se tudo foi configurado corretamente, use o comando ssh -T git@gitlab.com, como será a primeira vez que estamos autenticando, receberemos uma mensagem de confirmação. Digite yes, e se tudo estiver correto teremos como saída:

The authenticity of host 'gitlab.com (172.65.251.78)' can't be established.
ED25519 key fingerprint is SHA256:eUXGGm1YGsMAS7vkcx6JOJdOGHPem5gQp4taiCfCLB8.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'gitlab.com' (ED25519) to the list of known hosts.
Welcome to GitLab, @SEU_NOME_DE_USUARIO!

Pronto, finalmente nossa chave ssh está configurada no GitLab.

Criando o repositório git localmente

Ao usar o git pela primeira vez precisamos configurar um nome de usuário e nosso endereço de email, para tal use os comandos:

git config --global user.name "Fulano de Tal"
git config --global user.email fulanodetal@exemplo.br

Começaremos a colocar a mão na massa, e a desenvolver nosso projeto. Para tal, crie uma pasta para o projeto onde você julgar mais conveniente. Por exemplo, use o comando:

mkdir ~/projeto-aula-git; cd ~/projeto-aula-git

Para iniciar o git dentro deste diretório, use:

git init

Comumente, repositórios git possuem um arquivo chamado README.md na raiz, que é renderizado automaticamente por servidores remotos de git, como o GitLab ou GitHub. Crie esse arquivo com algum editor de texto (como gedit ou vim), por exemplo:

vim README.md

A extensão .md representa arquivos que usam Markdown Syntax, que é uma sintaxe usada para padronizar e facilitar formatação de texto na web. Um exemplo de conteúdo para esse arquivo criado é:

# Meu primeiro projeto usando o git
## Testador de labs de MC102
 
Neste projeto fazeremos um testador para os labs da disciplina de MC102

A primeira linha representa um título, a segunda um subtítulo e a terceira um texto. Quando enviarmos para o remoto nosso projeto, podemos observar o texto renderizado corretamente. Alguns editores de texto, como o Visual Studio Code possuem preview para arquivos .md. Além disso, para quem quiser saber mais sobre a Markdown Syntax podem acessar o tutorial.

Para ver o status atual do nosso repositório git, podemos usar o comando git status, e teremos como saída algo como:

On branch master
 
No commits yet
 
Untracked files:
 (use "git add <file>..." to include in what will be committed)
       README.md
 
nothing added to commit but untracked files present (use "git add" to track)

Note que em arquivos não-rastreados (untracked files), temos o nosso arquivo README.md. Para rastreá-lo, podemos usar o comando git add README.md. Repetindo o comando git status, temos:

On branch master
 
No commits yet
 
Changes to be committed:
 (use "git rm --cached <file>..." to unstage)
       new file:   README.md

Veja que agora o README.md está como “Changes to be committed”, ou seja, o mesmo está na área de arquivos staged. Na área staged temos todas as modificações que queremos incluir em nosso próximo commmit.

Além do git add <file>, outras possibilidades bastante utéis, são os comandos:

  • git add . : Adiciona todas as modificações contidas no diretório atual e subdiretórios no stage
  • git add -A : Adiciona todas as modificações do repositório git no stage

Quando tivermos concluído uma tarefa, podemos tornar as mudanças do stage em algo mais definitivo chamado commit. Um commit é nada mais que uma maneira eficiente o estado atual do nosso repositório ao aplicar as mudanças do stage. Para cada commit será criado um hash code, que depende de uma série de fatores, dentre eles os arquivos do repositório e as mudanças do stage. Além disso, o hash code tende a ser único, e qualquer mudança mínima, o mudará completamente.

Para fazer o commit usamos o comando git commit, então o editor de texto nano será aberto, e poderemos escrever uma mensagem, para um leitor futuro, indicando o propósito daquelas modificações. Há um padrão para tais mensagens, onde a primeira linha deve ser uma descrição sucinta do que foi feito, a segunda linha deverá estar em branco, e a partir da terceira linha poderemos descrever em maiores detalhes as alterações. Segue um exemplo:

Para salvar a mensagem no nano use CTRL + X, digite y e então tecle enter. Dando um git status, veremos que a stage está limpa. Para visualizarmos a lista de commits passados podemos dar um git log, para obtermos uma tela assim:

Uma informação relevante é o número 32f6138f0d2dbe77397c776b57adc2944fdf34a9, que corresponde ao hash code do nosso commit. Além disso, entre parênteses temos HEAD -> master, onde HEAD indica o commit que estamos visualizando atualmente, e master indica o nome da nossa branch atual (mais adiante explicamos o que é uma branch). Por fim, podemos ver os commits sucintamente (ocultando o corpo das mensagens) com o comando git log --oneline.

Enviando nosso repositório local para o GitLab

Nesta etapa, criaremos um repositório remoto no GitLab e o sincronizaremos com nosso repositório local. Com o GitLab aberto, vá no Menu, então clique em Your Projects. Na página aberta, clique em New project, em seguida, clique em Create blank project. Uma tela como está deverá ser aberta:

Então, coloque um nome para o seu projeto. Em Project URL precisamos de um namespace, marque seu próprio usuário. Por fim, selecione o nível de visibilidade desejada no projeto, e clique em Create Project (que está ao fim da página). Você será redirecionado para a tela inicial do seu repositório que deverá ser assim:

Clique em clone, e copie o link para o ssh com o botão mostrado a seguir:

Agora, execute os comandos, substituindo o LINK_SSH pelo link copiado:

git remote add origin LINK_SSH
git branch -M main
git push -uf origin main

O primeiro comando, adicionará um novo servidor remoto, que será localmente chamado de origin. O segundo comando renomeia a branch atual para main. Já o terceiro comando desempenha vários papéis. O git push envia as mudanças do repositório local para o repositório remoto. Contudo, executando apenas um git push, teremos o erro:

fatal: The current branch main has no upstream branch.
To push the current branch and set the remote as upstream, use
 
 git push --set-upstream origin main

Nos dizendo que a branch local atual não possui uma branch upstream no servidor remoto no momento. A sugestão dada pelo git git push --set-upstream origin main é equivalente a git push -u origin main. Mas se executarmos apenas tal comando, receberemos um erro dizendo que nosso servidor remoto possui commits que não existe localmente, e devido a isso não podemos fazer push. Neste ponto que entra a flag -f, sendo a abreviação da flag --force, que faz a branch do remoto ser sobrescrita de maneira a se tornar idêntica a branch local.

Contudo, teremos o seguinte erro executando o comando git push -uf origin main:

Esse erro acontece porque a branch principal (main) do GitLab possui proteção contra --force. Para desabilitá-la temporariamente podemos pelo menu lateral esquerdo ir em Settings -> Repository, depois expandir Protected branches, e então habilitar allowed to force push, conforme a imagem:

Agora tudo correrá bem com o comando git push -u origin main. Por fim, desabilite o force novamente, e pedimos que tome cuidado com qualquer comando que envolva o force, pois o mesmo é irreversível.

Pronto, agora se você ir à página inicial do seu projeto no GitLab, o README que criamos estará anteriormente reproduzido na mesma.

Criando uma branch

Nesta etapa falaremos sobre branches. Uma branch nada mais é que um ponteiro para um commit. Usando o comando git log, vemos que tanto a branch main (local), como a branch origin/main do servidor remoto estão apontando para o único commit que temos no nosso repositório.

Uma prática recomendável é criamos uma nova branch sempre que formos implementar uma nova funcionalidade (feature) em um projeto, realizamos todas as alterações necessárias nessa nova branch, e então fazermos um merge request para a branch main, onde aplicaremos as mudanças feitas na branch main todas de uma vez. Isso é uma boa prática, pois assim podemos manter a branch main sempre com código funcional.

A seguir, lidamos com download dos arquivos do laboratório. Para criar uma branch chamada download-files, basta usar o comando git branch download-files. Você pode checar com o git log, que a branch download-files existe e ela aponta para o mesmo commit que a branch atual.

Uma maneira lista todas as branches incluindo as remotas é com o comando git branch -a. Se o mesmo repositório remoto estiver sendo manipulado de vários repositórios locais, é importante usarmos um git fetch antes. Tal comando tem por função deixar o nosso repositório local cientes das modificações que ocorreram no remoto, por exemplo, a existência de novas branches (criadas por outros repositórios locais).

Para mudarmos para a branch download-files usamos o comando git checkout download-files. Usando o comando git log vemos que o HEAD mudou para a download-files. Além disso, podemos enviar essa nova branch para o remoto com o comando git push -u origin download-files.

Baixando os arquivos e usando o git ignore

Para baixar arquivos de input/output do laboratório de MC102, que estão numa pasta google drive, utilizaremos o pacote gdown do pip3. Crie um arquivo chamado downloadScript.py com o conteúdo:

import gdown
import os
from os import path
 
outputDirName = "Lab1"
url = "https://drive.google.com/drive/folders/1YzNMG2T9pCUlB-_jCEKMQPyX_4BTmYfk"
 
if not path.isdir(outputDirName):
 gdown.download_folder(url = url, output = outputDirName)
 
if os.system("dir " + outputDirName + "/*_in.txt > /dev/null 2&>1") == 0:
 # converter a extensao dos arquivos
 os.system("for file in $(ls -v " + outputDirName + "/*_in.txt); do PREF=$(echo $file | rev | cut -c 8- | rev); mv $file $PREF\".in\"; mv $PREF\"_out.txt\" $PREF\".out\"; done")

Então execute o script com python3 downloadScript.py, e uma pasta Lab1 será criada com os arquivos do drive da url.

Usando o comando git status, teremos os arquivos não rastreados Lab1/ e downloadScript.py. Contudo, não é interessante ficarmos enviando esses arquivos de input/output para nosso servidor remoto. Portanto, podemos utilizar a funcionalidade do git de ignorar arquivos, que consistem em criar um arquivo na raiz do nosso diretório local chamado .gitignore, que conterá todos os arquivos que desejamos ignorar, neste caso Lab1/. Dando um git status novamente, observe que Lab1/ não aparece mais como arquivos não rastreado.

Podemos commitar as mudanças com:

git add -A
git commit -m "add gitignore and downloadScript.py"

Observe dessa vez commitamos com a flag -m, que nos permite escrever o resumo logo em seguida usando aspas, e sem que o editor nano seja aberto.

Em seguida, envie as mudanças para o remoto com git push. Usando um git status temos que a branch download-files está um commit a frente em relação à branch main.

Fazendo um merge request

Uma vez tivermos uma feature pronta, podemos enviá-la para a branch main por um merge. Um merge envolve aplicar as mudanças de uma branch fonte em uma branch destino. Se estivermos trabalhando em equipe, geralmente um merge onde destino é main é feito por um merge request. Um merge request é um sistema de repositórios remotos (como o GitLab e GitHub), que permite que outros programadores analise as mudanças feitas, e deêm sugestões de melhoria, antes de realizarmos o merge de fato.

Na página do seu projeto no gitLab, clique em Merge request no menu lateral esquerdo, depois em Create merge request. Então, temos a tela:

Clicando em Change branches podemos escolher a branch fonte e a branch de destino, mas neste caso ambas estão corretamente selecionadas. Podemos escrever um título mais adequado e também acrescentar uma descrição sobre o que foi feito e o porquê. Mais abaixo na página, temos o Assignee e o Reviewer, onde o Assignee seria uma pessoa responsável que pode tanto fazer a revisão do código como efetuar o merge request de fato, e o Reviewer seria alguém responsável apenas pela revisão. Por fim, clique em Create merge request.

Agora no menu lateral a contagem se merge requests será um, e abrindo tal merge request e clicando em Changes, temos a tela:

Na esquerda temos os arquivos alterados, onde o ‘+’ representa quantas linhas foram adicionadas no arquivo e o ‘-’, quantas linhas foram removidas. Neste caso, só acrescentamos informações em ambos os arquivos.

Em seguida, clique em Overview, e depois no botão de merge (ao fim da página). Neste ponto, a branch main no remoto já possui os dois commits, mas usando o comando git log não vemos isso. Para ficarmos a par do que está no remoto usamos o git fetch. Para atualizarmos a nossa branch main usamos os comandos

git checkout main
git pull

Onde o primeiro comando muda para a branch main, e o segundo comado puxa as alterações do remoto. Usando um git log, vemos um novo commit, que foi criado no merge request.

Neste ponto a branch download-files não é mais necessária, então podemos deletar a sua versão local com o comando git branch -D download-files. Caso a branch remota também tiver sido deletada, podemos deixar de listá-la fazendo git fetch --prune.

Executando nossa solução do lab

Agora, faremos um script que executa nossa solução do laboratório com todos os casos de testes. Note que todos os arquivos de entrada terminam com “.in”, logo podemos filtrá-los e listá-los em ordem natural com ls -v Lab1/*.in (assumindo que estamos na raiz do nosso projeto).

Antes de tudo, vamos criar uma nova branch script, pois como já dissemos, não é uma boa prática implementar direto na main. Uma maneira mais sucinta de fazer isso é utilizar o comando git checkout -b "script", onde a flag -b indica para o checkout que ele deve criar a branch antes de mudar para a mesma.

Criaremos então nosso script para executar nossa solução com todas as entradas. Para tal, crie um arquivo run_all.sh, dê a permissão de execução com o comando chmod +x run_all.sh, e coloque o contéudo:

for file in $(ls -v Lab1/*.in)
do
 echo "Running test case: "$file
 python3 $1 < $file
done

Para fins de teste, crie um arquivo lab01.py, com o conteúdo:

input()
input()
value = float(input())
print("%.2f" % (value * 0.5))
print("Agradecemos a preferência, tenha um ótimo fim de semana!")

Tal código acerta apenas dois casos de teste. Para testar nosso script, fazemos ./run_all.sh lab01.py. Note que a string “lab01.py” é o primeiro argumento do comando, portanto, $1 será substituída pela mesma. Como saída do script teremos:

Running test case: Lab1/1.in
26.00
Agradecemos a preferência, tenha um ótimo fim de semana!
Running test case: Lab1/2.in
22.00
Agradecemos a preferência, tenha um ótimo fim de semana!
Running test case: Lab1/3.in
13.50
Agradecemos a preferência, tenha um ótimo fim de semana!
Running test case: Lab1/4.in
6.25
Agradecemos a preferência, tenha um ótimo fim de semana!
Running test case: Lab1/5.in
36.82
Agradecemos a preferência, tenha um ótimo fim de semana!
Running test case: Lab1/6.in
19.25
Agradecemos a preferência, tenha um ótimo fim de semana!
Running test case: Lab1/7.in
32.86
Agradecemos a preferência, tenha um ótimo fim de semana!
Running test case: Lab1/8.in
28.50
Agradecemos a preferência, tenha um ótimo fim de semana!
Running test case: Lab1/9.in
2.36
Agradecemos a preferência, tenha um ótimo fim de semana!
Running test case: Lab1/10.in
36.95
Agradecemos a preferência, tenha um ótimo fim de semana!
Running test case: Lab1/11.in
45.16
Agradecemos a preferência, tenha um ótimo fim de semana!
Running test case: Lab1/12.in
12.72
Agradecemos a preferência, tenha um ótimo fim de semana!

Podemos fazer um commit com git add -A; git commit -m "Add script to run all test cases", e você pode checar a sua criação com git log.

Guardando o output gerado pela nossa solução

Agora fazemos nosso script guardar a saída do nosso lab01.py em um arquivo com o mesmo prefixo que o arquivo entrada, mas com o suffixo .ans. Começaremos gerando o prefixo, para tal, temos o comando cut, e podemos fazer:

echo "Lab1/1.in" | cut -d "." -f 1

Onde, dado a string de entrada “Lab1/1.in”, cortamos ela no “.” e ficamos com a primeira metade, obtendo a string “Lab1/1”. Para imprimir todos os prefixos, podemos fazer:

for file in $(ls -v Lab1/*.in); do echo $file | cut -d "." -f 1; done

Em seguida, guardaremos o prefixo em uma variável PREF, então criar uma variável ANS que contatena PREF com “.ans”, usando o comando:

for file in $(ls -v Lab1/*.in); do PREF=$(echo $file | cut -d "." -f 1); ANS=$PREF".ans"; echo $ANS; done

Agora podemos atualizar nosso script, e fazê-lo redimensionar a saída para esse arquivo. O script run_all.sh fica assim:

for file in $(ls -v Lab1/*.in)
do
 echo "Running test case: "$file
 PREF=$(echo $file | cut -d "." -f 1)
 ANS=$PREF".ans"
 python3 $1 < $file > $ANS
done

Executando o script com ./run_all.sh lab01.py, podemos checar que nossa pasta Lab01 agora possui arquivos com a extensão “.ans”.

Podemos commitar as mudanças com git add run_all.sh; git commit -m "update the script to store the output". Além disso, esquecemos de enviar a banch script para o servidor remoto, podemos fazer isso com git push -u origin script.

Comparando a saída de nosso algoritmo com a saída esperada

Para comparar dois arquivos no linux podemos utilizar o comando diff. Por exemplo, diff -w --color Lab1/2.ans Lab1/2.out, compara os arquivos Lab1/2.ans e Lab1/2.out, e temos como saída:

1,2c1,2
< 22.00
< Agradecemos a preferência, tenha um ótimo fim de semana!
---
> 33.00
> Agradecemos a preferência, tenha uma ótima sexta-feira!
\ No newline at end of file

Onde a flag -w ignora diferenças envolvendo espaços, endline etc, e a flag --color habilita as cores do diff. Para executarmos automaticamente o diff entre todos os arquivos .ans e .out podemos atualizar nosso run_all.sh para:

for file in $(ls -v Lab1/*.in)
do
 echo "Running test case: "$file
 PREF=$(echo $file | cut -d "." -f 1)
 ANS=$PREF".ans"
 OUT=$PREF".out"
 python3 $1 < $file > $ANS
 diff -w --color $ANS $OUT
done

Além disso, podemos acrescentar uma mensagem indicando que nossa solução passou nos testes ou não, fazendo:

RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m' # No Color
 
for file in $(ls -v Lab1/*.in)
do
 echo -n "Running test case: "$file" "
 PREF=$(echo $file | cut -d "." -f 1)
 ANS=$PREF".ans"
 OUT=$PREF".out"
 python3 $1 < $file > $ANS
 if diff -w $ANS $OUT &>/dev/null ; then
   echo -e "${GREEN}OK${NC}"
 else
   echo -e "${RED}Fail${NC}"
   diff -w --color $ANS $OUT
   echo ""
   #break
 fi
done

Tirando o comentário do break, isto é, retirando o #, nosso script irá parar no primeiro teste que falhar. Agora podemos commitar e enviar as mudanças para o remoto, usando git add -A; git commit -m "Do the script compare the answer with the expected output"; git push.

Outros comandos úteis do git

Há alguns comandos que deixamos de fora, mas que também são utilizados com frequência, segue alguns dos mesmos:

  • git log --graph: Cria uma representação gráfica das branches e merges entre as mesmas.
  • git restore file: Desfaz as alterações de um arquivo rastreado.
  • git restore ---staged file: Retira as alterações de um arquivo rastreado da stage.
  • git commit --amend: “Remenda” o último commit, acrescentando as mudanças atuais da stage no mesmo. Cuidado!!! Pois, isso altera o hashcode, e se o commit já estiver no remoto isso gerará inconsistências.
  • git checkout file: Desfaz as alterações de um arquivo rastreado.
  • git checkout hashcode: Muda o HEAD para o commit correspondente ao hashcode.
  • git checkout branch: Muda o HEAD para o commit apontado pela branch.
  • git checkout hashcode -- file: Utiliza a versão do arquivo corresponde ao commit do hashcode.
  • git merge source: Faz um merge localmente, usando a branch atual como target
  • git merge --abort: Caso utilizarmos o comando acima e houver conflitos de arquivos, podemos utilizar esse comando para desistir do merge.
  • git add -a: Adiciona para a stage todas as alterações efetuadas em arquivos rastreados.
  • git clean -f: Deleta todos os arquivos não rastreados.
  • git clean -fn: Mostra quais arquivos seriam deletados pelo comando acima.
  • git stash: Coloca as mudanças da stage em uma pilha, e a limpa. É útil para quando queremos mudar de branch.
  • git stash pop: Recupera as últimas mudanças colocadas pelo comando acima, e retira tais mudanças da pilha.
  • git stash clean: Limpa a pilha da stash.

Para usuários mais avançados, cabe pesquisar sobre:

  • git rebase
  • git pull --rebase
  • git reflog
  • git reset (--hard)
Cuidado com esses comandos, principalmente o rebase e o reset --hard !!!

E chegamos ao fim de nosso tutorial, obrigado por ler =).