Utilizando o Git

Contribuíram neste tutorial Ieremies, Matheus Otávio Rodrigues

Utilizando o Git eficientemente

Neste curso serão utilizados a ferramenta de controle de versão Git em conjunto com a interface do Gitlab no IC. Na disciplina, utilizaremos apenas comandos básicos do Git, então é necessário entender alguns conceitos-chaves, como clonar repositórios, realizar commit, push e pull. Já o Gitlab funcionará apenas como repositório remoto e você não precisará tomar nenhuma ação adicional ou realizar outras configurações.

Embora a necessidade de utilizar gerenciamento de versão na disciplina seja reduzida, é importante aprender a utilizar ferramentas como o Git e Gitlab em maior profundidade. Ferramentas como essas fazem parte do dia-a-dia de profissionais da área de TI, que precisam criar, buscar e manusear grandes números de arquivos (principalmente arquivos de texto), que mudam com muita frequência. Na verdade, qualquer pessoa que precisa manter várias versões de um mesmo arquivo pode se beneficiar com um gerenciador de versão.

Você pode estudar alguns comandos básicos do git neste tutorial aqui.

Arquivo .gitignore

Nem todos os arquivos que manuseamos precisam ser guardados. Para controlar quais os arquivos são monitorados pelo git, podemos criar uma arquivo .gitignore. Esse arquivo lista quais arquivos serão totalmente ignorados ao fazer commits. Ele é usado quando mantemos na mesma pasta de repositório arquivos criados automaticamente, bibliotecas baixadas de outras fontes, arquivos temporários e afins.

No repositório de vocês, o arquivo .gitignore já existe e está populado com algumas exclusões. Vamos ver mais de perto:

tarefa*/testes/*.out
__pycache__

Cada linha contém um padrão representando um arquivo ou diretório a ser ignorado. Na primeira linha temos duas ocorrências do asterisco (*), que é um caracteres coringa ou wildcard e representa qualquer sequência de caracteres no nome de um arquivo. Por essa primeira linha, descobrimos que serão ignorados todos os arquivos com extensão .out (não importa o nome), que estejam dentro de um diretório testes, que por sua vez esteja dentro de quaisquer diretório que comece com o texto tarefa. Veja os exemplos:

tarefa00/testes/teste1.out -> será ignorado
tarefa00/testes/qualquer_nome_mesmo.out -> será ignorado
tarefa00/teste1.out -> NÃO será ignorado, pois não está dentro de testes
tarefa-qualquer/testes/oi.out -> será ignorado

A segunda linha contém apenas um nome (nenhum asterisco e nenhuma barra). Assim serão ignorados quaisquer arquivos ou diretórias com o nome __pycache__. Normalmente, diretórios com esse nome são criados pelo próprio interpretador Python durante a execução de um programa para gerados automaticamente, não queremos armazená-los no repositório. armazenar caches de programas compilados. Como esses arquivos são

Uma outra maneira de controlar quais alterações serão registradas em um commit é selecionando apenas os arquivos que tiveram alterações de interesse. Normalmente, fazemos git add . para adicionar no commit todas as alterações do diretório atual, mas se quisermos adicionar apenas alguns arquivos no commit, é melhor escrever git add <nome_arquivo>. Assim, quando fizer git commit -m "mensagem", poderá ver que outros arquivos modificados ainda não foram registrados.

Revendo a história

De nada adianta guardar várias versões de um arquivo se não pudermos ver versões anteriores. Por isso, precisamos descobrir como rever versões anteriores bem como decidir quando devemos registrar alterações.

Por que registrar alterações?

Ao utilizar o Git estamos utilizando uma ferramenta completa para controle de versão, isso inclui a possibilidade de registrar as alterações de arquivos por meio de commits, possibilitando posteriormente voltar atrás em versões (commits) anteriores do seu código.

A grande vantagem disso é poder recomeçar de um ponto base. Muitas vezes, se tenta fazer alguma alteração no código mas o resultado não se comporta como esperado, ou pior ainda, há um erro em uma alteração já feita e você só descobre algumas versões depois. Se todas as alterações de código estiverem em commits diferentes, podemos visualizá-las e escolher uma à qual retroceder.

Como não queremos jogar fora todo o trabalho realizado depois que retrocedemos, é importante fazer commits com certa frequência. Não seria muito útil fazer commit a cada linha que escrevemos, então podemos registrar a alteração sempre que tivermos realizado alguma mudança pequena, mas significativa. Por exemplo, ao escrevermos uma função, ao corrigirmos um bug, reformatarmos os documentos, etc.

Como desfazer alterações?

Desfazer uma alteração nada mais é do que voltar no tempo e recomeçar de um commit anterior. Para isso, precisamos inicialmente saber a qual commit queremos voltar. Para descobrir esse commit, podemos usar o comando git log no repositório desejado. Ao executar esse comando, você verá uma listagem semelhante a esta:

user@host:~/ra123456/tarefa00$ git log boanoite.py
commit a192d2ff5b5aa744fd8250704d778ba1e405c970 (HEAD -> master, origin/master, origin/HEAD)
Author: Matheus Otávio Rodrigues <ra222318@students.ic.unicamp.br>
Date:   Fri Mar 12 14:53:59 2021 -0300

    corrige identação boanoite.py

commit fe71f5d7312a2756a9353b996ccfecc9479f7ac1
Author: Matheus Otávio Rodrigues <ra222318@students.ic.unicamp.br>
Date:   Fri Mar 12 14:51:35 2021 -0300

    atualiza boanoite com o código disponibilizado na tarefa

...

Analisando o primeiro commit, vemos que ele possui uma identificação, a192d2ff5b5aa744fd8250704d778ba1e405c970, um autor, uma data e uma mensagem de commit logo abaixo. Digamos que o commit corrige identação boanoite.py introduziu um erro no arquivo boanoite.py. Assim, é preciso descobrir como esse arquivo estava antes da introdução do erro. Para isso, basta descobrir a identificação do último commit antes do erro (neste caso fe71f5d7312a2756a9353b996ccfecc9479f7ac1). Basta anotar os 6 primeiros caracteres. Com a identificação do commit em mãos, podemos olhar para o arquivo como estava naquele momento:

user@host:~/ra123456/tarefa00$ git show fe71f5:tarefa00/boanoite.py
n = int(input())

for _ in range(n):
    nome = input()
    print(f"Boa noite, {nome}.")

Pronto, agora você pode inspecionar a versão anterior e copiar as partes que tem interesse. Repare que você deve digitar a identificação da revisão, dois pontos e o caminho completo do arquivo.

Também é possível ver a árvore de commits feitos e analisar os arquivos rodando o comando gitk dentro da pasta do repositório. Se você usa WSL, pode ser necessário instalar o git para Windows e rodar gitk a partir de um console cmd. Ao abrir o gitk, uma nova janela se abrirá com uma lista de commits no canto superior esquerdo, um campo SHA1 ID (identificação do commit, copie a identificação de interesse) e mais abaixo as alterações em cada commit, onde linhas vermelhas são as linhas removidas e linhas verdes são as linhas que foram adicionadas.

Muito provavelmente, será mais conveniente utilizar o git diretamente integrado no seu editor de texto. Se estiver usando o VSCode, abra o arquivo e clique na visão TIMELINE. Você verá uma lista de mudanças registradas no seu arquivo aberto atualmente. Clicando em um item, será aberta uma comparação entre as versões do arquivo.

Ressuscitando arquivos

Algumas vezes, tudo dá errado e precisamos recomeçar de um certo ponto. Podemos retornar a um commit anterior executando git reset --hard <id_commit> em que <id_commit> é a identificação do commit desejado. Se tudo der certo, você receberá uma confirmação do tipo HEAD is now at fe71f5d atualiza boanoite com o código disponibilizado na tarefa. Tome MUITO CUIDADO ao executar esse comando, a opção --hard está nos alertando que todas as alterações não registradas no diretório atualmente serão perdidas. Assim, só execute isso quando já tiver feito commit em todas as alterações de interesse.

Comparando arquivos

Outro comando útil é o git diff, ele apresentará as alterações existentes entre os arquivos locais e os arquivos de um commit (por padrão o mais recente). Executando esse comando, teremos algo parecido com:

user@host:~/ra123456/tarefa00$ git diff
diff --git a/tarefa00/boanoite.py b/tarefa00/boanoite.py
index b26fb76..b8906e4 100644
--- a/tarefa00/boanoite.py
+++ b/tarefa00/boanoite.py
@@ -11,4 +11,5 @@ n = int(input())

 for _ in range(n):
     nome = input()
-    print(f"Boa noite, {nome}.")
+    print(f"Git diff, {nome}.")
+    print("teste")

Em que as linhas iniciadas com - (vermelhas) são linhas removidas e + (verdes) são linhas adicionadas Também podemos comparar com um commit em específico, basta rodar git diff <id_commit> em que <id_commit> é o id do commit desejado. Para sair da tela aberta para visualização, aperte ESC, depois digite q.

Problema comuns

Eu fiz o git push, mas minha tarefa não foi corrigida. Por quê?

Verifique se há um arquivo chamado nao_corrigir.txt ou similar na pasta da tarefa. Esse arquivo é criado quando a tarefa é adicionada ao seu repositório e serve para indicar que você ainda não terminou a tarefa. O bot irá corrigir tarefas, mas não irá atribuir uma nota. Quando tiver terminado a tarefa, remova esse arquivo, adicione todas as alterações no diretório com git add . e realize commit e push. Isso irá instruir o bot a atribuir uma nota para a tarefa.

Se não houver arquivo nao_corrigir.txt no diretório e, mesmo assim, a tarefa não tiver sido corrigida, pode ser que você ainda não terminou uma tarefa anterior. Nesta disciplina estamos utilizando avaliação contínua e individualizada. Cada tarefa só será corrigida pelo bot quando todas as tarefas anteriores tiverem passado pelo bot (veja o plano de desenvolvimento da disciplina). Assim, certifique-se de que as tarefas anteriores passaram (e que o monitor não deu D para alguma tarefa anterior).

Por que não consigo realizar git push dos meus arquivos algumas vezes?

Lembre-se que git push é o comando que irá colocar as mudanças “commitadas” no repositório local para o repositório remoto (por padrão, origin, que no nosso caso é o gitlab do ic). Imagine que você acabou de clonar o repositório da disciplina. A situação é essa:

origin:  c0 <--- c1

local:   c0 <--- c1

Ou seja, para o git, ocorreu o commit c0 e depois o c1 (na estrutura do git, cada commit aponta para seu parente). Como você acabou de clonar o repositório da disciplina, ambos estão iguais.

Digamos agora que você resolveu a tarefa 2 e “commitou” como t2. Como essa mudanças foram apenas locais, nossa situação é a seguinte:

origin: c0 <--- c1

local:  c0 <--- c1 <--- t2

Agora nosso repositório local possui modificações a mais do que o repositório remoto do GitLab. Caso desejamos colocar essas novas modificações no remoto, podemos fazer git push e o resultado será:

origin: c0 <--- c1 <--- t2

local:  c0 <--- c1 <--- t2

Digamos agora que você fez a tarefa 3 e “commitou” como t3. Mais uma vez, agora temos a seguinte situação:

origin: c0 <--- c1 <--- t2

local:  c0 <--- c1 <--- t2 <--- t3

Porém, antes de você sincronizar seu repositório local com o remoto, foi postada uma nova tarefa, a tarefa 4. Similarmente ao processo que fazemos manualmente, o sistema do prof Lehilton irá “commitar” a nova tarefa no seu repositório remoto do GitLab, digamos um commit t4.

origin: c0 <--- c1 <--- t2 <--- t4

local:  c0 <--- c1 <--- t2 <--- t3

Agora, se você tentar fazer git push você será apresentado com um erro, nesse caso:

warning: redirecting to https://gitlab.ic.unicamp.br/mc202-2020/ra217938.git/
To https://gitlab.ic.unicamp.br/mc202-2020/ra217938
    ! [rejected]        master -> master (fetch first)
error: failed to push some refs to 'https://gitlab.ic.unicamp.br/mc202-2020/ra217938'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Isso acontece pois existem mudanças no repositório remoto que ainda não foram adicionadas ao seu repositório local. Como nessa disciplina o sistema nunca irá mexer nos mesmo arquivos que você, tudo que você precisa fazer é atualizar seu repositório local com git pull, Isso faz duas coisas automaticamente. Primeiro, o git faz um fetch de t4 para a cópia local:

origin: c0 <--- c1 <--- t2 <----- t4


                             +----t4
                            /
                           /
                          /
                         /
                        v
local:  c0 <--- c1 <--- t2 <----- t3

Depois o git fará um “merge” das “branchs” t3 e t4, criando assim uma nova branch m.

origin: c0 <--- c1 <--- t2 <----- t4


                             +----t4 <---+
                            /             \
                           /               \
                          /                 m
                         /                 /
                        v                 /
local:  c0 <--- c1 <--- t2 <----- t3 <---+

E agora sim você poderá fazer git push para entregar sua tarefa.