Created at:

Modified at:

Notas sobre o git

Primeiros passos com o Git

Comecei com sistemas de controle de revisão já há algum tempo, com CVS. Naquela época, já existiam softwares de controle de revisão mais modernos mas simplesmente amávamos a simplicidade do CVS. Algum tempo depois a empresa que eu trabalhava mudou o sistema, mas que ainda era da abordagem cliente-servidor (centralizado): começamos a utilizar o Subversion, que ainda acho o melhor sistema para um ambiente centralizado, como em algumas empresas, mas essa é uma outra história.

CVS

Subversion

Quando comecei a desenvolver software livre, fui apresentado aos sistemas de versionamentos modernos e decentralizados. Depois de experimentar o Mercurial, Bazaar (usado pela Canonical para no projeto Ubuntu Linux), Monotone (sistema muito interessante que foi uma das principais inspirações para Linus Torvalds construir o git) e Fossil (outro sistema muito interessante que tem um sistema de rastreamento de bugs -- bug tracking -- junto com o sistema de versionamento de código). Também experimentei o git, que tem a enorme vantagem de ter sido adotado por uma comunidade gigantesca de desenvolvedores, dando origem a vários serviços que proveem hospedagem com o git, como o GitHub e outros.

Mercurial

Bazaar

Monotone

Fossil

git

GitHub

Esse guia não irá te dizer a diferença entre os sistemas mencionados (vale a pena dar uma olhada em cada um deles) mas irá te ajudar com o básico do git. Antes de começar, verifique se você tem o git instalado em sua máquina. Iremos utilizar a interface de linha de comando. Existem interfaces gráficas para o git, tanto quanto plugins para sua IDE favorita, mas vamos nos ater à simples, e ainda poderosa, interface de linha de comando.

Iniciando com um serviço de hospedagem

Se você não está utilizando o GitHub, pode ir direto para a seção "Modo standalone".

Primeiro: git é um sistema de controle de versão *decentralizado*, no qual você não precisa de um servidor. Você pode sincronizar repositórios de diferentes máquinas, como seu desktop, seu laptop e computadores de seus amigos. Mas ele permite que você tenha um *repositório de referência* a partir do qual todos podem adquirir o código-fonte. Você pode começar usando um dos vários serviços de hospedagem git. Vamos começar com o mais comum: GitHub. O GitHub pede que você registre uma chave SSH para que você possa colocar código no seu repositório remoto. Neste artigo, não vamos entrar em detalhes em como fazer isso. Você pode ver o link abaixo para maiores informações.

github-ssh

Crie uma conta no GitHub, configure suas chaves SSH e crie um repositório de teste. GitHub irá te fornecer um link para que você faça o *clone* de seu repositório. Então, simplesmente o clone com git clone!

    $ git clone git@github.com:username/project.git

Estou supondo que o nome de usuário é username e o nome do repositório que você escolheu é project.

O comando acima irá criar um diretório chamado project na sua máquina. É um diretório vazio, porque seu repositório no GitHub também está vazio por enquanto. Dentro dele, existe um diretório solitário com o nome .git que tem metadados de seu repositório e no qual você não deve fazer alterações (a menos que você saiba o que está fazendo).

Modo standalone

Se você não quer utilizar o GitHub, pode configurar seu próprio repositório. Primeiro, crie o diretório de seu projeto, então utilize o git init:

    $ mkdir project
    $ cd project
    $ git init

git init

É importante notar que, diferentemente do CVS or do Subversion, o repositório não é armazenado em outro lugar, mas dentro do diretório da cópia de trabalho. Depois que você modificar seus arquivos no diretório project, faça um commit (veja "Brincando com o git") e as mudanças serão armazenadas no diretório .git.

Brincando com o git

De acordo com o gittutorial, depois de instalar o git, é uma ideia dizer quem você é, com git config:

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

gittutorial

git config

Será criado um arquivo no seu home chamado .gitconfig com esses e outras configurações que você mudou com git config. É importante que você se identifique, como acima, assim os commits serão atribuídos corretamente aos desenvolvedores.

Agora, dentro do diretório project, crie dois arquivos, fileA.txt e fileB.txt. Neles, coloque qualquer conteúdo, pois isso não importa por enquanto. A estrutura será como essa:

    * project/
        * fileA.txt
        * fileB.txt

Agora, adicione arquivos ao seu repositório com git add:

    $ git add *

git add

Eles estão *marcados para adição*, mas não foram salvas no repositório ainda.

Na verdade, git add adiciona arquivo à *stage area* ou *index*. Essa é uma funcionalidade muito poderosa do git, mas está além do escopo desse texto. Para saber mais, leia sobre a "stage area":

staging-area

Em português, não há uma palavra adequada para traduzir commit. Neste tutorial vamos utilizar o verbo *salvar* quando nos referirmos ao verbo to commit em inglês, ou ainda vamos dizer a expressão *fazer commit*.

Para salvar os arquivos no repositório, use git commit:

    $ git commit

git commit

Seu editor de texto favorito surgirá, no qual é uma boa ideia escrever uma mensagem, explicando brevemente o que você mudou neste commit. Quando se trata do primeiro commit de um repositório, costumo colocar a mensagem Initial commit.

Agora, faça uma mudança nos seus arquivos e use git diff_ para ver as diferenças::

    $ git diff

git diff

Voilà! Esse comando mostra as diferenças entre a última versão salva e os arquivos na cópia de trabalho. Para salvar as mudanças, execute::

    $ git commit -a

Não esqueça da mensagem de commit com um resumo das mudanças que você fez. A flag -a é necessária para não ter que digitar git add para adicionar as mudanças. Git não salva as mudanças que você fez nos arquivos com git commit antes de marcá-las para adição (mesmo que as mudanças tenham sido feitas nos arquivos já adicionados ao repositório anteriormente).

Você pode passar a mensagem de commit para o comando git commit diretamente, sem abrir o seu editor. Utilize a flag -m::

    $ git commit -a -m 'Commit message comes here'

Para novos arquivos git commit -a não vai funcionar. Você terá que adicioná-los manualmente com git add.

Finalmente, o comando git log irá mostrar todo o histórico de mudanças. Se você não estiver convencido da importância de preencher uma mensagem de commit, você deve se convencer agora. O comando git log mostra as alterações feitas, junto com o nome do autor, data, etc.

git log

Vejam como fica a saída dos comandos executados acima, em um repositório de teste que fiz:

    $ cd /tmp/
    $ mkdir project
    $ cd project/

    $ git init
    Initialized empty Git repository in /tmp/project/.git/

    $ touch fileA.txt fileB.txt

    $ git add *

    $ git commit -a -m 'Initial commit'
    [master (root-commit) 112f5c1] Initial commit
     2 files changed, 0 insertions(+), 0 deletions(-)
     create mode 100644 fileA.txt
     create mode 100644 fileB.txt

    $ echo 'A test line' >> fileA.txt

    $ git diff
    diff --git a/fileA.txt b/fileA.txt
    index e69de29..9efab9b 100644
    --- a/fileA.txt
    +++ b/fileA.txt
    @@ -0,0 +1 @@
    +A test line

    $ git commit -a -m 'Adding a test line'
    [master cc28b9f] Adding a test line
     1 file changed, 1 insertion(+)

    $ git log
    commit cc28b9f73a5d487c9d3166b0a3585926ab7d8961
    Author: My Name <my@email.com>
    Date:   Thu Jan 5 15:39:43 2017 -0200

        Adding a test line

    commit 112f5c1ec517bb25d1a054c063652c796d918a77
    Author: My Name <my@email.com>
    Date:   Thu Jan 5 15:39:24 2017 -0200

        Initial commit

Repositório e cópia de trabalho

Em um sistema de controle de revisão tradicional e centralizado como o CVS e o Subversion, o repositório é sempre separado da cópia de trabalho local, onde desenvolvedores trabalham. O repositório é normalmente localizado em uma máquina remota e o usuário faz checkout do repositório para a cópia de trabalho e faz commit da cópia de trabalho ao repositório.

No git, o repositório fica junto da cópia de trabalho. Veja todos os arquivos de seu project:

    $ ls -A1
    .git
    fileA.txt
    fileB.txt

Juntamente com fileA.txt e fileB.txt existe o diretório .git. Esse é seu repositório! Todos os logs, commits, endereços de repositórios remotos, etc., está dentro desse diretório e se você excluí-lo irá perder todos os dados.

O repositório localizado no GitHub é um tipo especial de repositório chamado *repositório bare*.

Repositórios bare

Repositórios bare são como repositórios locais, sem a cópia de trabalho. Eles são concebidos para armazenar apenas a informação do repositório, não havendo a necessidade da cópia de trabalho. Supõe-se que os desenvolvedores não tenham acesso direto a um repositório bare e devem acessá-lo remotamente, utilizando os comandos do git.

Para configurar um repositório bare, passe a opção --bare ao comando git init. Vamos fazer isso com um outro exemplo::

    $ cd /tmp/
    $ mkdir project2.git
    $ cd project2.git
    $ git init --bare
    Initialized empty Git repository in /tmp/project2.git/

Você pode agora cloná-lo, como faria com qualquer outro repositório::

    $ cd /tmp
    $ git clone /tmp/project2.git /tmp/project2-working-copy
    Cloning into '/tmp/project2-working-copy'...
    warning: You appear to have cloned an empty repository.
    done.

Git nos avisa que clonamos um repositório vazio. Mas isso não importa. Podemos agora adicionar arquivos, fazer commit e por aí em diante. Mas os arquivos ficarão em nosso clone (/tmp/project2-working-copy). Para enviar as mudanças de volta ao repositório que nós acabamos acabamos de clonar, utilizamos o comando git push_::

    $ git push origin master

git push

origin é o repositório que nós clonamos (esse nome pode ser mudado com git remote rename). master é o nome da branch que estamos sincronizando com os outros repositórios. Veremos sobre branches na seção "Branch, checkout e merge".

Na discussão de e-mails do git existe uma discussão interessante sobre repositórios bare que talvez você queira dar uma olhada:

Discussion: how to use two bare repositories?

Remote, push, fetch e pull

Agora que você sabe o que repositórios são (e repositórios bare), vamos descobrir como sincronizá-los. Voltei ao diretório project (que você fez clone do GitHub) ou project2 (o que você clonou de seu repositório bare). Utilize o comando git remote:

    $ git remote -v
    origin  git@github.com:username/project.git (fetch)
    origin  git@github.com:username/project.git (push)

git remote

Quando clonamos um repositório, o nome do repositório que clonamos é, por padrão, origin (você pode mudá-lo com git remote mv). Quando fazemos commit ao repositório, mudanças **não** são sincronizadas de volta ao repositório origin por padrão. Para enviar as mudanças, precisamos executar o seguinte comando::

    $ git push origin master

origin é o repositório ao qual desejamos enviar as mudanças e master é o nome do branch que queremos sincronizar. Verifique a seção ;Branch, checkout e merge" para mais informações sobre branches.

Você pode adicionar diferentes remotes. Use o comando git remote add::

    $ git remote add test mygithost:/foo/bar
    $ git remote -v
    origin  git@github.com:username/project.git (fetch)
    origin  git@github.com:username/project.git (push)
    test    mygithost:/foo/bar (fetch)
    test    mygithost:/foo/bar (push)

Agora pode enviar também as mudanças ao repositório remoto test. Sem dúvidas, o ambiente do git precisa também estar instalado e funcionando na máquina remota.

Agora, e se outro desenvolvedor também enviou suas mudanças para o repositório remoto? Como as obtemos? Usando o comando git pull_::

    $ git pull origin master

git pull

É o oposto de git push.

Existe também o comando git fetch_::

    $ git fetch origin

git fetch

A diferença entre git fetch e git pull é que o fetch obtém as diferenças do repositório remoto mas não as consolida na sua cópia de trabalho local, enquanto git pull já faz isso para você. Na prática, git pull é o mesmo que git fetch + git merge. Para mais informações sobre merge, veja a seção "Branch, checkout e merge".

Branch, checkout e merge

Uma tradução adequada para branch seria *galho*. Entretanto, o termo é tão comumente utilizado no original em inglês, que vamos utilizar a própria palavra branch no decorrer do texto.

Branching é normalmente uma funcionalidade avançada dos sistemas de revisão de controle mas no git é apresentado desde o início ao desenvolvedor, pois você trabalha com branches todo tempo, mesmo que não perceba ou que não use o comando git branch.

Branching

Todo repositório tem pelo menos um branch, normalmente chamado master (você pode mudar seu nome com git branch -m). Vamos experimentar em nosso diretório project:

    $ git branch
    * master

O asterisco no começo da linha mostra o branch atual. Para criar um novo branch, apenas passe um parâmetro para git branch_::

    $ git branch mybranch

git branch

E mostre os branches novamente:

    $ git branch
    * master
      mybranch

mybranch já está criado (é uma cópia exata de master) but não está ainda obtido na sua cópia de trabalho. Para mudar para esse branch, use git checkout::

    $ git checkout mybranch
    Switched to branch 'mybranch'

git checkout

E mostre os branches novamente:

    $ git branch
      master
    * mybranch

Agora, toda mudança salva com git commit irá no branch mybranch, não master.

Nota: O comando git checkout tem muitos usos diferentes, não apenas obter um branch diferente. Ele pode ser usado, por exemplo, para obter uma revisão antiga de um determinado arquivo.

Finalmente, e se você quiser trazer as mudanças para o branch master? Primeiro, precisamos mudar (checkout) para o branch que irá receber as modificações:

    $ git checkout master
    Switched to branch 'master'

E executamos o comando git merge:

    $ git merge mybranch
    Updating cc28b9f..af0381b
    Fast-forward
     fileA.txt | 1 +
     1 file changed, 1 insertion(+)

git merge

Nota: Você percebeu a linha Fast-forward? É o algoritmo utilizado por git merge. Git usa diferentes algoritmos de merge, dependendo do caso. git merge é um comando muito poderoso com muitas opções. Vale a pena ler seu manual__.

Outras páginas

Esse tutorial é apenas um ponto de partida para uma ferramenta tão poderosa. É altamente recomendável que você leia a documentação oficial do Git que tem muitas informações e também os manuais dos comandos que você está utilizando.

Git oficial documentation

Além disso, existe um livro chamado Pro Git que percorre desde aspectos básicos até os mais avançados. É altamente recomendado!

Pro Git book

Finalmente, quando tiver uma dúvida, procure pelas soluções na web. Muitas pessoas usam git e é provável que alguém já tenha postado algo em um fórum, fazendo as mesmas perguntas que você.

Boa sorte!

Dicas aleatórias

Fazendo mirror de repositórios bare

Como o git é um sistema decentralizado, você pode clonar repositórios bare e rastreá-los (tracking). É útil, digamos, se você tiver um repositório bare em algum lugar (exemplo: no GitHub), mas, por algum motivo, você ainda precisar de um repositório bare localmente. Suponha que a.git é o seu repositório primário e b.git é o repositório que você quer "copiar", e ambos estão localizados em /tmp (esse caminho pode ser remoto também), siga esses passos:

    $ cd /tmp/
    $ git clone --bare /tmp/a.git b.git
    $ cd b.git
    $ git remote add a.git /tmp/a.git # --mirror flag can appear here

Então, sempre que você quiser sincronizar b.git, obtendo as modificações feitas em a.git, faça:

    $ cd /tmp/b.git
    $ git fetch a.git

Discussion: how to use two bare repositories?

Branches e update de refs em um repositório bare

(2012-10-18)

Frequentemente utilizo repositórios bare não como "reposotório fim", mas como "repositório meio". Por exemplo, no meu servidor tenho alguns repositórios bare de projetos de software livre. Por que manter um repositório bare que rastreia outro repositório? Eles são organizados da seguinte forma:

Então, nesse diagrama, "upstream" é um repositório hospedado em provedores como o SourceForge ou o GitHub. "origin" é um repositório bare no meu servidor pessoal que clona o "upstream" e obtém novas mudanças a partir dele. É onde eu faço "push" das minhas branches privadas. "work" é a minha cópia local de trabalho.

O problema aparece quando obtenho mudanças do "upstream", usando git fetch:

    $ git fetch upstream

git-fetch

Trará "objetos", mas não "refs". Refs (dentro do diretório refs no diretório de controle do git) são apenas arquivos texto apontando para commits na árvore. Por exemplo, se você tem uma tag chamada "bla", haverá uma ref em refs/tags/bla e, se você ver seu conteúdo com cat, verá uma hash representando um commit. Um exemplo de minha ref refs/heads/master atual:

    $ cat refs/heads/master
    a82cd88b10906fae8d5a0f10020eb6d7fb9274a7

Então, de volta para o nosso exemplo, em um repositório bare, não posso fazer "rebase" ou mesclar ("merge") branches, como faria com um repositório com cópia de trabalho. E quando uso o git-fetch seme specificar outros argumentos, ele não atualiza as refs. Então, tenho que usar:

    $ git fetch upstream master:master

Então o arquivo refs/heads/master será atualizado para o ponto em que o "head" de "upstream" aponta. Às vezes pode não ser fácil saber o nome da branch que você está obtendo as modificações (em algumas situações como um shell script, por exemplo), então pode ser uma boa ideia usar o git-update-ref...

    $ git update-ref refs/heads/master refs/remotes/upstream/master

... irá atualizar refs/heads/master, para que aponte para o mesmo objeto que refs/remotes/upstream/master aponta.

git-update-ref

Trabalhando com bundles

Às vezes você não tem uma conexão de internet para fazer push e pull nos repositórios. Mover seus dados para outro tipo de mídia (email, um HD, etc.), use git bundle. Ele cria um "bundle" (um arquivo) com o conteúdo de um repositório. Esse arquivo pode ser transferido de forma segura.

Primeiro, entre no diretório de seu repositório

    $ cd myrepo

Então, cria o bundle com todas as branches, tags, (todas as refs), etc.:

    $ git bundle create myrepo.bundle --all

Irá criar um bundle com todas as branches. Depois, para clonar para um repositório com uma cópia de trabalho, faça:

    $ git clone myrepo.bundle -b master myrepo

Isso irá criar o diretório myrepo com a branch master. Você pode então obter as outras branches:

    git fetch ../myrepo.bundle mybranch:mybranch

Excluindo uma branch remota

(2014-05-30)

Se sua branch remota é chamada "foo", faça:

    $ git push origin :foo

Veja a seção "remote: *** Deleting a branch is not allowed in this repository" se você ver um erro como esse.

Resolução de problemas

Essa seção tenta explicar algumas dificuldades que tive tentando configurar o Git, e as soluções que encontrei.

Erro "*** Project description file hasn't been set" ao usar git pull origin

Quando tentei enviar modificações (com git push) para um novo repositório: obtive a seguinte mensagem de erro:

    *** Project description file hasn't been set
    error: hooks/update exited with error code 1
    error: hook declined to update refs/heads/master

Tive que mudar o arquivo description do repositório remoto, o que significa que tive que prover uma pequena descrição para ele. Lembre-se: é o arquivo description do repositório remoto (não local).

"Project description file" error in git?

Push do Git dando erro de Project description

"fatal: git upload-pack: aborting due to possible repository corruption on the"

    Initialized empty Git repository in /tmp/repo/.git/
    error: git upload-pack: git-pack-objects died with error.
    remote: aborting due to possible repository corruption on the remote side.
    fatal: early EOF
    fatal: git upload-pack: aborting due to possible repository corruption on the remote side.
    fatal: index-pack failed

Antes de chamar git fsck desesperadamente, tenha certeza de que o shell do usuário remoto pode encontrar o git no $PATH. Meu problema é que eu tinha instalado o git pelo pkgsrc, que por padrão instala tudo em /usr/pkg.

pkgsrc

Isso foi facilmente resolvido fazendo:

    # cd /usr/local/bin
    # ln -s /usr/pkg/bin/git
    # ln -s /usr/pkg/bin/git-receive-pack
    # ln -s /usr/pkg/bin/git-upload-archive
    # ln -s /usr/pkg/bin/git-upload-pack

git-clone: path or ssh problem with git-upload-pack in 1.6.0?

erro: "patch failed .. Applying: .. trailing whitespace when git svn rebase"

Talvez você obtenha esse erro:

    Applying: <Commit messasge>
    .../path-to-repository/.git/rebase-apply/patch:21: trailing whitespace.

Quando git svn rebase retorna esse erro, significa que alguém fez commite no repositório subversion que "quebra" as restrições impostas pelo git sobre estilo de código. Para permitir que esse código seja "commitado" no seu repositório local, execute esse comando:

    git config core.whitespace -trailing-space,-space-before-tab

git-config

erro: "patch failed .. patch does not apply .. Patch already applied"

Depois de um git svn rebase, se você ver um erro como:

    Applying: <Commit message>
    error: patch failed: <file:line>
    error: <file>: patch does not apply
    Using index info to reconstruct a base tree...
    Falling back to patching base and 3-way merge...
    No changes -- Patch already applied.

Por alguma razão seu repositório local e seu repositório subversion não estão sincronizados e git tenta obter as mudanças do SVN que ja estão sincronizadas. Tente:

    git rebase --skip

Pode ser uma boa ideia executar git diff para verificar se o merge funcionou.

"warning: remote HEAD refers to nonexistent ref, unable to checkout."

Esse erro é causado quando feito clone de um bundle (verifique git-bundle para detalhes). O clone foi feito, mas git não obteve uma cópia de trabalho.

git-bundle

Um git checkout com os parâmetros corretos é suficiente. Então:

    $ git checkout -b master origin/master

remote: *** Deleting a branch is not allowed in this repository

Se você receber um erro como esse quando tentando excluir uma branch remota, possivelmente você tem um "hook" habilitado no repositório remoto que te impede de excluir a branch. Possivelmente está em seurepositorio.git/hooks/update.

Faça login na máquina remota e desabilite o "hook":

    $ git --git-dir /path/to/yourrepo.git config --bool hooks.allowdeletebranch true

Cannot delete remote branch in git

fatal: unable to access '...': SSL certificate

Isso normalmente acontece para certificados auto-assinados ao tentar fazer clone de um repositório. Para fazer o git ignorar a verificação de certificados, configure a variável GIT_SSL_NO_VERIFY:

    $ GIT_SSL_NO_VERIFY=true git ...