Programação não linear utilizando Git
Tabela de conteúdo |
Programação não linear com GIT
OBS: este texto está em constante alteração e desenvolvimento. Como diz a lei de Linus, segundo Eric Raymond, "release early, release often".
Um pouco de teoria para começar
Programação não linear, no contexto deste texto, refere-se a uma metodologia de programação onde você vai alterando partes distintas do sistema ao mesmo tempo com o intuito de mesclá-las ao final do processo.
O processo assemelha-se à edição de vídeo não linear: o editor vai trabalhando em partes distintas do filme, de acordo com seu interesse, disponibilidade de tempo, etc e, no final do processo, junta todas as partes para compor o filme como um todo.
A maneira mais fácil de se trabalhar com programação não linear é com um sistema de controle de versões que tenha uma implementação dos recursos de branching e merging fácil de usar. No caso deste texto iremos utilizar o Git, sistema de controle de versão criado por Linus Torvalds para gerenciar o código da Kernel do Linux após os problemas entre os desenvolvedores e a empresa fabricante do BitKeeper, controle de versão utilizado para a kernel até então.
Também existe toda uma teoria de programação não linear com algoritmos, objetivos e fórmulas próprias. Para mais informações consulte o verbete na Wikipedia: Nonlinear Programming
Tutorial
(obs: esse tutorial iniciou como uma tradução do artigo Git merging by example escrito por Jonathan Rockway)
Vamos iniciar criando um novo projeto e colocá-lo em um repositório git.
$ mkdir teste-git $ cd teste-git $ git init
Agora iremos criar um arquivo para brincar, o teste.pl:
#!/usr/bin/env perl print "Olá, mundo.\n"; __END__
E fazer o commit:
$ git add teste.pl $ git commit -m "importando projeto" [master (root-commit) 1336ee1] imporando projeto 1 files changed, 5 insertions(+), 0 deletions(-) create mode 100644 teste.pl
Nesse ponto iremos criar uma nova ramificação (branch) para que possamos refatorar essa bagunça toda sem arruinar o código que funciona:
$ git checkout -b refatoracao Switched to a new branch 'refatoracao'
Agora que estamos no branch de refatoração, vamos reescrever o código colocando a declaração de impressão ("print") em uma subrotina:
#!/usr/bin/env perl diga_ola(); sub diga_ola{ print "Olá, mundo.\n"; } __END__
E fazer o commit:
$ git commit -a -m 'refatorado o print em uma subrotina'; [refatoracao 38359be] refatorado o print em uma subrotina 1 files changed, 5 insertions(+), 1 deletions(-)
I have an idea for a new UI feature, so I'm going to make a branch here, but not switch to it, since I want to add another feature on this refactor branch first. Eu tenho uma idéia para um novo recurso para a UI (User Interface - Interface com o usuário), então iremos criar um branch aqui, mas não iremos ir para ele ainda pois ainda desejamos adicionar outro recurso no branch de refatoração antes.
$ git branch nova-ui
Enquanto continuamos no branch refatoração, vamos nos livrar daquela quebra de linha horrível ("\n")
#!/usr/bin/env perl use 5.010; diga_ola(); sub diga_ola{ say "Olá, mundo."; } __END__
(Nota para não programadores perl; nós adicionamos o use 5.010 para utilizar o recurso say. Infelizment o programa agora depende do perl 5.10 em vez de perl 5.qualquer coisa)
Muito melhor. Agora vamos fazer o commit
$ git commit -a -m 'usar say em vez de print' [refatoracao 86e7c56] usar say em vez de print 1 files changed, 2 insertions(+), 1 deletions(-)
OK, com essa alteração feita, vamos mexer no branch nova-ui:
$ git checkout nova-ui
Switched to branch 'nova-ui'Agora vamos adicionar aquele recurso super legal, isto é, imprimir "Olá, mundo" três vezes em vez de apenas uma. Você acredita que o chefe vai aceitar essa mudança antes mesmo de converter todos os servidores para Perl 5.10, então você adiciona nesse branch o qual não requer perl 5.10.
#!/usr/bin/env perl diga_ola(); diga_ola(); diga_ola(); sub diga_ola{ print "Olá, mundo.\n"; } __END__
Vamos fazer o commit das alterações:
$ git commit -a -m 'diga ola 3 vezes' [nova-ui d39bcd0] diga ola 3 vezes 1 files changed, 2 insertions(+), 0 deletions(-)
A parte chata é que a gerênciz não aprovou aquelas alterações na interface e eles não deixaram você atualizar seu servidor de produção para 5.10 ainda. Então você volta para trabalhar na tarefa que necessita ser executada imediatamente -- adicionar documentação ao seu código.
$ git checkout master
Switched to branch "master"E então edite o arquivo:
#!/usr/bin/env perl print "Olá, mundo.\n"; __END__ =head1 NAME teste.pl diga ola para o mundo =head1 SYNOPSIS perl teste.pl
E faça o commit:
$ git commit -a -m 'adicionada documentacao' [master ef98694] adicionada documentacao 1 files changed, 8 insertions(+), 0 deletions(-)
Com toda essa produtividade você sente que merece uma grande caneca de café, então vai para a cafeteria, compra um e volta. Confere seu e-mail e descobre que a equipe responsável pela interface (UI) adorou sua alteração que "diz ola 3 vezes". Então vamos mesclar (merge) ela no branch principal (master):
$ git pull . nova-ui From . * branch nova-ui -> FETCH_HEAD Auto-merging teste.pl Merge made by recursive. teste.pl | 8 +++++++- 1 files changed, 7 insertions(+), 1 deletions(-)
Seu arquivo ficou assim agora:
#!/usr/bin/env perl diga_ola(); diga_ola(); diga_ola(); sub diga_ola{ print "Olá, mundo.\n"; } __END__ =head1 NAME teste.pl diga ola para o mundo =head1 SYNOPSIS perl teste.pl
Se você executar o git log irá perceber que git adiciona cada mudança que você mesclou no histórico:
commit 21e3049caf792b537d0c25c1cbd52076147e15c2 Merge: ef98694 d39bcd0 Author: Fernando Micheloti <artista@frustrado.com.br> Date: Wed Aug 19 14:26:12 2009 -0300 Merge branch 'nova-ui' commit ef98694da11ab0b481ac2d389d44808f1832e6ef Author: Fernando Micheloti <artista@frustrado.com.br> Date: Wed Aug 19 14:25:38 2009 -0300 adicionada documentacao commit d39bcd0d2929a09751e9784bb43e5db6418ec14c Author: Fernando Micheloti <artista@frustrado.com.br> Date: Wed Aug 19 14:23:30 2009 -0300 diga ola 3 vezes commit 38359be952fca46e4458f45195fc6d5df68f8f1e Author: Fernando Micheloti <artista@frustrado.com.br> Date: Wed Aug 19 14:20:05 2009 -0300 refatorado o print em uma subrotina commit 1336ee1449c586b5aafc439d81ecc0d84c2b4d62 Author: Fernando Micheloti <artista@frustrado.com.br> Date: Wed Aug 19 14:18:17 2009 -0300 importando projeto
Agora você descobriu que todas as cópias do Perl 5.8 foram destruídas (culpe as tempestades solares), e você terá que atualizar para o Perl 5.10. Por causa disso você pode mesclar seu 5.10 branch. Que sorte! O grande problema é que o branch nova-ui que você acabou de mesclar possui alguns commits em comum com o branch refatoração que você acabou de mesclar. O git irá tentar aplicar essas alterações dias vezes? (não.)
Vamos tentar:
$ git pull . refatoracao From . * branch refatoracao -> FETCH_HEAD Auto-merging teste.pl Merge made by recursive. teste.pl | 3 ++- 1 files changed, 2 insertions(+), 1 deletions(-)
Como esperado, funcionou perfeitamente. E aque está o arquivo final:
#!/usr/bin/env perl use 5.010; diga_ola(); diga_ola(); diga_ola(); sub diga_ola{ say "Olá, mundo."; } __END__ =head1 NAME teste.pl diga ola para o mundo =head1 SYNOPSIS perl teste.pl
Se você conferir os logs irá notar que o git sabe exatamente quais alterações foram incluídas por essa mesclagem (merge):
commit 5f50a42c2eeca35eec400a77e3728a46c3cfb495 Merge: 21e3049 86e7c56 Author: Fernando Micheloti <artista@frustrado.com.br> Date: Wed Aug 19 14:28:29 2009 -0300 Merge branch 'refatoracao' commit 21e3049caf792b537d0c25c1cbd52076147e15c2 Merge: ef98694 d39bcd0 Author: Fernando Micheloti <artista@frustrado.com.br> Date: Wed Aug 19 14:26:12 2009 -0300 Merge branch 'nova-ui' commit ef98694da11ab0b481ac2d389d44808f1832e6ef Author: Fernando Micheloti <artista@frustrado.com.br> Date: Wed Aug 19 14:25:38 2009 -0300 adicionada documentacao commit d39bcd0d2929a09751e9784bb43e5db6418ec14c Author: Fernando Micheloti <artista@frustrado.com.br> Date: Wed Aug 19 14:23:30 2009 -0300 diga ola 3 vezes commit 86e7c56de1985e5e76e0e0429b6d6422e6ad73aa Author: Fernando Micheloti <artista@frustrado.com.br> Date: Wed Aug 19 14:21:53 2009 -0300 usar say em vez de print commit 38359be952fca46e4458f45195fc6d5df68f8f1e Author: Fernando Micheloti <artista@frustrado.com.br> Date: Wed Aug 19 14:20:05 2009 -0300 refatorado o print em uma subrotina commit 1336ee1449c586b5aafc439d81ecc0d84c2b4d62 Author: Fernando Micheloti <artista@frustrado.com.br> Date: Wed Aug 19 14:18:17 2009 -0300 importando projeto
Apesar de você ter mesclado as alterações no master você ainda pode trabalhar nelas:
$ git checkout nova-ui Switched to branch 'nova-ui' $ git rebase master First, rewinding head to replay your work on top of it... Fast-forwarded nova-ui to master.
Rebase irá mesclar o master no branch atual, porém isso funciona apagando seus commits, trazendo os commits do master e então reaplicando os seus sobre eles. Isso evita o commit desnecessário "merge brange 'foo'" mas com o preço de ser incapaz de compartilhas suas alterações com outro repositório (pois os ids dos commits irão mudar; ids dos commits dependem do histórico e reabase reescre o histórico).
O caso mais comum de uso para o rebase é quando você está trablhando num recurso que demora alguns dias para programar e quer sincronizá-lo com o upstream diariamente. Ao invés de ficar puluindo seu branch de recursos com mesclas você pode fazer o rebase e ninguém irá saber que suas alterações não foram feitas contra o branch de upstream de hoje. (Se existirem conflitos, git irá permitir que você os resolva para cada um dos paches e irá lembrar como você resolveu em caso da mesma coisa acontecer quando você fizer um rebase amanhã. Veja git rerere --help para mais detalhes)
Agora, depois do rebase, nosso branch nova ui está atualizada com o master. Caso você faça alguma alteração aqui poderá mesclá-las com o master sem nenhum problema.
Vamos a mais um exemplo; nós iremos ver como mesclar dois branches ao mesmo tempo. Nós iremos voltar ao master e fazer um branch do-refatoracao pois o programa realmente necessita de mais documentação.
$ git checkout master Switched to branch 'master' $ git checkout -b doc-refatoracao Switched to a new branch 'doc-refatoracao'
E aqui iremos adicionar alguns POD para o final do arquivo:
__END__ =head1 NAME teste.pl - diga ola para o mundo =head1 SYNOPSIS perl teste.pl =head1 HISTORY A partir da versão 0.01, C<teste.pl> agora imprime "Olá, mundo." três vezes, para o melhor aproveitamento.
Ainda não está perfeito, mas vamos fazer o commit.
$ git commit -a -m 'explicar o recurso 3x' [doc-refatoracao 20e2962] explicar o recurso 3x 1 files changed, 4 insertions(+), 0 deletions(-)
Enquanto isso, um bug crítico foi descoberto -- um usuário pauso tempo demais enquanto lendo a vírgula em "Olá, mundo" então você tem que removê-la. Nós iremos criar um branch do master pois essa complicada correção pode demorar alguns dias de teste para ser completamente implementado:
$ git branch correcao-bug-virgula master $ git checkout correcao-bug-virgula Switched to branch 'correcao-bug-virgula' # concerta isso $ git commit -a -m 'correcao do bug da virgula' [correcao-bug-virgula 5e193a9] correcao do bug da virgula 1 files changed, 1 insertions(+), 1 deletions(-)
Isso foi mais fácil do que o esperado. Sendo que a correção do bug foi simples e sua documentação está feita, você está pronto para compartilhar essas alterações com seus colegas de trabalho mesclando-as no master:
$ git checkout master Switched to branch 'master' $ git pull . correcao-bug-virgula doc-refatoracao From . * branch correcao-bug-virgula -> FETCH_HEAD * branch doc-refatoracao -> FETCH_HEAD Trying simple merge with 5e193a9b5ec801176a25410c34c1b288d326b5f5 Trying simple merge with 20e29624a91a5029109762430e9e4df59d8823b2 Simple merge did not work, trying automatic merge. Auto-merging teste.pl Merge made by octopus. teste.pl | 6 +++++- 1 files changed, 5 insertions(+), 1 deletions(-)
Finalmente, você não precisa mais de todos aqueles branches e pode destruí-los:
$ git branch -d refatoracao nova-ui correcao-bug-virgula doc-refatoracao Deleted branch refatoracao (was 86e7c56). Deleted branch nova-ui (was 5f50a42). Deleted branch correcao-bug-virgula (was 5e193a9). Deleted branch doc-refatoracao (was 20e2962).
Git irá apenas apagar branches que foram completamente mesclados com o branch atual então você não precisa se preocupar com perda de dados.
Concluindo, git torna o desenvolvimento não linear fácil É inteligente, então as mesclas (merges) apenas funcionam; você não precisa mais se preocupar mais com branches. Além disso, se você preferir ferramentas visuais, , tente executar gitk --all. Ele irá desenhar um gráfico interativo de todos os seus pontos de mescla, para que você possa vizualizar o estado de todos os seus branches.
Criando um patch - diferença entre dois branches
A implementação de diff/patch do Git leva, no arquivo de patch, todo o histórico de alterações. Nem sempre isso pode ser desejável então coloco abaixo 2 formas de se criar o patch: usando o sistema de diff e patch do próprio Git e utilizando-se de um arquivo de patch padrão do unix criado pelo próprio Git mas aplicado com o comando patch.
Patch levando o histórico de alterações
Vou demonstrar um exemplo prático utilizando um dos projetos que estou mantendo, o FreeFood.
Primeiramente vamos fazer um clone do repositório
$ git clone git://github.com/artistafrustrado/FreeFood.git Initialized empty Git repository in /tmp/git/FreeFood/.git/ remote: Counting objects: 78, done. remote: Compressing objects: 100% (69/69), done. remote: Total 78 (delta 27), reused 0 (delta 0) Receiving objects: 100% (78/78), 91.35 KiB | 50 KiB/s, done. Resolving deltas: 100% (27/27), done.
Agora vamos criar um branch para nossas alterações
$ cd FreeFood/ $ git checkout -b refactor Switched to a new branch 'refactor'
Faça todas as alterações no codigo que achar necessárias.
Volte para o branch master e o atualize (git pull). Faça o checkout do branch refactor novamente, execute um rebase com o master e execute git format-patch.
$ git checkout master Switched to branch 'master' $ git checkout refactor $ git rebase master $ git rebase master First, rewinding head to replay your work on top of it... ... $ git format-patch master --stdout > /tmp/seu-aquivo-patch.diff
Para aplicar o patch. Vamos para o outro repositório. Para esse exemplo iremos utilizar outro clone do FreeFood mas caso vá utilizar um repositório existente substitua o git clone por git pull. Criamos um novo branch para aplicar o patch e testá-lo.
$ git clone git://github.com/artistafrustrado/FreeFood.git Initialized empty Git repository in /tmp/git/FreeFood/.git/ remote: Counting objects: 78, done. remote: Compressing objects: 100% (69/69), done. remote: Total 78 (delta 27), reused 0 (delta 0) Receiving objects: 100% (78/78), 91.35 KiB | 50 KiB/s, done. Resolving deltas: 100% (27/27), done. $ cd FreeFood/ $ git checkout -b refactor_frustrado Switched to a new branch 'refactor_frustrado' $ git am < /tmp/your-patch-file.diff Applying: * Added TODO file * Added instalation instructions to README file Applying: * Created INSTALL file
Após confirmação de que as alterações estão OK vamos fazer o merge do branch refactor_frustrado de volta para o master e apagá-lo.
$ git checkout master Switched to branch 'master' $ git am < /tmp/seu-arquivo-patch.diff $ git pull . refactor_frustrado From . * branch refactor_frustrado -> FETCH_HEAD Updating 9e22347..4d3ee92 Fast forward INSTALL | 4 ++++ README | 2 +- TODO | 2 ++ 3 files changed, 7 insertions(+), 1 deletions(-) create mode 100644 INSTALL create mode 100644 TODO $ git branch -d refactor_frustrado Deleted branch refactor_frustrado (was 4d3ee92).
Patch sem levar todo o histórico
Se você, como eu, faz vários commits durante o desenvolvimento que são dispensáveis no histórico geral do projeto e gostaria de ignorá-los e somente criar um commit com todas as alterações que fez no código em um único commit, essa dica é para vocês.
No seu repositório de trabalho.
$ git checkout refactor $ git diff master..refactor > /tmp/seu-arquivo-patch-sem-histórico.diff
No repositório que vai aceitar o patch
$ git clone git://github.com/artistafrustrado/FreeFood.git $ git checkout -b refactor_frustrado $ diff -p -i /tmp/seu-arquivo-patch-sem-histórico.diff $ git commit -a
Caso algum arquivo novo tenha sido criado no patch o git-commit vai avisá-lo. Cancele, faça o git add deles e reinicie o commit. Não esqueça de detalhar todas as alerações feitas no antigo branch pois todo os histórico foi ignorado pois utilizamos basicamente um patch padrão do unix para transferir as diferenças entre os branches.
Faça seus testes e, caso tudo esteja funcionando ao seu agrado, faça o merge com o master e apague o branch de trabalho, no meu caso o refactor_frustrado.
$ git checkout master Switched to branch 'master' $ git pull . refactor_frustrado From . * branch refactor_frustrado -> FETCH_HEAD Updating 9e22347..4d3ee92 Fast forward INSTALL | 4 ++++ README | 2 +- TODO | 2 ++ 3 files changed, 7 insertions(+), 1 deletions(-) create mode 100644 INSTALL create mode 100644 TODO $ git branch -d refactor_frustrado Deleted branch refactor_frustrado (was 4d3ee92).
Criando tags para versionamento de releases do software
$ git tag 1.0 -a
E, caso necessário, fazer o push para um repositório remoto.
$ git push --tagsMais Informações
- Pro Git Book
- GitSharp
- git-cheat-sheet.txt
- http://lwn.net/Articles/210045/
- http://wiki.locaweb.com.br/pt-br/Usando_GIT_na_Hospedagem_Linux
- http://caiustheory.com/adding-a-remote-to-existing-git-repo
- http://quirkygba.blogspot.com/2007/10/using-git-with-google-code-hosting.html
- http://code.google.com/p/tortoisegit/
- http://code.google.com/p/msysgit/
git-svn
- http://google-opensource.blogspot.com/2008/05/develop-with-git-on-google-code-project.html
- http://quirkygba.blogspot.com/2007/10/using-git-with-google-code-hosting.html
- http://google-opensource.blogspot.com/2008/05/export-git-project-to-google-code.html
- http://code.google.com/p/support/wiki/ImportingFromGit
Submodules
- http://gaarai.com/2009/04/20/git-submodules-adding-using-removing-and-updating/
- https://git.wiki.kernel.org/index.php/GitSubmoduleTutorial
Eclipse
- http://ekkescorner.wordpress.com/2010/02/15/dvcs-install-gitegit-and-mercurialhgeclipse/
- http://www.vogella.de/articles/EGit/article.html
- http://wiki.eclipse.org/EGit/User_Guide
- http://eclipse.org/egit/
Tutoriais
- http://www.vogella.de/articles/Git/article.html
- http://www.kernel.org/pub/software/scm/git/docs/gittutorial.html
- http://progit.org/