All articles, tagged with “git”

Your Own Git Hosting: Gitolite

So, let’s start with setting up gitolite to host your repositories and provide SSH access to them. There is also a tool called gitosis, but it is not as supported and featurefull as gitolite.

So what does it actually do? In essence, all the developers that will access your repositories will login to your server as user “git”, run server-side git daemon and sync data with it. Gitolite will determine if a user has access to perform operations he is going to. Ok, but why bother with all this crap, and not just create a bunch of users on the server and tell them where is the repository? There is a couple of reasons.

Firstly, you may not want to give your developers shell access to your server. Why wouldn’t you? Who knows, probably because they are stupid or malicious, or whatever. With gitolite they won’t get a shell access, because they will only be able to login with their SSH keys and that SSH keys will be restricted to invoking gitolite only. At this point you may want to read up SSH manual on keys and command restrictions, if you do not yet know about it.

Secondly, you may want to impose certain access restrictions on them. Gitolite allows to restrict read write and rewrite down to per-branch level.

Thirdly, with local multiuser git access you have to fiddle a little with umasks or write a hook that fixes the permissions, but that’s unimportant and uninteresting.

Fourthly, you may even not have a root access. That’s right, gitolite allows you to setup multi-user repositories on systems where you have only one non-root account.

So let’s go ahead with installation. The INSTALL doc is rather straightforward and you should in fact read it instead of this blog post. I am not offering the text below as a setup instructions, but merely as a log of how I installed it on my server.

First off, we require some git on our server. Please be careful to use as fresh git as possible, because it generally tends to gets even more super awesome with time. Also gitolite requires at least git 1.6.2 at the time of writing of this post. Unless you use especially sucky distribution (ahem debian ahem), you should be fine with git from your repository. I just told Portage to install me one:

    emerge -av git

Although gitolite has an ebuild in Gentoo, I set it up manually and will walk you along the procedure. First, lets obtain the sources:

    git clone git://github.com/sitaramc/gitolite.git

This will create a “gitolite” directory with gitolite sources. Let’s create a system directory for gitolite. I use /opt for all non-Portage packages, so let’s go ahead with:

    GITOLITE=/opt/gitolite
    sudo mkdir -p ${GITOLITE}/{bin,conf,hooks}
    sudo chown -R `whoami` ${GITOLITE}/{bin,conf,hooks}

You may of course use another set of directories if you want. Next, we let gitolite install itself wherever we want it:

    ./src/gl-system-install ${GITOLITE}/{bin,conf,hooks}

This takes three arguments: directory for binaries, directory for configs and directory for hooks. Also this is the same command you would use to update your gitolite installation after you git pull the new sources.

We would now require a user account for git. Let’s create them!

    sudo groupadd -r git
    sudo useradd -d /srv/git -g git -m -r -s `which bash` git

This creates a system (-r) group git and a system (-r) user git with home directory (-d) /srv/git and shell (-s) bash, in group (-g) git, and creates its home directory (-m). Now login into your new git account. Don’t forget to add your ${GITOLITE}/bin directory to the .bashrc if it is not in the PATH:

    sudo su — git
    echo PATH='${PATH}':/opt/gitolite/bin/ >> .bashrc
    source .bashrc
    which gl-setup

Now copy over your public key to a file called username.pub and finalize gitolite setup by running:

    gl-setup username.pub

After that, follow the instructions, they should be fairly straightforward. Congratulations, you’re done! The gitolite’s configuration is stored in a Git repository under gitolite, so you’d want to clone it from your computer (i.e. where you have the private key for username.pub):

    git clone git@yourservername:gitolite-admin
    cd gitolite-admin
    vim conf/gitolite.conf

The file gitolite.conf is in fact your gitolite config file. After you edit it, save it, commit it and push to the gitolite-admin repository on your server, gitolite checks the config and makes appropriate changes to the repositories. To add users, just place their keys into keydir directory. It’s that simple! You probably would want to read official admin doc just about now.

0 comments »

Your Own Git Hosting: Prologue

As you might know, Git itself does not deal with collaboration, letting you choose any adequate model. So how does one go about publishing his repository? Or letting someone push into their repository? The answers are multiple and confusing. Official Git Manual gives a dry summary of the basic options, which you may read now, or at your leisure. I’ll just note the few key points.

Limiting the discussion to network exchange, Git supports three basic protocols: git protocol, HTTP and SSH. All of them support pulling and pushing, however each of them has drawbacks. Git protocol does not support any kind of authentication, so it is mostly used for read-only public access. HTTP is slow and stupid (which means that it allows only file transfer, so you can’t use Git hooks). SSH is great in all respects, but it implies that every person that has access to your repository has an account on the server, where it is hosted.

In the majority of the cases, two protocols are set up for Git repository: public read-only git protocol access and private read/write SSH access. However I can name you upon request at least one real-world case, when git protocol was successfully used for unauthenticated read/write access :). HTTP protocol is usually used in those morbid cases where you can in no conceivable way get rid of it (imagine a firewall that blocks everything except port 80 or 443).

So how do you setup a Git hosting? There are a lot of web Git hostings (gitorious.org, github.com), and project hostings with Git support (sf.net) out there. Mostly they provide free hosting for open-source projects and charge a fee for private repositories.

However, public Git hostings can not satisfy everyone. There are closed projects, that could not be trusted to a third-party. There are closed projects that do not have spare money for Git hosting. Also, it is hard to integrate hosted Git repository with other development tools (buildbot, bugzilla, etc.), mainly because custom hooks are not supported. So that brings us to the following problem.

Given:

  • a server.

Required:

  • hosting of any number of Git repositories;
  • public read-only access to a subset of them via git protocol;
  • read/write access for authorized users via SSH;
  • web interface to the repositories.

In the following posts, I will tell you how I solved the problem for myself using the following tools:

  • gitolite for SSH access management;
  • git-daemon for anonymous git protocol access;
  • cgit for web interface.

Please stay tuned!

P.S: I’ve heard some concerns regarding cgit not working properly in some cases. I can neither confirm this nor disprove (since I rarely use web frontends for code browsing), but I’ll look into it.

0 comments »

Git Is Your Friend not a Foe Vol. 4: Rebasing

This time I’ll talk about more complex things, which give developers more power over their life. Actually, they just look complex. In fact these are quite natural operations over Git commit history structure, which was described already in my previous posts and gazillions of posts by other people.

So, let’s start with the simplest. Mr. Cleverhead sometimes fails to remember which branch he is currently on. He commits to a branch master, while in fact he should have committed that to staging. What can he do to fix it? He decides to run git show, save the commit as a patch, then checkout branch staging, apply patch there and commit with the same commit message. Well, it happens so that Git already has a command that does exactly this automatically! It is called git cherry-pick. Besides fixing Mr. Cleverhead’s reputation, it is also quite often used for example to backport commits to release branches.

Git cherry-pick

Note, however, that this still requires Mr. Cleverhead to remove his commit from master. We believe in him.

The next big complex thing is a branch-wise cherry-pick. This is called a rebase and often casts a great deal of confusion upon novice Git users. But actually rebase is to cherry-pick as is multiplication to addition: by doing rebase you just cherry-pick a series of commits on another branch. Although git-rebase manual page is ugly and unfriendly, it tells the same basic thing: rebase is a series of cherry-picks, followed by a branch reset.

Git rebase

Note the desaturated old commits. Despite Git changed the branch head, it didn’t remove these old commits. They are still accessible through reflog, or by their SHA1 ids, in case you realise you’ve made a mistake.

There are many use-cases for git rebase, from Mr. Cleverhead committing several commits in a row to a wrong branch, to complex integration and release cycle management workflows. See, for example, http://nvie.com/git-model.

There is also an interactive mode for git rebase. And it is truly awesome! It allows you to edit your branch in any way you want: remove commits, edit commits, squash commits, even change commit order. So try it out. Now.

The post would be incomplete if I didn’t mention git filter-branch. It is basically the same as rebase, but it usually affects the whole branch up to the initial commit and for every commit Git performs a certain action. It is very complex and powerful tool that is quite rarely needed and quite more rarely used. Its uses include: removing a file from the whole project history (for example because of license issues, or because Mr. Cleverhead accidentally added his grandmother’s recipe book five years ago); fixing author’s name in commit messages; creating a separate Git repository from a subdirectory. All this comes free with Git and doesn’t require you to spend weeks doing this any other clumsy way.

Last thing I would like to mention today is pull-rebasing. This refers to the following problem: by default git pull means git fetch followed by git merge, which is perfectly fine if you haven’t committed anything locally. But if you have, this creates a merge commit. This is perfectly fine either. But if you do small commits often, or work on a big feature in your branch and merge master often to be in sync with updates, this will create a so-called “loopy history”, which usually pisses people off. Especially Linus. So if you have committed a small fix that you can’t push because Mrs. Slowrunner managed to push a commit before you, use git pull --rebase. This will rebase your work upon Mrs. Slowrunner’s work and no merge commit will be created. If you work on a topic branch and would like to sync it with master, simply run git rebase master. This will reduce pissed off people count too.

Git loopy and normal history

Note, however, that rebase rewrites history! This means, that if you have published your work somewhere, you shouldn’t rebase it, unless you have warned people that they may expect history rewriting. This is usually refered to as throw-away branches (for example branch pu of git.git). If you have published your work, rewritten it and try to publish it again, git push won’t let you. If you force it to, expect angry people to come to your house with your local analogue of baseball bats.

Regarding angry people: some workflows require topic branches to be squashed to a single commit before merging to mainline. This is easy in Git: just use the --squash option of git merge, and everyone will be happy. If you want to squash only some of them (for example, make 5 commits out of 20), you can use aforementioned interactive rebase.

This pretty much concludes this series of Git posts. If you feel that I have left something unveiled, please tell me that! I appreciate all the comments.

Previous posts:

All posts about Git

3 comments »

Git Is Your Friend not a Foe Vol. 3: Refs and Index

Let’s take a walk along Git repository structure. The central square is Git Object Database. Objects reference each other by 160-bit unique IDs with a certain semantics (for example, commit-object references its parent commit(s) and the tree that corresponds to project’s root directory; tree-object references blob-objects that correspond to file content and tree-objects that correspond to subdirectories; etc., see gittutorial-2(7) for details). For the sake of simplicity, let’s forget about trees and blobs for now, and look at commits only.

Git with only commits

We now have a bunch of commits that know who were their parents. We can trace history from any given commit back to the very beginning. But how do we know what is the current state of things? What was the latest commit in the history? To answer that let’s look at Git refs (short for references). They are basically named references for Git commits. There are two major types of refs: tags and heads. Tags are fixed references that mark a specific point in history, for example v2.6.29. On the contrary, heads are always moved to reflect the current position of project development.

Git with refs

Now we know what is happening in the project. But to know what is happening right here, right now there is a special reference called HEAD. It serves two major purposes: it tells Git which commit to take files from when you checkout and it tells Git where to put new commits when you commit. When you run git checkout ref it points HEAD to the ref you’ve designated and extracts files from it. When you run git commit it creates a new commit object, which becomes a child of current HEAD. Normally HEAD points to one of the heads, so everything works out just fine.

Git with branch HEAD

But if you checkout a specific commit instead of a branch, your HEAD starts pointing at this commit. This is referred to as detached head and you may be told that you are not on a branch (git branch says “(no branch)”). This is perfectly fine, but if you commit anything to it, your commits won’t have a known ref, so if you checkout another branch, you can lose them.

Git with detached HEAD

Having said about committing, can’t help stopping by the process of committing itself. You may already know that the Git’s “add” operation differs from almost every other VCS in that you have to “add” not only files that are not yet known to Git, but also files that you have just modified. This is because Git takes content for next commit not from your working copy, but from a special temporary area, called index. This allows finer control over what is going to be committed. You can not only exclude some files from commit, you can exclude even certain pieces of files from commit (try git add -i). This helps developers stick to atomic commits principle.

And if you have inhuman ability of creating only perfect committs and need stupid VCS only to obey your orders, then you can just use option “-a” for git-commit. And I envy you.

Another special kind of refs are remotes. Whenever you run git fetch, it asks the remote repository, what heads and tags does it have, downloads missing objects (if any) and stores remote refs under refs/remotes prefix. The remote heads are displayed if you run git branch -r.

Some of your branches (notably master) may be what is called tracking branches. That means that a certain branch “tracks” its remote counterpart. Physically that means that when you run git pull on that branch, the corresponding remote branch gets automatically merged into your local branch. Fairly recent versions of Git set up tracking automatically when you checkout a remote branch (for example, git checkout -b stable origin/stable). Note, however, that sometimes it’s better to rebase instead of merge.

But that’s a whole new story…

Previous posts:

Next post:

All posts about Git

9 comments »

Git Is Your Friend not a Foe Vol. 2: Branches

So if you worked with some version control systems for a bit, you’ve probably heard of a concept called branches. It is quite a simple concept: you can perform several development processes in parallel without them interfering with each other. Most projects use branches for experimental features that could set hell loose and for backporting bugfixes to older releases. Subversion and CVS people usually dislike branches, because they involve lots of uninteresting and painful work that they don’t want to do. That is easily explained by the way branches are implemented there.

As you might know, branches in SVN are implemented in a very interesting fashion. They are not, in fact, implemented at all. SVN branch is just a folder, which is created when a branch is started. If you want to merge it back, you need to remember the revision number, when you created the branch, and use that magical number in a complex “svn merge” command. But still, SVN project history remains a straight line.

SVN History

What’s wrong with this way of interpreting the branch concept? Nothing. It’s completely fine, if you don’t want to work with branches. And you should want to work with branches, because they are actually awesome! Especially if implemented as in Git.

Instead of SVN-y linear development history, Git’s commit history is a more complex structure: each commit can have multiple parents and multiple children. In computer science this is called a Directed Acyclic Graph (and if this rings any bells you may want to read Tv’s article “Git for Computer Scientists”). In practice that means that you are not restricted to developing upon the latest revision in project’s history. Instead, you can take any existing commits and start creating commits off it. If you want to merge them, you create a commit that is a child of two commits (such children are called merge commits).

This way you get a graph with several commits with no children (let’s call them branch heads for now). Every commit has a reference to its parent. So if we take the branch head, we can trace back the project history to the very beginning. This is why the Git branch is simply a reference to its head (you can go ahead and look into the files in .git/refs/heads directory of any of your Git repositories).

Git History

Most of the time you will have a checked-out branch (the special reference HEAD points to the current branch, see .git/HEAD for example). When you commit something, your commits are attached to it and the branch reference is moved to the new commit. Simple. But sometimes your HEAD may point to something other than a branch head (for example, when you checkout an older revision by its ID or tag). This is called a detached head. It is a very simple, important and confusing situation. There’s nothing wrong about it, but it hides a peril: if you commit to a detached head, Git creates a new commit and attaches it to the current commit, forming a branch. But this branch has no name! It will just grow sideways as a normal branch without a name. Here’s what’s wrong with it:

  • it is confusing, because commits do not go to master, or whatever branch you had checked out before;
  • if you check out another branch, you won’t be able to return to this branch by its name, it simply doesn’t have any.

Note, that Git won’t let you lose data easily and won’t force you to do unneeded work. Let me tell you what to do in case you have committed to a detached head. Suppose you created just one single commit to a detached head and now just sit and look at it. You have two options:

  • create a new branch with the current commit as a head: git branch branchname,
  • attach the new commit to another branch (suppose it is branch master): remember the ID of the new commit, checkout the required branch (git checkout master), cherry-pick your commit to it (git cherry-pick id).

If you find yourself in the second situation (you’ve just committed to a detached head, checked out another branch and don’t remember the commit id), you may use Git’s reflog (git reflog, or git log -g). This will list the history of your HEAD (checkouts, commits and the such), where you can take commit ID and use it wisely.

Merging is an important part of Git workflow. You will, in fact, do merges frequently even if you don’t use branches other than master, provided you use more than one repository. That is because master of one repository and master of another repository are, in general, different branches. So when you do push or pull to/from another repository you do a merge. Git differentiates two merge types (suppose you attempt to merge branch B into branch A):

  • fast-forward merge. This happens when B is a direct descendant of A. This is resolved trivially: Git simply moves reference A to point to B,
    Git Fast-Forward
  • non fast-forward merge. This covers all the remaining cases, and requires a merge commit to be created (merge commit is a commit with at least two parents).
    Git Fast-Forward

This differentiation is important because the fast-forward merge can be performed automatically without human intervention. That’s why this is the only merge possible during Git push. The non fast-forward merge may result in edit conflicts (the situation when two lines of development changed the same line of the same file differently), so a human intervention may be required. This is what is meant by a (not immediately clear) git-push message “remote rejected: non fast-forward”: sorry, I can’t push your modifications, because remote branch has diverged, please resolve this manually. Most often this occurs when another developer managed to push his changes first. In this case just run “git pull”, resolve conflicts (if any), then run “git push”. Less often this occurs when a remote branch has been changed completely (for example, branch pu of Git Git repository is changed very frequently and is not supposed to be developed upon). This means that either you or the remote repository owner screwed up, so you’d better talk to each other. Sometimes this occurs when you try to push to a completely unrelated repository. So just be careful there.

I should note here, that the “—force” option to git-push along with +refspec notation is not going to solve your problems automagically. It will simply destroy the remote history, replacing with your own. So you should never use it, unless you know exactly what you are doing.

Next up: rebasing and staging area.

Previous post:

Next posts:

All posts about Git

10 comments »

Git Is Your Friend not a Foe Vol. 1: Distributed

Recently, I’ve been preaching Git to everyone that use the inferior version control software (like SVN or, pardon me, CVS). But somewhy the main obstacle I see in these people is that they are so used to SVN workflow that they don’t see the magnificence and flexibility Git offers. They mostly are able to read http://whygitisbetterthanx.com/ and acknowledge the fact that more and more projects have been switching over to it.

But still, many of them don’t grasp the benefits Git gives, falling back to classic centralized edit–commit-to-server workflow of SVN and whining that “this stupid Git didn’t commit changes in that file; this stupid Git complains about ‘non fast-forward’; this stupid Git ate my kittens; etc.”. I would like to clear something out and introduce them to a better world.

First of all, Git is a distributed version control system. What does that mean? In classic VCS you have a single holy place called The Repository, where all the project’s history is kept. Developers get only the small fraction of information from it: the actual files from the latest revision (termed the “working copy”, which is obviously an exaggeration). Basically, the only thing SVN client is able to do is compare your files with the latest revision and send this diff to the server. In SVN communications are possible only between The Repository and the puny client with the working copy.

SVN

In contrast, Git does not differentiate His Holiness The Repository from mere mortal working copies. Everyone gets a repository of his own. Everyone can do anything they want with it. Each developer can communicate with any other developer. This gives a developer so much freedom, that he often does not get into it, and just simply asks this:

Uhm, an entire development history? With every working copy? Man, that will eat a lot of disk space! And I even can’t imagine how long it will take to checkout that repository!

Well, first, not checkout, but clone. The checkout in Git is a somewhat different operation, and that is a Git club entry fee: you need to lose your centralized VCS habits and get used to new terms and ways. This can be painful at first, but it pays off at the end. You’ll thank me later.

So, back to the repository size. Yes, Git requires you to have the whole repository on your person. Yes, it does increase your project directory size. But Git is extremely efficient in packing stuff, so that increase should not hurt you. In fact, the whole Git repository (with full project history) is known to take less space than an SVN checkout. And SVN’s checkout process is so inefficient, that for most projects Git clone takes less time than SVN checkout.

Okay, now the next question is: what is so cool about having the whole repository along with project files? Well, the most basic advantage is that a developer can do everything without access to the server, i.e.:

  • view the revision log starting from the very first commits;
  • browse old versions of the project;
  • and more importantly, commit his changes.

It is a nice feature being able to browse the history without Internet access for people with slow link, or for people that travel a lot. But being able to commit things without asking anyone’s permission is so important that it’s worth a separate paragraph. Here it goes.

Most software teams recognize the two simple principles that a developer should follow: keep commits atomic and don’t commit bad stuff. The problem is that centralized VCS make these principles incompatible. People just don’t work in a linear discrete fashion, instead they tend to steer between several things: a touch there, a refactor here, an occasional stupid bug fix. In the end you get a working tree with bunch of unrelated, uncommitted and untested changes. In Git you can commit as often as you want because commits are local to your repository, no one sees them except yourself! You can commit total rubbish and test everything later — you can edit every single commit without fear of embarrassment and humiliation. You can find out that the way you started to implement this killer feature everyone wanted is totally wrong and start from scratch — without spoiling the project version history.

The second advantage is that developers can exchange their revisions with each other without the central server. Imagine John having reworked the main loop of nuclear reactor coolant control computer. He doesn’t want to incorporate this change to a live system, so he asks Fred to download the respective changes from his repository and test them on his nuclear plant in less populated area. After not having heard any loud explosions, John knows that at least one plant survived the change.

You can also benefit from this even if you are the only developer. Imagine you have several different computers (for example Mac, Linux x86 and Linux amd64). You have developed something on your Mac box and tested it through and are ready to push this to the main repository. But you may also push it first to your Linux boxes and test it there. In SVN you would have to generate patch, transfer it to the boxes, and apply it. Everything manually. So you most probably wouldn’t bother at all and would discover that nasty bug that occurs only on 64-bit computers only in two month and lose your job.

Git

Finally, the concept of “central repository” may be eliminated altogether. Every developer gets a “public” repository where he keeps the stuff he is not ashamed of and a private repository where he works as he wants. Or a bunch of private repositories. The developers exchange their work by pulling commits from each other’s public repositories. Or they can have a single lead developer, who collects the good commits, and use his repository as a “blessed” repository. The lead developer either watches for changes in other public repositories, or waits for a “merge request”. Merge request is a message (e-mail traditionally) that says something along the lines of “Hey, Sam, I’ve implemented the automatic road crosser for blind one-legged homosexuals, ‘git pull git://acmesoftware.com/~dave/shiny.git crosser’, love, Dave”. Sam copies-and-pastes the command and gets a new branch, tests it, and then pushes to his blessed public repository.

For large projects (for example, Linux) lead developer has several people responsible for specific subsystems (the so called Lieutenants). They collect the small commits from their fellow developers, test them and forward to Linus, who aggregates all the good stuff in his own repository. This ensures that the code is seen by at least one other person, before it gets stored in the repository and completely forgotten.

The aforementioned site has a nice section about different Git workflows (see under Any workflow) with pictures.

Also, the nice side-effect of Git being a distributed system is that every repository is essentially a backup of the main repository. It doesn’t mean you should not do backups — you should! — it just means, that in case everything crashes and burns, any developer will provide you with full revision history, not only the recent project files.

There are some more things that confuse novice users, especially branches and staging area. I shall cover them in following posts, stay tuned!

Next posts:

All posts about Git

27 comments »

Басня о Git



Git — это простая, но очень мощная система. Большинство людей пытаются обучить других пользоваться Git демонстрируя пару дюжин команд, а затем восклицая «та-да!». Я считаю, что этот способ несовершенен. После такого можно научиться использовать Git для выполнения простых задач, но команды Git будут всё равно казаться волшебными заклинаниями. Попытки сделать что-нибудь необычное будут ужасными. До тех пор, пока не возникнет понимания концепций, на которых построен Git, ты будешь чувствовать себя чужаком в стране чужой.

Эта басня расскажет о создании системы, похожей на Git, с нуля. Понимание концепций, представленных здесь, будет самым ценным, что ты можешь сделать для подготовки к освоению всей мощи Git. Сами концепции весьма просты, но обеспечивают потрясающее богатство функциональности. Прочитай эту басню до конца, и у тебя не должно остаться проблем с освоением различных команд Git и взятием под контроль той огромной мощи, которую он тебе предоставляет.

Басня


Представь, что у тебя есть компьютер, на котором нет ничего, кроме текстового редактора и нескольких команд для работы с файлами. Теперь представь, что ты решил написать большую программу на этом компьютере. Будучи ответственным разработчиком программного обеспечения, ты решаешь, что тебе нужно придумать какой-нибудь способ отслеживания версий твоей программы, чтобы можно было вернуть код, который ты изменил или удалил. Далее следует история о том, как бы ты мог разработать такую систему контроля версий, и причины тех или иных проектных решений.

Снимки


Альфред — твой друг, который работает в торговом центре в одной из кабинок «Особые моменты». Весь день он фотографирует маленьких детей, неуклюже позирующих на фоне джунглей или океана. Во время одного из ваших постоянных обедов возле киоска с кренделями, Альфред рассказывает тебе историю о женщине по имени Хейзел, которая приводит свою дочь и просит сделать её портрет каждый год в один и тот же день. «Она приносит все её предыдущие фотографии, — говорит тебе Альфред. — Она любит вспоминать, как выглядела её дочь на каждом отдельном этапе, как будто снимки действительно позволяют ей путешествовать назад и вперёд во времени к своим воспоминаниям».

Как обычно происходит в детективах, невинное замечание Альфреда срабатывает как катализатор, и ты осознаёшь идеальное решение твоей проблемы контроля версий. Снимки (snapshots), как точки сохранения в видеоигре, действительно то, что тебе нужно при работе с системой контроля версий. Что, если бы ты мог брать снимки твоего кода в любой момент и восстанавливать их по требованию? Альфред читает выражение осознания, заполнившее твоё лицо, и понимает, что ты сейчас молча уйдёшь домой реализовывать что бы гениального ты там ни придумал благодаря нему. И ты его не разочаровываешь.

Ты начинаешь свой проект в каталоге с именем working. По мере программирования, ты решаешь писать за раз по одной функции. Когда ты заканчиваешь самостоятельную часть функции, ты сохраняешь все файлы и делаешь копию всего рабочего каталога, называя его snapshot-0. После этой операции копирования, ты больше никогда не будешь изменять файлы в этом новом каталоге. После следующего этапа работы, ты снова копируешь, только теперь каталог уже называется snapshot-1, и так далее.

Чтобы было проще вспомнить, какие изменения ты сделал в каждом снимке, ты добавляешь специальный файл по имени message к каждому снимку, который содержит описание работы, которую ты сделал, и дату завершения. Выводя на печать содержимое каждого сообщения, легко найти любое изменение, которое ты сделал в прошлом, в случае необходимости воскресить старый код.

Ветки


Через некоторое время, начинает проявляться что-то похожее на релиз. Долгие ночи за клавиатурой наконец-то выдают snapshot-99, который и станет Релизом Версии 1.0. Без лишней волокиты, снимок упаковывается и раздаётся нетерпеливо ждущим массам. Воодушевлённый прекрасным приёмом твоей программы публикой, ты рвёшься вперёд, стремясь сделать следующую версию ещё более успешной.

Твоя система контроля версий до сих пор была верным помощником. Старые версии кода никуда не исчезают, и легко доступны. Но вскорости после релиза, начинают приходить баг-репорты. Никто не идеален, говоришь ты себе, и snapshot-99 готов к восстановлению для исправления ошибок.

После релиза, ты создал 10 новых снимков. Эта новая работа не должна быть включена в версию 1.0.1, в которой ты собираешься только исправить ошибки. Чтобы решить проблему, ты копируешь snapshot-99 в working, чтобы твой рабочий каталог совпадал с тем, что было в Версии 1.0. Немного строк кода и баг в рабочем каталоге исправлен.

Вот здесь-то проблема и проявляет себя. Система контроля версий хорошо работает при линейной разработке, но впервые тебе понадобилось создать снимок, который не является прямым потомком предыдущего. Если ты создашь snapshot-110 (вспомни, что ты уже сделал 10 снимков после релиза), то ты прервёшь линейный поток и ни для одного снимка не будет известно, что было до него. Очевидно, что тебе нужно нечто более мощное, чем линейная система.

Исследования показали, что даже короткое пребывание на природе может помочь восстановить творческий потенциал мозга. Ты дни напролёт сидел и смотрел на искусственно поляризованный свет твоего монитора. Прогулка по лесу и свежий осенний воздух помогут тебе, и если повезёт, ты даже придумаешь как решить проблему.

Огромные дубы, вдоль которых ты идёшь, всегда тебе нравились. Они величественно и гордо стоят на фоне идеально голубого неба. Половина покрасневших листьев покинула дерево, обнажая сложный узор веток. Зацепившись взглядом за один из тысяч кончиков веток, ты неторопливо пытаешься отследить её до их единого ствола. Эта органически созданная структура может быть сколь угодно сложной, но правила нахождения обратного пути к стволу так просты и идеально подходят для отслеживания множества линий разработки! Оказывается, правду говорят про природу и творчество.

Если рассматривать историю кода как дерево, решение проблемы поиска предков становится тривиальным. Надо только добавить название снимка-отца в файл message, который ты пишешь для каждого снимка. Добавив только один указатель, можно легко и точно отследить историю любого снимка до самого корня.

Имена веток


История твоего кода теперь представляет из себя дерево. Вместо единственного последнего снимка, у тебя теперь два: по одному на каждую ветку. При линейной системе, последовательное нумерование позволяло легко идентифицировать последний снимок. Теперь этой возможности нет.

Создавать новые ветки разработки стало так просто, что ты захочешь пользоваться этим всё время. Ты будешь создавать ветки для исправлений старых релизов, для экспериментов, которые могут быть неудачными; на самом деле, вполне можно создавать новую ветку для каждой функции, которую ты начинаешь разрабатывать!

Но за всё хорошее надо платить. Каждый раз создавая снимок, ты должен помнить, что он становится последним в своей ветке. Без этой информации переключение на новую ветку станет весьма трудоёмким процессом.

Каждый раз создавая новую ветку, ты возможно мысленно назовёшь её как-нибудь. «Это будет Ветка Доработки Версии 1.0,» — возможно скажешь ты. Может быть ты даже станешь называть ветку, в которой раньше велась линейная разработка, основной («master»).

Но подумай об этом ещё. В контексте дерева, что означает назвать ветку? Можно назвать каждый снимок в истории ветки её именем, но это потенциально потребует хранения большого объёма данных. И это всё ещё никак не способствует эффективному поиску последнего снимка ветки.

Минимальные сведения для идентификации ветки — местонахождение последнего её снимка. Если тебе надо узнать список снимков, входящих в ветку, отследить предков последнего снимка не составит труда.

Хранение названий веток тривиально. В файле с именем branches, хранящемся снаружи всех снимков, просто перечисли пары имя/снимок, которые представляют кончики веток. Чтобы переключиться на ветку с данным именем, тебе потребуется всего лишь посмотреть номер соответствующего ей снимка в этом файле.

Поскольку ты хранишь только номер последнего снимка ветки, создание снимка теперь включает дополнительный шаг. Если создаваемый снимок является частью ветки, то нужно обновить файл branches, чтобы имя ветки стало ассоциироваться с новым снимком. Не такая уж и высокая цена за получаемые преимущества.

Теги


Попользовавшись ветками некоторое время, ты замечаешь, что они могут служить двум целям. Во-первых, они могут являться передвигающимися указателями на снимки для отслеживания кончиков веток. Во-вторых, они могут указывать на один снимок и никогда не двигаться.

Первый вариант позволяет отслеживать процесс разработки, такие вещи как «Доработка Релиза». Второй можно использовать для пометки интересных точек истории, например «Версия 1.0» и «Версия 1.0.1».

Смешивание веток обоих типов в одном файле попахивает бардаком. Оба типа веток — указатели на снимки, но одни двигаются, а другие нет. Ради ясности и элегантности, ты решаешь создать другой файл с именем tags, в который и помещаешь указатели второго типа.

Хранение разных типов указателей в отдельных файлах снизит вероятность того, что ты случайно воспользуешься тегом как веткой или наоборот.

Распределённость


Работать в одиночку становится скучно. Разве не лучше было бы пригласить друга работать над твоим проектом? Так вот, тебе повезло. У твоей подруги Зои есть компьютер вроде твоего и она хочет помочь тебе с проектом. Поскольку ты создал такую шикарную систему контроля версий, ты незамедлительно рассказываешь всё о ней Зое и отправляешь ей копию всех своих снимков, веток и тегов, чтобы у неё были все возможности доступа к истории кода.

Тебе нравится, что Зоя в твоей команде, но у неё есть привычка надолго уезжать в дальние края без доступа к интернету. Как только у неё появился исходный код, она села на рейс до Патагонии и пропала без вести на неделю. В это время вы оба усиленно программируете. Когда она наконец возвращается, вы обнаруживаете критический недостаток в вашей системе контроля версий. Из-за того, что вы оба использовали одну и ту же систему нумерования, теперь у вас обоих каталоги с именами snapshot-114, snapshot-115, и так далее, но с разным содержимым!

Хуже того, вы даже не знаете кто в какие снимки вносил изменения. Вместе, вы придумали план решения этих проблем. Во-первых, отныне сообщения в снимках будут содержать имя автора и адрес электронной почты. Во-вторых, снимки больше не будут называться просто числами. Вместо этого, вы будете хешировать содержимое сообщения. Этот хеш будет гарантированно уникальным, так как никакие два сообщения не могут содержать одну и ту же дату, текст, снимок-родитель и имя автора. Чтобы всё точно работало как надо, вы оба согласились использовать алгоритм хеширования SHA1, который берёт содержимое файла и возвращает 40-символьную шестнадцатеричную строку. Вы оба обновляете истории по новой технике, и вместо конфликтующих каталогов snapshot-114, у вас теперь различные каталоги с именами 8ba3441b6b89cad23387ee875f2ae55069291f4b и db9ecb5b5a6294a8733503ab57577db96ff2249e.

С новой схемой именования, тривиальной задачей становится скачать все новые снимки с компьютера Зои и разместить их вместе с твоими существующими снимками. Поскольку каждый снимок указывает на родителя, а идентичные сообщения (а следовательно и одинаковые снимки) имеют идентичные имена несмотря на то, где они были созданы, история кода всё ещё представляет из себя дерево. Только теперь это дерево состоит и из твоих снимков и из снимков Зои.

Этот факт достаточно важен, чтобы его повторить. Снимок идентифицируется хешем SHA1, который уникально его (и его предка) определяет. Эти снимки могут быть созданы и перемещены между компьютерами без потери их индивидуальности и места, где они находятся в дереве истории проекта. Более того, снимками можно делиться, а можно хранить у себя, по желанию. Если у тебя есть какие-то экспериментальные снимки, которые ты не хочешь никому показывать, это легко устроить. Просто не давай их Зое!

Автономность


Привычка Зои постоянно путешествовать приводит к тому, что она проводит несчётные часы в самолётах и кораблях. Большинство мест, которые она посещает, не оборудованы доступом в интернет. В итоге она проводит больше времени оффлайн чем онлайн.

Неудивительно, что Зое так нравится твоя система контроля версий. Все повседневные операции, которые ей нужны, могут быть осуществлены локально. Сетевое соединение ей нужно только тогда, когда она готова поделиться своими снимками с тобой.

Слияние


Перед тем как Зоя отправилась в своё путешествие, ты попросил её начать работать с веткой math и реализовать функцию, генерирующую простые числа. Тем временем, ты тоже разрабатывал в ветке math, только ты писал функцию, генерирующую волшебные числа. После возвращения Зои, перед вами встала задача слияния этих двух отдельных веток разработки в один снимок. Так как вы оба работали над отдельными задачами, слияние осуществляется легко. При написании сообщения для снимка слияния, ты осознал, что этот снимок — особенный. Вместо одного предка, у этого снимка слияния их аж два! Первый предок — это твой последний снимок в ветке math, а второй — последний снимок Зои в её ветке math. Сам снимок слияния не содержит никаких изменений, кроме тех, что нужны для объединения двух различных снимков в один программный код.

После того, как ты закончил слияние, Зоя скачивает все твои снимки, которых нет у неё, что включает и твою разработку в ветке math и твой снимок слияния. После этого, ваши истории становятся совершенно одинаковыми!

Переписывание истории


Как и многие разработчики программного обеспечение, ты стремишься поддерживать чистоту и очень хорошую организацию кода. Это также подразумевает поддержку опрятной истории разработки. Вчера вечером ты пришёл домой, слегка переборщив Гинессом в местном пабе, и начал программировать, создав по пути несколько снимков. Сегодня утром, просматривая код, тебя слегка затрясло. В целом он хороший, но в самом начале ты допустил много ошибок, которые исправил в последующих снимках.

Допустим, ты вёл свою пьяную разработку в ветке drunk и сделал три снимка с тех пор, как вернулся из бара. Если название drunk указывает на последний снимок в этой ветке, то ты можешь воспользоваться удобным обозначением предка этого снимка. Запись drunk^ означает предка снимка, на который указывает имя ветки drunk. Аналогично, drunk^^ означает дедушку снимка drunk. Таким образом, три снимка в хронологическом порядке выглядят так: drunk^^, drunk^ и drunk.

На самом деле, ты бы хотел видеть вместо этих трёх плохих снимков два хороших. В одном изменяется существующая функция, а в другом добавляется новый файл. Чтобы добиться этого, ты копируешь drunk в working и удаляешь файл, который был создан в последнем снимке. Теперь в working находится правильные изменения в существующей функции. Ты делаешь новый снимок каталога working и пишешь соответствующее сообщение. В качестве предка ты указываешь хеш SHA1 снимка drunk^^^, по сути создав новую ветку с того же снимка, что и вчера вечером. Теперь ты копируешь drunk в working и создаёшь снимок с новым файлом. В качестве предка ты указываешь снимок, созданный на предыдущем шаге.

В последнюю очередь, ты изменяешь указатель ветки drunk на последний созданный снимок.

История ветки drunk теперь представляет улучшенную версию того, что ты сделал вчера. Те три снимка, которые ты заменил, больше не нужны, так что ты можешь удалить их или оставить в назидание потомкам. На них не указывает ни одно имя ветки, поэтому их потом будет трудно найти, но если ты их не удалишь, они будут просто там торчать.

Временная область


Как бы ты ни пытался делать модификации, относящиеся только к одной функции или логическому участку, иногда тебя заносит и ты начинаешь делать что-то совершенно не относящееся к текущей работе. Только наполовину закончив, ты осознаёшь, что твой рабочий каталог теперь содержит то, что на самом деле должно быть двумя отдельными снимками.

Помочь тебе в этой раздражающей ситуации призвана концепция временной области (staging area). Она служит промежуточным шагом между твоим рабочим каталогом и окончательным снимком. Каждый раз, когда ты заканчиваешь снимок, ты копируешь его в каталог staging. Теперь, как только ты заканчиваешь редактировать файл, создаёшь новый файл или удаляешь старый в рабочем каталоге, ты можешь решить, должно ли это изменение войти в следующий снимок. Если должно, ты делаешь такое же изменение в папке staging. Если нет, ты можешь оставить его в working и сделать частью следующего снимка. С этих пор, снимки создаются прямо с каталога staging.

Это разделение процессов программирования и подготовки временной области упрощает разделение того, что должно, и того, что не должно войти в следующий снимок. Тебе больше не надо сильно волноваться по поводу случайного, не связанного с текущей работой, изменения в рабочем каталоге.

Однако надо соблюдать некоторую осторожность. Возьмём, к примеру, файл README. Ты вносишь изменение в этот файл, а затем такое же в staging. Затем продолжаешь делать своё дело, редактировать другие файлы. Через некоторое время ты вносишь ещё одно изменение в README. Теперь у тебя в нём два изменения, но только одно из них отражено во временной области! Если ты создашь снимок сейчас, второго изменения там не будет.

Мораль такова: каждое новое изменение должно быть добавлено во временную область, если оно должно стать частью следующего снимка.

Диффы


Имея рабочий каталог, временную область и кучу снимков, становится сложно понять, какая конкретно разница в коде между этими каталогами. Сообщение в снимке даёт только общее описание изменений, а не конкретные изменённые строки файлов.

Используя алгоритм сравнения файлов, ты можешь создать небольшую программку, которая будет показывать разницу между двумя каталогами с кодом. По мере разработки и копирования изменений из рабочего каталога во временную область, ты захочешь узнать, чем они отличаются, чтобы определить, что ещё необходимо поместить во временную область. Также важно знать, в чём разница между временной областью и последним снимком, поскольку именно эти изменения и составят следующий снимок.

Есть множество других диффов (diff, результат сравнения текстовых файлов), которые могут быть интересны. Разница между конкретным снимком и его предком покажет набор изменений (changeset), который привнёс этот снимок. Дифф между двумя ветками поможет выяснить, как далеко разошлись две ветки разработки.

Исключение избыточности


После ещё нескольких путешествий по Намибии, Стамбулу и Галапагосу, Зоя начала жаловаться, что её жёсткий диск заполняется сотнями практически одинаковых копий программы. У тебя тоже уже возникло ощущение избыточности дублирующихся файлов. После недолгих раздумий, ты придумал кое-что очень умное.

Ты помнишь, что хеш SHA1 выдаёт короткую строку, уникальную для заданного содержимого файла. С самого первого снимка в истории проекта ты начинаешь процедуру преобразования. Для начала, ты создаёшь каталог с именем objects снаружи каталогов с кодом. Далее, ты находишь наиболее глубоко вложенный каталог в снимке. Также, ты открываешь временный файл на запись. Для каждого файла в этом каталоге ты выполняешь три действия. Первое: вычисляешь хеш SHA1 его содержимого. Второе: добавляешь запись во временный файл со словом ‘blob’ (binary large object, большой двоичный объект), хешем SHA1 из первого действия и именем файла. Третье: копируешь файл в каталог objects и переименовываешь его в тот же хеш. Как только ты заканчиваешь со всеми файлами, вычисляешь SHA1 временного файла, сохраняешь его также в каталоге objects с именем, совпадающим с хешем.

Если при записи файла в каталог objects, там уже содержится файл с тем же именем, то это значит, что ты уже сохранил содержимое этого файла и нет нужды это делать снова.

Теперь перейди в каталог выше и начни заново. Только на этот раз, когда дойдёшь до каталога, который только что обработал, пиши слово ‘tree’, хеш SHA1 временного файла от предыдущего каталога и имя каталога в новый временный файл. Таким образом ты можешь построить дерево файлов-объектов, соответствующих каталогам. Эти файлы будут содержать хеши SHA1 и имена файлов и вложенных каталогов, которые находятся в соответствующих каталогах.

Как только эта процедура будет завершена для каждого каталога и файла в снимке, у тебя будет единственный файл-объект для корневого каталога и соответствующий ему SHA1. Поскольку ничто не содержит корневой каталог, ты должен записать его хеш куда-нибудь. Идеальным местом для этого будет файл с сообщением из снимка. Таким образом, уникальность хеша SHA1 сообщения также будет зависеть от всего содержимого снимка, и ты можешь гарантировать с абсолютной уверенностью, что снимки с одинаковыми хешами сообщений содержат одни и те же файлы!

Также удобно создать объект из сообщения снимка, как ты делал для блобов и деревьев. Поскольку ты поддерживаешь список имён веток и тегов, которые указывают на хеши сообщений, тебе не придётся беспокоиться о потере снимков, которые важны для тебя.

Со всей этой информацией в каталоге objects, ты можешь без проблем удалить каталог снимка, который ты преобразовал. Если тебе понадобится восстановить снимок, тебе просто придётся взять SHA1 корневого дерева, записанный в сообщении, и восстановить из каждого дерева и блоба соответствующие подкаталоги и файлы.

Для одного снимка это преобразование ничего особенного тебе не дало. Ты просто превратил одну файловую систему в другую и создал себе дополнительные трудности. Но настоящая выгода от этой системы проявляется при повторном использовании деревьев и блобов от разных снимков. Представь себе два последовательных снимка, которые отличаются лишь одним файлом в корневом каталоге. Если оба снимка содержат 10 каталогов и 100 файлов, процесс преобразования создаст 10 деревьев и 100 блобов для первого снимка, но только одно дерево и один блоб для второго!

Сжатие блобов


Избавившись от дублирования блобов и деревьев, ты существенно снизил общий размер истории твоего проекта. Но это не всё, что ты можешь сделать для экономии места. Исходный код — это просто текст. А текст можно очень эффективно сжимать, используя алгоритм сжатия вроде LZW и DEFLATE. Если ты сожмёшь каждый блоб перед вычислением его SHA1 и сохранением на диск, снижение размера истории твоего проекта снова окажется впечатляющим.

Настоящий Git


Система контроля версий, которую ты построил, теперь в достаточной степени похожа на Git. Основное отличие в том, что Git предоставляет очень хорошие утилиты командной строки для осуществления таких вещей как создание новых снимков и переключение на старые (Git использует термин «коммит» (commit) вместо «снимок»), отслеживание истории, поддержка кончиков веток, загрузка новых изменений у других людей, сливание и сравнение веток, и сотни других распространённых (и не очень) задач.

Продолжая изучать Git, помни эту басню. Git действительно очень простой по сути, и именно эта простота делает его таким гибким и мощным. Ещё одна вещь напоследок, пока ты не побежал изучать все команды Git: помни, что практически невозможно потерять работу, которая была закоммичена. Даже когда ты удаляешь ветку, на самом деле исчезает только указатель на коммит. Все снимки всё ещё хранятся в каталоге объектов, достаточно только найти хеш SHA1 коммита. В таких случаях, попробуй git reflog. Он содержит историю того, на что указывала каждая ветка, и в трудную минуту, он тебя выручит.

Вот некоторые места, где можно продолжить изучение. А теперь беги и становись мастером Git!



Creative Commons License 2 comments »

Git and Kompare

Everyone knows, that Git is the best version control system, and KDE is the best desktop environment out there.

So recently I had a nice idea: wouldn’t it be nice if git diffs could be viewed by KDE file comparison and diff viewer tool called Kompare? Sure it would.

Still don’t know if it possible by editing Git configuration only, but I did a little bash function that does it (put in your .bashrc):

kg(){
  file=`mktemp`
  git “$@” > $file
  kompare $file
  rm $file
}

Use as follows: kg show -v, or kg diff HEAD^.

1 comment »