Git — начинаем работать :)

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

Этот пост предназначен для тех, кто приступает только работать на Git. Все нижесказанное будет предназначено для Unix окружения и оболочки Bash.

  1. Заходим в Unix, где есть bash & git
  2. Для начала работ с git отконфигурируем его глобальные настройки для того пользователя, под которым мы будем работать, то есть под себя:
    git config --global user.name "Your nickname/name"
    git config --global user.email "your@email.com"
    git config --global i18n.commitEncoding utf8
    git config --global status.showUntrackedFiles all
    

    Здесь по строкам: 1 — указываем своё имя, будет видно в логах истории (git log); 2 — ваш email, будет видно там же; 3 — вы указываете, в какой кодировке будете писать комментарии к коммитам (commits), рекомендую UTF-8, а значит, надо, чтобы ваш терминал с Unix был в той же кодировке (если всё сделаете правильно — с русскими буквами проблем не будет, даже если кто-то будет писать commit в windows-1251 кодировке — git умеет конвертировать их прозрачно); 4 — весьма полезная опция — рекомендую поставить (см. последний пример) 😉

  3. Создание репозитария («репо», repo). Можно взять готовые исходники, которые вы хотите начать отслеживать git-ом, тогда:
    cd корень_исходников
    git init
    # теперь появится папка .git/
    

    Либо, можете склонировать чей то проект для дальнейших правок, например так:

    cd папка_где_будет_папка_исходников
    git clone sshuser@ssh.host.com:where/git/repo/project.git
    # Теперь будет папка project
    
  4. Если вы хотите хранить репозитарий так, чтобы вы или кто-то другой могли его клонировать и закачивать туда, можете поступать так:
    • Если вы проект откуда то уже до этого клонировали, то «закачка» обратно делается просто:
      git push

      Правда, одно условие — оригинальное место, откуда проект скачивали, должно быть read-write доступа. Обычно, общедоступные проекты лежат в read-only доступе.

    • Если вы проект только создали, то вам надо сделать:
      1. Создать место, где будет храниться. Самый простой и надежный способ — создание отдельного Unix аккаунта для этого:
        useradd mygit # Linux вариант
      2. Очень рекомендую положить ваши ssh ключи в ~/.ssh/authorized_keys, чтобы не вводить каждый раз пароль при запуске на удаленной машине:
        man sshd # смотрим раздел "AUTHORIZED_KEYS FILE FORMAT"
      3. Из вашего репозитария, что вы проинициализировали git init сделать bare репозитарий — репозитарий, где нет будет рабочего дерева директорий с исходниками, а будет лишь база git со всеми версиями:
        cd our_project_before_git
        git init # Теперь появилась .git папка с базой, но репозитарий не bare - не годится для всех
        git clone --bare . ../our_project.git # Теперь выше мы создаем bare директорию
        cd ..
        scp -r our_project.git mygit@host.com:our_project.git # закачиваем на хост, откуда потом мы и другие будут забирать его
        
      4. Теперь на удаленном хосте желательно аккаунт mygit ограничить доступом только по git:
        echo /usr/bin/git-shell >>/etc/shells
        # Уточните точный путь до git-shell
        vipw
        # там меняем shell у пользователя mygit на:
        # /usr/bin/git-shell
        # Можно указать shell сразу при создании пользователя через <useradd>
      5. Теперь, чтобы ваш первоначальный репозитарий был закреплен за тем хостом + путь (mygit@host.com:our_project.git) у вас есть два пути:
        1. Либо добавить хост и путь репо как remote origin командой:
          git remote add origin mygit@host.com:our_project.git
          # более подробно - смотрите man git-remote
        2. Либо склонировать из удаленного залитого репозитария в новую директорию, а старую можете удалить (первый способ, наверное, лучше)
          mygit@host.com:our_project.git

Теперь, пройдемся в кратце еще раз по концепции git, о которой я рассказывал ранее:

Git хранит в своём репозитарии, который хранится в папке .git внутри корня вашего проекта, все файлы всех версий файлов. Точнее, их упакованные версии. Последние версии git иногда хранят дельты, но это редко. Засчет этого, git очень быстро работает, так как оперирует сразу полными версиями файлов, основываясь на иерархии commit-ов. Каждый commit — хеш SHA1 таких данных как: хеши всех файлов рабочей директории (точнее того, что добавлено в «индекс» при commit) и хешей parent-ов. То есть, не может быть одного и того же номера commit в мире, но с другим сожержимым, кроме вашего проекта именно с теми файлами и именно с тем же положением в иерархии commit-ов. Именно благодаря такой простой и понятной структуре, разработчики ядря Linux смогли проверить исходники ядра после взлома kernel.org — самого главного сайта в публикации исходников новых ядер Linux. Если бы кто-то внес изменения, изменились бы SHA1 контрольные суммы с той точки, где было вмешательство, и номера коммитов были бы другими. А поскольку репозитарии в git каждый раз копируются к каждому, кто клонирует, то и проверить на вмешательство можно было простым сравнением номеров коммитов. Даже достаточно сравнить номер самого последнего коммита из заведомо проверенных репозитариев, чтобы убедиться, что вмешательства не было (если вставить что-то со стороны, проверка моментально обнаружит несоответствие хешей и репозитарий будет просто «битым»). Это было отступление. Продолжаем далее 🙂

Представьте себе иерархическое дерево, каждый узел которого может иметь одного и более «родителей» (parents), где есть «конечные» узлы, которые не являются родителем для какого либо узла — это «концы» ветки. Ветка в git — не что иное, как указатель на этот самый последний commit, которым все заканчивается. Когда вы делаете очередной commit, указатель ветки переназначается в новый последний commit, а предыдущий становится «родительским» для вновь созданного. Git, оперируя названиями веток, берет всегда от туда «конечный» commit, и далее по parents идет вверх по иерархии. У git есть очень гибкий синтаксис указания списка commit-ов через ветки и иерархические статусы узлов (parents, «предков»), а также через временным интервалы (например, что было сделано за последнюю неделю, например). Имена веток могут содержать ‘/’ символ. Например: git diff feature/foo..master покажет изменения между веткой feature/foo и master, а если быть более точным — покажет изменения в commit-ах, которые недостежими в обоих ветках (то есть, если идти по иерархии от концов обеих веток вверх, то учитывать только те коммиты, которые есть только в одной их них. Если мы доходим до «резвилки», то послее нее, разумеется, коммиты есть в обоих вариантах, и тогда поиск прекращается). По умолчанию принято, что default ветка в git имеет имя master. Часто, разработчики под ней выпускают релизные версии.

Но устройство git такого, что при работе с ним вам надо очень активно использовать ветвление — т.е. создавать свои ветки для новых «фич», тестировать программу, и если все нормально — «сливать» с главной веткой. Это позволяет четко вести разрботку и разграничивать правки кода. Причем, создание веток вами идет локально — вы можете им давать любые имена, не опасаясь, что они пересекутся по названию с ветками других разрботчиков проекта — по умолчанию, новые имена веток хранятся в локальной копии репозитария и при «push» операциях это не затрагивается. Вы легко можете закачать новую ветку в удаленный репозитарий:

git push origin my_new_branch:my_new_branch

Либо даже удалить в удаленном репозитарии какую либо ветку:

git push origin :my_new_branch

Немного по этим двум примерам: синтаксис закачки/выкачки имеет формат src:dst, где src — имя ветки на source, dst — имя ветки на destination. То есть, если мы делаем push, то src — это имя ветки в нащем репозитарии, а dst — имя ветки в удаленном, а если мы делает :my_new_branch, то src — «пусто», т.е. нет, а my_new_branch — имя удаленной ветки — то есть толкнуть «нечто пустое» куда-то — это значит «удалить».

В git есть промежуточный слой между репозитарием и вашей рабочей директорией — индекс (index или stage). Файлы, добавленные в индекс, в документации называются staged. Это поначалу может показаться непривычно, но потом это становится понятным. Это как бы «фильтр», «прослойка», между вашим проектом и репозитарием. Ведь проекты, почти всегда — это не просто исходними, но и служебные файлы, которые создаются при компиляции, тестировании и т.п.. Чтобы не тащить с собой в репозитарий весь «этот мусор», который создается в директории проекта, и индивидуален для каждого разработчика проекта, git имеет этот index. Index — это тоже дерево файлов и директорий, но «очищенное», и содержит в себе только то, что будет добавлено при очередном commit. Если git status показывает какой либо файл как untracked — значит того файла нет в индексе, и пока вы его специально не добавите git add, он не попадет в репозитарий. Для удобства можно делать git add . из корня проекта — добавить все, что не отслеживается, но тогда надо активно следить за файлами .gitignore!

Возьмите себе за правило активно использовать файл .gitignore (см. man gitignore). Этот файл указывает git-у, какие файлы надо исключать при работе и не добавлять в индекс! Эти файлы могут присутствовать в любой поддиректории проекта, и их действие распространяется либо на поддиректории той, где есть этот файл, либо только на файлы в этой — в зависимости, как описана маска файла. Например:

# Это комментарий

# Исключать только Makefile.old из текущей директории!
/Makefile.old

# Исключать все файлы с расширением .o как в этой, так и в поддиректориях
*.o

# Исключать все txt файлы только в этой директории
/*.txt

# Исключать всю директорию build в текущей папке
/build/

#  Исключать все директории в этой и ниже с именем blib, файлы могут быть с таким именем
blib/

И еще парочка заметок. Git не хранит права файлов в репозитарии, за исключением ‘x’ флага, и при клонировании все файлы имеют права либо 644, либо 755. Поэтому не делайте репозитариев, с расчетом на отдельные права некоторых файлов (например, права 600 или 700). Сначала это может кому-то не понравиться, потом привыкаешь. Для разработки проектов этого достаточно, так как все проекты по правильному делаются с двумя фазами — компиляция и инсталирование. Правильные права должны выставляться на последнем этапе дистрибутива. Также, любой клонированный репозитарий может быть репозитарием для другого, если вы это позволите — получается очень распределенная, несокрушимая система 🙂 К тому же, работа над каждым репозитарием не требует интернета — вы можете делать commit-ы в репозитарий на своем диске (вместо ssh хоста в моих примерах можно использовать обычные Unix пути), а если захотите, то при наличии интернета уже «сливать» коммиты в другой remote репозитарий (см. man git-remote, man git-push)

Вернемся к примерам в начале статтьи. Теперь мы имеем репозитарии, с которыми можно работать. Теперь несколько советов по работе

  1. Рекомендую прописать у себя в ~/.bash_profile такие команды, которые использую на основании своего опыта:
    alias gitcommit='git commit -a'
    alias gitdiff='git diff|less -S'
    alias gitgraph='git log --graph --all --decorate|less -S'
    alias gitremote="git remote show origin"
    alias gitlog='git log --decorate'
    export LESS=S#4
    

    Теперь пояснения:
    gitcommit — укороченная команда, когда вы делаете commit (принятие изменений, их фиксация в базе), так, чтобы перед commit были обновлены файлы из рабочего дерева в индекс, а также, чтобы те файлы, которые вы удалили, были также удалены в commit. При такой команде не попадают в commit те файлы, которые есть в рабочем дереве, но нет в индексе (из надо добавлять git add)
    gitdiff — просмотр изменений между рабочим деревом и индексом, но запускает less с опцией -S
    gitgraph — очень полезная командочка для просмотра «дерева» commit-ов и веток
    gitremote — смотрит в удаленный репозитарий origin (может быть несколько репозитариев в git — я сделал для широкоупотребимой ситуации) и показывает статус вашего репозитария в сравнении с удаленным
    gitlog — почти то же, что gitlog, но вместе с commit хешем показывается, если есть, имя версии ветки, которая заканчивается этим commit
    export LESS … — устанавливаем переменную, чтобы команда less работала наиболее удобным способом (см. man less)

  2. Как правило, работа сводится к такому примерно списку команд:
    cd my_project
    # например, текущая ветка - master
    git pull # вытащить, если есть, что-то новое из репозитария из ветки master
    
    # не обязательно, но если вы работает в команде, лучше, чтобы ваша новая "фича" велась в отдельной ветке до ее окончания
    git checkout -b new_branch
    # теперь мы в new_branch
    # редактируем как надо что либо
    
    # Проверяем, что мы наделали, смотрим, появились ли новые файлы, которых нет в репозитарии (untracked)
    # если есть untracked - думаем, либо добавлять их в репо,
    # либо добавить в .gitignore, чтобы не маячили в глазах
    # здесь нам пригодится настройка git config --global status.showUntrackedFiles all (см. начало статьи)
    # без этой настройки новые целые директории не показываются пофайлово, а только как имя директории
    git status
    
    # опционально, если есть untracked файлы - можем добавить их, либо сразу директорию - как здесь - всё под '.'
    git add .
    
    # Если порядок наведен, тогда делаем commit:
    # если есть alias из моих "команд", тогда:
    gitcommit
    # либо на "голом" git:
    git commit -a
    
    # тестируем, проверяем, что наваяли, и если все OK, тогда...
    
    # закидываем изменения либо нашей ветки "новой" с созданием её на origin, что нежелательно, как правило:
    git push origin new_branch:new_branch
    
    # либо закидываем её также, но и говорим своему репозитарию следить за веткой, когда git pull
    git push origin -u new_branch:new_branch
    
    # либо в master, не переключаясь с нашей new_branch, что нежелательно
    git push origin new_branch:master
    
    # А лучше сделать так:
    # переключаемся обратно на master
    git checkout master
    # На всякий случаем тащим что-то новенькое - вдруг кто-то закинул новое, пока мы работали над new_branch
    git pull
    # Теперь объединяем изменения в new_branch с master, могут быть конфликты, если при последнем pull мы что-то получили новое
    git merge new_branch
    
    # Либо три вышеуказанные команды можно сделать другим способом:
    # Делаем push на локальный наш репозитарий, но только если его тип Fast Forward (то есть без merge)
    git push . HEAD:master
    # Если git не сругнулся - всё OK, продолжаем...
    # переключаемся обратно на master
    git checkout master
    # Теперь при переключении у нас реально файлы не менялись, как в предыдущем примере - иногда удобно
    # если открыт редактор, да и для make хорошо
    # На всякий случай тащим что-то новенькое - вдруг кто-то закинул новое, пока мы работали над new_branch
    git pull
    # При этом уже git merge делать не надо - git pull уже сделал и слил ветки с нашими изменениями
    # А теперь уже продолжаем далее:
    # если конфликты - смотрим git diff, правим file, помечаем как resolved (git add file), делаем git commit -a и т.п..
    # Заливаем в репозитарий master уже с нашими изменениями
    git push
    

Пока всё. Думаю, этого вполне достаточно, чтобы стартануть для работы с git

P.S. Также можете почитать:

  1. Книга «Pro Git» на русском, в переводе (каждый может помогать в переводе через ее git репозитарий ;-))
  2. Сравнение Git с SVN от Ruslan Brest