O Git é o standard da indústria quando se fala em controlo de versões. Se já alguma vez pesquisaram sobre isto, já devem ter lido que permite “voltar ao passado” do nosso código, permitindo regressar a versões anteriores, voltar à versão atual, programar em paralelo, etc.

No curso, a grande maioria utiliza o Git em conjunto com o GitHub. Por estarem tão interligados no nosso workflow, por vezes é difícil distinguir o que é que é Git e o que é que é GitHub. Algo que também acontece é usarmos o Git e GitHub como uma simples plataforma de partilha de código, em que nós fazemos push e os nossos colegas fazem pull, não sendo, provavelmente, a melhor maneira, é bem melhor do que partilhar os ficheiros por Facebook Messenger e ter de lidar manualmente com o que mudou entretanto.

Este guia visa clarificar o que são o Git e o GitHub e como devem ser utilizados, dando exemplos práticos desde a instalação do Git à programação em paralelo via GitHub. O guia está feito para ser acompanhado num ambiente Unix (seja em Mac ou numa distribuição Linux), que é o que aconselhamos para quem está a aprender. Sendo cross-platform, o Git também funciona no Windows e não terás qualquer problema em seguir este guião nesse SO. Apesar de haver aplicações gráficas para usar Git, vamos usar exclusivamente o terminal Depois de aprenderes, está à vontade para usar qualquer aplicação gráfica que gostes; é mais fácil passar do terminal para um ambiente gráfico do que o contrário.

A primeira parte, Git Basics, é um tutorial de Git que não assume qualquer conhecimento prévido da plataforma. Se já te sentes confortável com o Git e só pretendes aprender como melhorar o workflow do teu grupo, podes passar diretamente para a secção GitHub - Working remote.

Git

Git Basics

Git is a free and open source distributed version control system designed to handle everything from small to very large projects with speed and efficiency.

‘nough said.

Por agora, vamos esquecer o distributed e vamos assumir que o Git é local, ou seja, instalamo-lo no nosso PC e utilizamo-lo só nós, e só nos nossos projetos. Mais tarde falaremos do trabalho em equipa.

Instalar e configurar

A instalação é simples, basta correr o comando apropriado à vossa distribuição:

Ubuntu/Mint:

$ sudo apt install git

Manjaro/Arch Linx:

$ sudo pacman -S git

Mac/Windows

Nota: Na instalação em Windows, podes aceitar as opções recomenddas. Aproveita é para trocares o editor de texto default para o teu favorito, assim poderás, mais abaixo, ignorar essa configuração.

Depois de instalado, convém fazer uma pequena configuração que consiste em identificarem-se ao Git:

$ git config --global user.name "Your Name Comes Here"
$ git config --global user.email you@email.com

Nota: Isto serve para o Git identificar o código que submeteram como vosso, mas expõe o vosso email se colocarem o vosso repositório online e o tornarem público.

Podem também alterar o editor de texto do Git com o comando

$ git config --global core.editor "nome_do_editor"

em que nome “nome_do_editor” é o nome que usariam para abrir o editor de texto através do terminal. Por exemplo, para definir o Visual Studio Code, o comando seria

$ git config --global core.editor "code"

Aviso para os utilizadores de Windows: Para seguir este guia, deverás usar a Git Bash. A Git Bash é um emulador de bash para Windows e é instalado automaticamente com o Git. Se usares a Git Bash, deves poder seguir o tutorial tal e qual como está.

Repositórios

No Git trabalha-se em repositórios. O vosso código pertence a um repositório e todas as alterações que lhe façam são registadas dentro desse repositório.

Os repositórios são criados manualmente e um repositório deve corresponder a um e só um projeto.

Para criar um repositório, devem fazê-lo dentro na raíz do projeto.

Por exemplo, eu tenho um projeto de LI3 que estou a começar, guardado em ~/dev/li3. O projeto já tem alguns ficheiros de código e eu quero criar um repositório Git para me ajudar daqui para a frente:

$ cd ~/dev/li3
$ ls
enunciado.pdf  include  src

Criar um repositório é simples:

$ git init
Initialized empty Git repository in /home/miguel/dev/li3/.git/

Como se pode observar pelo output do comando, o Git criou a diretoria .git na raíz do meu projeto. É uma diretoria oculta, pelo que não aparece no output do ls, temos de passar uma opção para ver ficheiros ocultos:

$ ls
enunciado.pdf  include  src
$ ls -a
.  ..  enunciado.pdf  .git  include  src

Felizmente, nunca vamos precisar de a usar. A diretoria .git é para uso exclusivo do Git: é onde ele guarda todas as informações relativas ao vosso repositório. Não lhe devemos mexer. Toda a interação com o Git será feita via linha de comandos.

Agora que eu tenho o meu repositório criado, posso ver qual o estado dele:

$ git status
On branch master

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
  enunciado.pdf
	include/
	src/

nothing added to commit but untracked files present (use "git add" to track)

Vamos dissecar o output:

Commits

O Git está-me a dizer que os ficheiros enunciado.pdf, include/ e src/ estão untracked. Para o Git, os ficheiros ou estão tracked ou untracked: um ficheiro tracked é, como o nome indica, um ficheiro que o Git monitoriza, ou seja, em que regista as alterações que lhe são feitas. Por defeito, qualquer ficheiro novo num repositório é untracked por defeito e tem de explicitamente tracked.

Nota: Podem ter reparado que include/ e src/ não são, na verdade, ficheiros: são diretorias, como evidenciado pelo sufixo /. Com esta sintaxe, o Git está-nos a dizer que todos os ficheiro naquelas diretorias (e possíveis sub-diretorias) estão untracked.

Também fala em commits. Diz-me que ainda não tenho commits (No commits yet) e que não tenho nada para fazer commit (nothing added to commit). Mas o que é um commit?

Um commit é um estado; é um “game save”; é uma versão do código. Um commit guarda o estado de um projeto a uma certa altura. Quando fazemos um commit, estamos a dizer ao Git para tirar um snapshot a como o código está naquele momento e o guardar para sempre. É com base nesta mecânica que podemos iterar entre versões do nosso projeto: quando um projeto tem vários commits, tem vários estados no tempo, permitindo-nos “voltar atrás” e ver/recuperar versões anteriores do código. Mas, mais para a frente, falaremos melhor disto.

Voltando ao output de há bocado:

$ git status
On branch master

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
  enunciado.pdf
	include/
	src/

nothing added to commit but untracked files present (use "git add" to track)

Como o Git sugere, podemos fazer track dos ficheiros usando git add <file>:

$ git add src

Neste caso, fiz track dos ficheiros em src/. Se voltar a ver qual o estado do repositório

$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
	new file:   Makefile
	new file:   src/interface.c
	new file:   src/main.c
	new file:   src/program
	new file:   src/sort.c

Untracked files:
  (use "git add <file>..." to include in what will be committed)
  enunciado.pdf
	include/

posso ver que todos os ficheiros em src/ serão adicionados no próximo commit como novos ficheiros. Mas falta-me adicionar os restantes ficheiros. Posso adicionar todos com um simples comando:

$ git add .

Num ambiente Unix, o . simboliza a diretoria atual. Assim, estou a indicar ao Git que quero adicionar tudo o que está dentro da minha diretoria.

O estado do repositório é, agora:

$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
	new file:   enunciado.pdf
	new file:   include/common.h
	new file:   include/dataSimples.h
	new file:   include/date.h
	new file:   include/estrutura.h
	new file:   include/interface.h
	new file:   include/list.h
	new file:   include/pair.h
	new file:   include/sort.h
	new file:   include/user.h
	new file:   Makefile
	new file:   src/interface.c
	new file:   src/main.c
	new file:   src/program
	new file:   src/sort.c

Boa! Tenho tudo adicionado, estou pronto para fazer o meu primeiro commit, certo?

Não.

Bom, tecnicamente, sim. Não há nada que nos impeça de fazer um commit agora. No entanto, como tudo no mundo da informática, também o Git tem um conjunto de boas práticas que devem ser seguidas. Uma delas é evitar adicionar ficheiros binários. Num repositório, só devem ser adicionadas as sources; não se deve adicionar os binários gerados por essas sources.

No meu caso, tenho dois ficheiros binários: o relatorio, o binário do meu programa, gerado pela Makefile, e enunciado.pdf, o enunciado do meu trabalho.

Assim sendo, vou removê-lo dos ficheiros a que quero fazer commit. O Git diz-me como o fazer: (use "git rm --cached <file>..." to unstage)

git rm --cached enunciado.pdf src/program

Agora, o estado do repositório é

On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
	new file:   include/common.h
	new file:   include/dataSimples.h
	new file:   include/date.h
	new file:   include/estrutura.h
	new file:   include/interface.h
	new file:   include/list.h
	new file:   include/pair.h
	new file:   include/sort.h
	new file:   include/user.h
	new file:   Makefile
	new file:   src/interface.c
	new file:   src/main.c
	new file:   src/sort.c

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	enunciado.pdf
	src/program

Estou pronto para fazer o commit! Um commit contém uma mensagem que descreve o que é que o código daquele commit faz: que funcionalidade implementa ou que bug resolve, etc. Essa mensagem é constituída por um assunto (obrigatório) e um corpo (opcional).

Um commit é feito com um comando simples:

$ git commit

Este comando abre o editor de texto default do sistema, onde podemos escrever a mensagem. O assunto é escrito primeiro, seguido do corpo, separados por uma linha vazia:

Isto é o assunto

Isto é o corpo
# Isto é um comentário, vai ser ignorado e não vai ser incluído no corpo da mensagem

No entanto, a grande maioria das vezes, o assunto da mensagem é suficiente. Se for esse o caso, podemos utilizar antes o comando

$ git commit -m "O assunto aqui"

que não abre o editor de texto e faz imediatamente o commit.

Para o meu repositório, vou executar o comando git commit -m "Init". Visto que este é o meu primeiro commit utilizo uma mensagem que simbolize que estou a inicializar o repositório com código pré-existente.

Nota: Também há boas práticas a seguir na escrita de mensagens: devem ser claras e escritas na mesma língua usada no código, ou seja, de preferência, em Inglês. O corpo da mensagem deve ser usado para descrever o quê e o porquê do commit de uma forma mais detalhada, não o como. O assunto deve ser capitalizado (a primeira letra deve ser uma maiúscula) e não deve acabar com um ponto final. O assunto também deve ser escrito no modo imperativo, ou seja, deve fazer sentido quando colocado na frase This commit will …, por exemplo: Boa mensagem: Add data structure to represent Person Má mensagem: Added data structure

Agora o estado do meu repostório é

$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)
	enunciado.pdf
	src/program

nothing added to commit but untracked files present (use "git add" to track)

Ignorar ficheiros

O Git continua a avisar que o enunciado.pdf e o src/program não estão tracked. Mas eu não quero que eles sejam adicionados. Felizmente, o Git permite-nos criar um ficheiro na raíz do repositório onde podemos especificar que ficheiros é que queremos que o Git ignore: o .gitignore.

Vamos criar um. No ficheiro, só temos de especificar o caminho do ficheiro que queremos ignorar:

$ cat .gitignore
enunciado.pdf
src/program
# Isto é um comentário

O Git reconhece o ficheiro e ignora o que lhe dissemos, identificando também que o .gitignore ainda não é tracked.

$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)
	.gitignore

nothing added to commit but untracked files present (use "git add" to track)

Vou fazer o commit deste novo ficheiro:

$ git add .
$ git commit -m "Add .gitignore"
$ git status
On branch master
nothing to commit, working tree clean

Boa! Os binários estão a ser ignorados e não há nada que ainda não esteja representado num commit.

Mas e se agora eu guardar outro ficheiro PDF na raíz do repositório e mais um binário em src/?

$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)
	enunciado2.pdf
	src/program2

nothing added to commit but untracked files present (use "git add" to track)

Teria de modificar o meu .gitignore e adicionar o nome dos ficheiros novos… Ou posso generalizar, alterando o meu .gitignore para

$ cat .gitignore
*.pdf
src/program

Assim, qualquer ficheiro no meu repositório cujo nome termine em .pdf é automaticamente ignorado! Mas como ignorar todos os binários em src/ se eles não têm uma extensão? A forma mais fácil é, simplesmente, alterar a Makefile para os gerar noutra diretoria e ignorar todo o seu conteúdo! Depois de criar a diretoria bin/ e alterar a Makefile , o estado do repositório é

$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   .gitignore
	modified:   Makefile

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	bin/

no changes added to commit (use "git add" and/or "git commit -a")

Como previsto, bin/ aparece como untracked. O program e o program2 já não aparecem, porque, como pertencem a bin/, já estão representados, embora ocultos.

Para ignorar todo o conteúdo da diretoria, inclusivamente as suas possíveis subdiretorias, altero o meu .gitignore para

$ cat .gitignore
*.pdf
bin/

Agora, o estado é

$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   .gitignore
	modified:   Makefile

no changes added to commit (use "git add" and/or "git commit -a")

Assim, ignoramos com sucesso os binários e PDFs que temos e que possamos vir a adicionar. Agora, basta fazer o commit:

$git add .
$git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	modified:   .gitignore
	modified:   Makefile
$ git commit -m "Move binaries to 'bin/' and ignore 'bin/' and all PDFs"

Nota: Esta não é uma boa mensagem de commit. O assunto é um pouco longo (58 caratéres) e ligeiramente confuso de ler. Mas o problema não é a forma como a mensagem foi escrita, mas a forma como o commit foi estruturado: os commits devem representar uma única alteração ao funcionamento, independentemente do número de ficheiros alterados para fazer essa alteração. Neste caso, fizemos duas alterações ao funcionamento: mudar a geração dos binários para bin/ e ignorar a diretoria e ignorar todos os PDFs. A mensagem ficou mal em consequência de termos feito num commit o que deveria ter sido feito em dois.

Agora que já temos alguns commits, podemos executar

$ git log
commit bed65eefe12d19d26c9a7a2ac847245982eba007 (HEAD -> master)
Author: CeSIUM <pedagogico@cesium.di.uminho.pt>
Date:   Sun Mar 22 12:10:53 2020 +0000

    Move binaries to 'bin/' and ignore 'bin/' and all PDFs

commit a582641704c87d0912355aba9cfa189613c56cac
Author: CeSIUM <pedagogico@cesium.di.uminho.pt>
Date:   Sun Mar 22 11:34:18 2020 +0000

    Add .gitignore

commit ed10169a2ed0fe5b7aa756f501665d925cb5dd10
Author: CeSIUM <pedagogico@cesium.di.uminho.pt>
Date:   Sun Mar 22 11:22:52 2020 +0000

    Init

para ver o histórico dos commits. Como podemos ver, os commits são identificados por uma hash única e contêm a informação da data em que foram criados e do seu autor. Também nos é mostrada a sua mensagem para fácil identificação do que é que o commit faz.

Nota: Para identificar um commit pode-se usar a hash completa, mas basta usar os primeiros 7 caratéres.

Usando estas hashes, podemos ver diferenças entre o estado atual e o de um determinado commit (neste exemplo, o commit ‘Init’) com

$ git diff ed10169
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..89ab085
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+*.pdf
+bin/
diff --git a/Makefile b/Makefile
index e4fe0a5..b2d8630 100755
--- a/Makefile
+++ b/Makefile
@@ -19,7 +19,7 @@ $(ODIR)/%.o : %.c $(DEPS)
        $(CC) $(CFLAGS) -c -o $@ $<
 
 program: $(SOURCES_OBJ) $(MY_LIBS_OBJ)
-       $(CC) $(CFLAGS) $(wildcard $(ODIR)/*.o)  $(wildcard $(OLDIR)/*.o) -o program $(LIBS)
+       $(CC) $(CFLAGS) $(wildcard $(ODIR)/*.o)  $(wildcard $(OLDIR)/*.o) -o bin/program $(LIBS)
 
 clean:
        rm obj/*.o

ou, quando usado sem uma hash (git diff), as diferenças entre o estado atual e o último commit ou, quando usado com duas hashes (git diff ed10169 a582641), as diferenças entre esses dois commits.

Também podemos voltar, temporariamente, a um commit anterior (neste caso, o primeiro, ‘Init’) com

$ git checkout ed10169

usando

$ git checkout master

para regressar.

Reverter commits

Por qualquer razão, podemos ter reverter algo que foi feito. Sem Git, esta seria capaz de desgastar o CTRL-Z de qualquer teclado, isto se tivermos a sorte do histórico ainda estar acessível pelo ‘Undo’.

Podemos estar, por exemplo, a meio de implementar uma funcionalidade em que alteramos vários ficheiros e apercebermo-nos de que não é uma boa implementação e querermos apagar. Sem Git, teríamos de encontrar e apagar todo o código manualmente ou utilizar o sempre presente CTRL-Z em todos os ficheiros, rezando para que não apaguemos acidentalmente qualquer outra parte importante.

No Git, podemos simplesmente reverter o estado para o último commit. As alterações que eu fiz ao meu repostiório consistiram em alterar alguns ficheiros e adicionar uns novos, que ainda estão untracked.

$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   src/interface.c
	modified:   src/main.c

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	src/dataSimples.c
	src/estrutura.c
	src/lib/

no changes added to commit (use "git add" and/or "git commit -a")

Como estão untracked, eu sei que são ficheiros novos e que, portanto, não estavam em qualquer commit anterior, podendo eliminá-los, manualmente, com confiança. Os restantes ficheiros, como já existem num commit anterior e econtram-se no estado modified, podem simplesmente ser revertidos como o Git nos indica: (use "git restore <file>..." to discard changes in working directory).

Como eu quero reverter tudo, executo

$ git restore .

Assim, os meus ficheiros voltaram ao estado original.

Mas e se eu já tiver feito commit do que quero reverter? Nesse caso, posso usar git revert. Este comando vai criar “anti-commits”, ou seja, commits inversos aos que queremos apagar. Para este exemplo, criei um commit para apagar, o commit db13e27:

$ git log
commit db13e27b66e5020ae73991ea89d3cd2dd15dab90 (HEAD -> master)
Author: CeSIUM <pedagogico@cesium.di.uminho.pt>
Date:   Sun Mar 22 13:23:24 2020 +0000

    Commit a apagar

commit bed65eefe12d19d26c9a7a2ac847245982eba007
Author: CeSIUM <pedagogico@cesium.di.uminho.pt>
Date:   Sun Mar 22 12:10:53 2020 +0000

    Move binaries to 'bin/' and ignore 'bin/' and all PDFs

[...]

Para o apagar, faço

$ git revert db13e27
Removing src/test.c
[master 0ec1358] Revert "Commit a apagar"
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 src/test.c

e aceito a mensagem pré-definida que o Git escreve para o commit inverso. Consultando

$ git log
commit 0ec1358f0af1a8be7e8d2f1191bffe368b7b6585 (HEAD -> master)
Author: CeSIUM <pedagogico@cesium.di.uminho.pt>
Date:   Sun Mar 22 13:25:39 2020 +0000

    Revert "Commit a apagar"
    
    This reverts commit db13e27b66e5020ae73991ea89d3cd2dd15dab90.

commit db13e27b66e5020ae73991ea89d3cd2dd15dab90
Author: CeSIUM <pedagogico@cesium.di.uminho.pt>
Date:   Sun Mar 22 13:23:24 2020 +0000

    Commit a apagar

commit bed65eefe12d19d26c9a7a2ac847245982eba007
Author: CeSIUM <pedagogico@cesium.di.uminho.pt>
Date:   Sun Mar 22 12:10:53 2020 +0000

    Move binaries to 'bin/' and ignore 'bin/' and all PDFs

posso ver que, apesar do resultado do meu commit ter sido apagado (o ficheiro que criei já não está no repositório), a história mantém-se e posso, se quiser, reverter a reversão.

Aviso: É por isto que é importante ter uma boa disciplina quando se trata de criar commits. Se trabalharmos em várias funcionalidades ao mesmo tempo, sem fazer commit conforme as vamos acabando, esta vantagem do Git é inutilizável porque também iríamos apagar código que queremos manter.

Nota: Há muitas mais formas de utilizar o revert e ainda mais de reverter commits. Algumas mantêm a história de commits, outras não. Algumas pessoas consideram mau não manter a história, outras consideram necessário para manter o histórico de commits limpo e legível (geralmente em projetos grandes). Esta resposta no StackOverflow explica outras formas de reverter commits.

Branches

Os branches são uma ferramenta muito poderosa. Todos os repositórios contêm, no minímo, um branch, o master, o default branch.

Podemos ver em que branch estamos fazendo

$ git status
On branch master
nothing to commit, working tree clean

Obviamente, como ainda não criamos nenhum branch, estamos no master.

Para criar um branch chamado branch1, podemos executar

$ git branch branch1

Para ver que branches existem, executamos

$ git branch
  branch1
* master

em que o branch com o asterisco é o branch em que nos encontramos.

Para mudar de qualquer branch para branch1, utilizamos

$ git checkout branch1
Switched to branch 'branch1'

e, consequentemente, para voltar para o master fazemos

$ git checkout master
Switched to branch 'master'

Também podemos, ao mesmo tempo, criar um branch e mudar para lá:

$ git checkout -b branch2
Switched to a new branch 'branch2'

Mas o que é um branch, afinal? Um branch é uma linha de desenvolvimento independente. Quando criamos um branch, ocorre uma bifurcação (fork) no histórico do repositório: a linha de desenvolvimento é dividida em duas, independentes, em que o commit inicial do novo branch é o commit do branch original a partir do qual o novo branch foi criado. Confuso, certo?

Neste momento, o que temos é isto:

branch1

Cada nodo é um commit, e a etiqueta de um branch aponta para o nodo (commit) em que esse branch está.

Neste momento, todos os branches estão no mesmo commit, visto que ainda não criamos nenhum commit em nenhum branch desde que os criamos. Vamos alterar isso:

$ git checkout branch1
$ vim um_ficheiro.c
$ git status
On branch branch1
Untracked files:
  (use "git add <file>..." to include in what will be committed)
	um_ficheiro.c

nothing added to commit but untracked files present (use "git add" to track)
$ git add .
$ git commit -m "Add a new test file"
[branch1 a416568] Add a new test file
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 um_ficheiro.c

Agora, este é o histórico do repositório:

branch2

E se fizer outro commit, mas desta vez no branch2, com um ficheiro com o mesmo nome mas, em vez de vazio, com algum conteúdo?

$ git checkout branch2
$ vim um_ficheiro.c
$ cat um_ficheiro.c
algum conteudo
$ git add .
$ git commit -m "Add a file"
[branch2 e7111d4] Add a file
 1 file changed, 1 insertion(+)
 create mode 100644 um_ficheiro.c

O histórico do repositório fica assim:

branch3

Nota: Como já podem ter reparado, HEAD aponta para onde estivermos no histórico. Se fizermos git checkout ed10169 (Init), HEAD apontará para esse commit.

Agora, vou voltar para o branch1, alterar o documento um_ficheiro.c e fazer commit:

$ git checkout branch1
[alterar documento]
$ cat um_ficheiro.c
#include <stdio.h>

int main(){
	printf("Hello World!");
	return 0;
}
$ git add .
$ git commit -m "Add Hello World"
[branch1 d1c55cb] Add Hello World
 1 file changed, 6 insertions(+)

Mas agora quero colocar as alterações que fiz no branch1 no master. Quero fazer o merge de branch1 com o master. Para isto, temos de, primeiro, mudar para o branch para onde queremos “mandar” as alterações, neste caso o master. De seguida, ordenamos o merge com branch1 com o comando git merge:

$ git checkout master
Switched to branch 'master'
$ git merge branch1
Updating 0ec1358..d1c55cb
Fast-forward
 um_ficheiro.c | 6 ++++++
 1 file changed, 6 insertions(+)
 create mode 100644 um_ficheiro.c

Como não vamos precisar mais do branch1, podemos apagá-lo com git branch -d branch1.

O gráfico do histórico do repositório agora está assim (o branch1 está semi-transparente para representar que já não existe):

branch4

Por último, quero fazer o merge do branch2 para o master. Como já estou no master, basta fazer

$ git merge branch2
CONFLICT (add/add): Merge conflict in um_ficheiro.c
Auto-merging um_ficheiro.c
Automatic merge failed; fix conflicts and then commit the result.

E… Houve problemas. Mais especificamente, houve um conflito. Quando o Git tenta juntar dois branches, ele faz um auto-merge: vê as diferenças entre os branches e junta-os harmoniosamente. No entanto, por vezes, o Git não sabe como fazer a junção, resultando num conflito. Um conflito pode ocorrer por várias razões. No nosso caso, criamos um ficheiro com o mesmo nome nos branches branch1 e branch2, mas cujo conteúdo era diferente. O Git não sabe se deve manter o conteúdo do ficheiro do master (que era o do branch1) ou o conteúdo do ficheiro do branch2, ou se deve subsituir um pelo outro, ou se deve juntar o conteúdo dos dois, etc. Para isso, temos de intervir e resolver o conflito manualmente. Para isso, temos de abrir o ficheiro num editor de texto (ou IDE):

<<<<<< HEAD
#include <stdio.h>

int main(){
	printf("Hello World!");
	return 0;
}
======
algum conteudo
>>>>>> branch2

Como podemos ver, o ficheiro está marcado com sintaxe do Git: entre <<<<<< HEAD e ====== está o conteúdo do nosso ficheiro; entre ====== e >>>>>> branch2 está o conteúdo deles. Aqui, nosso e deles referem-se aos branches. nosso refere-se branch em que estamos, neste caso o master, deles refere-se ao branch que está a ser incluído, neste caso o branch2.

Neste conflito, por ser um mero exemplo, foi, arbitráriamente, escolher manter o conteúdo do master. Para isso, basta eliminar tudo o que não for conteúdo do master, incluindo as marcações do Git:

#include <stdio.h>

int main(){
	printf("Hello World!");
	return 0;
}

Com o ficheiro corrigido, basta fazer um commit.

$ git status
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both added:      um_ficheiro.c

no changes added to commit (use "git add" and/or "git commit -a")
$ git add .
$ git commit
[master 843a289] Merge branch 'branch2'

Nota: Como este commit resulta de um merge, não defini a minha mensagem e aceitei a que o Git me propõs.

O histórico final do repositório fica

branch5

Podemos ver um esquema parecido a partir do Git:

$ git log --graph
*   commit 843a2893407150a66d2a2bab93107ad27760f89a  (HEAD -> master)
|\  Merge: d1c55cb e7111d4
| | Author: CeSIUM <pedagogico@cesium.di.uminho.pt>
| | Date:   Sun Mar 22 16:35:58 2020 +0000
| | 
| |     Merge branch 'branch2'
| | 
| * commit e7111d4d2f101c55cb5d6d2c5ed5fd855bec6fd3  (branch2)
| | Author: CeSIUM <pedagogico@cesium.di.uminho.pt>
| | Date:   Sun Mar 22 15:29:28 2020 +0000
| | 
| |     Add a file
| | 
* | commit d1c55cb0b3997724ab70b9aca9a2fc4117d456e0
| | Author: CeSIUM <pedagogico@cesium.di.uminho.pt>
| | Date:   Sun Mar 22 15:44:53 2020 +0000
| | 
| |     Add Hello World
| | 
* | commit a416568514b37f0275b263213637fdaf56b45f2a
|/  Author: CeSIUM <pedagogico@cesium.di.uminho.pt>
|   Date:   Sun Mar 22 15:06:50 2020 +0000
|   
|       Add a new test file
| 
* commit 0ec1358f0af1a8be7e8d2f1191bffe368b7b6585
| Author: CeSIUM <pedagogico@cesium.di.uminho.pt>
| Date:   Sun Mar 22 13:25:39 2020 +0000
| 
|     Revert "Commit a apagar"
|     
|     This reverts commit db13e27b66e5020ae73991ea89d3cd2dd15dab90.
| 
* commit db13e27b66e5020ae73991ea89d3cd2dd15dab90
| Author: CeSIUM <pedagogico@cesium.di.uminho.pt>
| Date:   Sun Mar 22 13:23:24 2020 +0000
| 
|     Commit a apagar
| 
* commit bed65eefe12d19d26c9a7a2ac847245982eba007
| Author: CeSIUM <pedagogico@cesium.di.uminho.pt>
| Date:   Sun Mar 22 12:10:53 2020 +0000
| 
|     Move binaries to 'bin/' and ignore 'bin/' and all PDFs
| 
* commit a582641704c87d0912355aba9cfa189613c56cac
| Author: CeSIUM <pedagogico@cesium.di.uminho.pt>
| Date:   Sun Mar 22 11:34:18 2020 +0000
| 
|     Add .gitignore
| 
* commit ed10169a2ed0fe5b7aa756f501665d925cb5dd10
  Author: CeSIUM <pedagogico@cesium.di.uminho.pt>
  Date:   Sun Mar 22 11:22:52 2020 +0000
  
      Init

que nos mostra uma representação parecida com as imagens que temos visto, mas claro, sem o branch1, visto que foi apagado.

Nota: Há mais formas de fazer um merge. Geralmente, essas formas diferem em como é feita a gestão do histórico de commits e a prevenção/resolução de conflitos. Sugiro que pesquises sobre git squash e git rebase.

Os branches são uma ferramenta muito poderosa porque permitem programar de forma “concurrente”. Podemos ter um branch principal, habitualmente o master que contenha a versão estável do nosso software, enquanto o desenvolvimento é feito noutros branches. Assim, podemos ter, por exemplo, um branch por cada grande funcionalidade a implementar e trabalhar em várias ao mesmo tempo, sem misturar os seus commits facilitando imenso a reversão, por exemplo. Depois, quando funcionalidade é acabada, o seu branch é merged com o master e eliminado.

GitHub

GitHub - Working remote

Parabéns por teres chegado aqui! Esta secção vai ser muito mais breve! Agora que já sabes os básicos de Git, vamos falar sobre a parte remote do Git e onde é que o GitHub entra.

Na introdução, falamos que o Git é um distributed version control system. A verdade é que, apesar do Git ser muito útil para ser utilizado individualmente, é ainda melhor para trabalhar em equipa, suportando vários tipos de workflow.

O Git suporta remotes, ou seja, servidores remotos com Git configurado que guardam uma cópia do repositório. Isto permite a distribuição do código (não há um ponto central de falha) e permite que várias pessoas acedam ao mesmo repositório e tenham cópias do mesmo repositório.

Estando em MIEI, não deves ser estranho ao GitHub. Até já deves ter uma conta PRO. Se não, aproveita a oferta, é grátis para alunos universitários.

O GitHub é uma plataforma de hospedagem de source code que usa o Git. Ou seja, serve como um remote para repositórios Git.

Criar um repositório

Para um projeto, há três formas de ter um repositório no GitHub:

  1. se ainda não tivermos começado o projeto, podemos criar um repositório inicializado na plataforma e depois clonar o repositório para o nosso PC (a forma mais fácil)
  2. se já tivermos começado o projeto no nosso PC e pretendermos adicionar Git e colocar no GitHub, podemos fazer a mesma coisa que o ponto anterior e, depois de clonado, copiar o código para o repositório e fazer o primeiro commit.
  3. se já tivermos comçado o projeto no nosso PC que já seja um repositório, criamos um repositório vazio no GitHub e adicionamo-lo como remote ao nosso repositório local.

Ponto 1

Criar um repositório remote antes de começar um projeto é a forma mais fácil de o fazer. Basta entrar no GitHub, e clicar em New.

github1

Na página que se abre, escolhemos o nome do repositório, escrevemos, opcionalmente, uma descrição e se queremos que seja público ou privado.

Como pretendemos clonar o repositório para o computador, inicializar com o README. Também temos a opção de escolher inicializar o repositório com um .gitignore pré-definido: dependendo da tecnologia que vamos usar no projeto, pode dar bastante jeito escolher começar com um .gitignore genérico para essa linguagem e irmos adaptando conforme necessário. Podemos também escolher uma licença. Se não escolhermos nenhuma, as leis normais dos direitos de autor aplicam-se. O botão informativo ao lado liga a esta página, que explica todos os detalhes de escolher, ou não, uma licença.

github2

Depois, é só clicar no botão de clonar, copiar o link, abrir um termainal na diretoria onde queremos ter o repositório e executar o comando git clone <link>, substituindo <link> pelo link que copiamos. Se o repositório for privado, o Git pedir-nos-á as nossas credenciais de acesso ao GitHub: o username e a password.

github3

O repositório está pronto a ser usado! A sua diretoria tem o mesmo nome que o repositório no GitHub. Vamos ver como tirar partido do GitHub mais à frente.

Ponto 2

É idêntico ao Ponto 1. A única diferença é que, no fim do repositório ser clonado, temos de copiar o nosso código para o repositório e fazer o commit.

Ponto 3

Neste cenário, já temos um projeto local que queremos colocar no GitHub. Fazer como no ponto 2 é possível, mas nada, mesmo nada é recomendado, pois perde-se todos os branches, histórico do repositório e outra informação do repositório.

O processo é parecido com o do ponto 1. A única diferença na criação do repositório é que não o podemos inicializar, pelo que não podemos escolher criar o README, adicionar um .gitignore pré-defindo ou uma licença. No entanto, tudo isto pode ser feito manualmente, à posteriori.

Depois de criado, copiamos o link.

github4

No nosso repositório local no branch master, executamos o comando

git remote add origin https://github.com/user/git-github-test.git

Este comando adiciona um remote ao nosso repositório, chamado (por convenção) origin.

De seguida, executamos o comando

git push --set-upstream origin master

Este comando faz duas funções: git push envia o branch atual (neste caso, o master) para o remote. No entanto, o nosso remote (o repositório que acabamos de criar no GitHub) ainda não tem nenhum branch. Portanto, temos de associar o branch local a um branch no remote. É isso o que faz --set-upstream origin master: define que o branch local master deve ser enviado para o branch master do remote origin.

Nota: Sempre que queremos fazer o push de um branch novo, temos de fazer git push --set-upstream origin <nome_do_branch>, para criar a associação entre o branch local e o seu remote. Os pushes consequentes desse branch podem ser feitos correndo apenas git push.

O repositório está pronto a ser usado!

Push, Pull e Fetch

Para utilizar um remote, há três comandos a saber: git pull, git fetch e git push.

Push

Já falamos um pouco do push: utiliza-se para atualizar o remote com os novos commits do branch ativo.

Pull

O git pull faz o download dos novos dados remotos e, de seguida, integra esses dados nos ficheiros ativos, ou seja, faz o merge. Por fazer merge, pode resultar em conflitos.

Seguindo o repositório de exemplo estabelecido na secção de Git Basics, um colega fez push de um branch branch3. No meu repositório local, posso executar

$ git pull
Enter passphrase for key '/home/miguel/.ssh/id_rsa': 
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 4 (delta 0), reused 4 (delta 0), pack-reused 0
Unpacking objects: 100% (4/4), 1.01 KiB | 344.00 KiB/s, done.
From github.com:miguelbrandao/git-github-test
 * [new branch]      branch3    -> origin/branch3
Already up to date.

Neste exemplo, o pull apenas fez o download do novo branch, pois era a única diferença entre o nosso repositório e o remote. Como o branch, para nós, é novo, não causou conflitos.

Fetch

O git fetch apenas faz o download dos novos dados remotos. É útil para poder ver o que aconteceu no remote sem ter de fazer a integração.

Trabalhar em equipa

Se para um trabalho de grupo tiveres, no GitHub, um repositório privado (o que deves ter, pelo menos enquanto não és avaliado, por razões óbvias), precisas de dar permissões aos teus colegas para acederem ao teu repositório e poderem fazer pushes e pulls.

Tal é feito no separador Settings, em Manage Access. Para convidar um colaborador, basta clicar no botão, escrever o username do teu colega (que também precisa de ter uma conta no GitHub) e esperar que ele receba um email e aceite o convite.

github5

Depois de teres convidado os teus colegas, têm de definir um workflow. Como é que vão fazer os pushes para o remote e sincronizar o código entre vocês?

Uma solução básica é simplesmente trabalharem a partir do master. Ou seja, toda a gente trabalha num único branch, o master. Todos os commits de toda a gente são feitos para o master, e cada um, antes de fazer push, faz pull do master, resolve os possíveis conflitos e faz push. Funciona, mas pode correr mal. O que vai acontecer, muito provavelmente, é que vai ficar um esparguete de commits e vai ser muito difícil qualquer roll-back que se tenha de fazer. Além disso, fica bastante mais difícil saber que ficheiros e linhas é que um colega alterou.

Outra solução é utilizarem branches: por cada funcionalidade criam um branch, podendo até colocar como prefixo, no nome do branch, as iniciais do colega responsável por aquela funcionalidade. Assim, cada um tem o seu branch e pode ir fazendo pushes sem ter de se preocupar com conflitos. Quando a funcionalidade for acabada ou acharem necessário, podem fazer o merge para o master. Assim, reduzem a quantidade de vezes que têm de lidar com conflitos.

Pull Requests (PRs)

No entanto, a melhor solução é usarem os Pull Requests. Um Pull Request, tal como o nome indica, é um pedido para fazer pull. Quando um colega submete um Pull Request, está a pedir ao resto do grupo para reverem e aceitarem as alterações que ele fez ao código. Desta forma, a equipa sabe sempre quais as alterações feitas e tem mais facilidade a analisar a qualidade do código.

Nesta solução, como na anterior, o master reúne o trabalho acabado de toda a equipa. Por cada funcionalidade, é criado um branch. Quando o responsável por um branch achar que está pronto a ser merged com o master faz push do branch para o remote e cria um Pull Request.

Criar um Pull Request de um branch recentemente enviado é fácil: basta ir ao gitHub do repositório e clicar no botão Compare & pull request. O GitHub assume, por defeito, que queres fazer um Pull Request do teu branch para o master.

github6

De seguida, basta preencher o título, ou usar o pré-definido pelo GitHub, escrever um comentário, opcionalmente, e escolher Reviewers, o que notificará os colegas escolhidos que fixeste um Pull Request e precisas de uma Review.

github7

Enquanto o Pull Request está aberto, os teus colegas podem deixar comentário e sugestões. Se decideres alterar alguma coisa, basta fazê-lo no mesmo branch e, quando terminares, fazeres o git push normal. O GitHub adicionará automaticamente os novos commits ao teu Pull Request.

Quando o teu Pull Request for aprovado podes fazer merge. Podes escolher uma de três opções para o fazer (é importante para a organização do repositório que toda a equipa use a mesma opção):

  • Create a merge commit: esta é o equivalente a fazer git merge, mantém todos os commits e deixa intacta o histórico do repositório;
  • Squash and merge: esta opção altera a história do repositório: se tiveres mais do que um commit no teu branch, esta opção junta-os todos num único commit, fazendo depois um merge normal. Esta é uma opção interessante porque, durante o desenvolvimento, muitas vezes fazemos commits pequenos que não importam na escala do projeto e que tornariam difícil a consulta do histórico do ´master´, devido à sua quantidade, sendo substituídos por um único commit que engloba toda a funcionalidade.
  • Rebase and merge: também é uma opção interessante e também altera a história do repositório. No entanto, é uma opção mais avançada que não está no escopo deste guião.

github8

E pronto! As tuas alterações fazem parte do master. Assim, se já não precisares dele, podes eliminar o branch remoto (o teu branch local não é eliminado, tal só pode ser feito por ti no teu PC).

Conclusão

O mundo do Git é imenso, mas agora, se chegaste até aqui, deves conseguir pelo menos ter uma noção de como funciona e como pode melhorar a tua vida enquanto estudante de MIEI e futuro software developer.

Há muitos sites onde podes aprender e muitas ferramentas que podes instalar para melhorar a forma como interages com o Git e o GitHub, é só procurar :)

Este site, por exemplo, é forma engraçada de aprenderes e praticares um pouco de forma interativa.

Se adoraste este guia, se o destestaste, se tens críticas construtivas ou ideias que o possam melhorar, podes-nos contactar diretamente por email (pedagogico@cesium.uminho.pt).

Bibliografia