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.