Pull to refresh

Как я использую git

Reading time 6 min
Views 90K

Intro


Основам git мне пришлось научиться на своем первом месте работы (около трех лет назад).
С тех пор я считал, что для полноценной работы нужно запомнить всего-лишь несколько команд:


  • git add <path>
  • git commit
  • git checkout <path/branch>
  • git checkout -b <new branch>

И дополнительно:


  • git push/pull
  • git merge <branch>
  • git rebase master (а что, можно еще и на другие ветки ребейзить? О_о)

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


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


Некоторые настройки для удобной работы


Автодополнение


Удивительно, но не у всех оно есть. Отправляемся в гугл по запросу "git_completion", скачиваем скрипт и действуем по инструкции к нему.


Выводим текущую ветку в строке bash


Данный код нужно добавить в .bashrc. Он со мной с некоторыми изменениями путешествует еще с того самого первого места работы.


function git-current-branch {
    git branch --no-color 2> /dev/null | grep \* | colrm 1 2
}

function set_prompt_line {
    local        BLUE="\[\033[0;34m\]"

    # OPTIONAL - if you want to use any of these other colors:
    local         RED="\[\033[0;31m\]"
    local   LIGHT_RED="\[\033[1;31m\]"
    local       GREEN="\[\033[0;32m\]"
    local LIGHT_GREEN="\[\033[1;32m\]"
    local       WHITE="\[\033[1;37m\]"
    local  LIGHT_GRAY="\[\033[0;37m\]"
    # END OPTIONAL
    local     DEFAULT="\[\033[0m\]"
    export PS1="$BLUE\w $LIGHT_RED[\$(git-current-branch)]$DEFAULT \$ "
}

set_prompt_line

Для справки: за внешний вид командной строки баша отвечает переменная PS1. Have fun.


Алиасы


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


#
# Git
#
alias current-branch='git-current-branch'
alias git-uncommit='git reset --soft $(git log --format=%H -2 | tail -1)'
alias gst='git status'
alias glog='git log'
alias gcheck='git checkout'
alias gamend='git commit --amend'
__git_complete gcheck _git_checkout
alias gcom='git commit'
__git_complete gcom _git_commit
alias gdiff='git diff'
__git_complete gdiff _git_diff
alias gadd='git add'
__git_complete gadd _git_add

Обратите внимание на __git_complete <something> <another>. Эта команда включает гитовое автодополнение для алиаса.


Редактируем сообщения к коммитам в своем любимом текстовом редакторе


Для начала небольшая страшилка, основанная на реальных событиях:


Как-то раз молодой неопытный программист хотел впервые закоммитить код, а гит открыл ему vim!

Да, история произошла со мной. Через несколько часов я смог его закрыть и начал коммитить только с однострочными комментариями через git commit -m.


Git, как и некоторые другие утилиты (crontab, например) проверяют наличие переменной EDITOR.


В конфиге баша (~/.bashrc) можно добавить вот такую строчку:


export EDITOR=<команда, открывающая ваш текстовый редактор>

У меня это emacsclient, раньше был subl (Sublime Text). Я не проверял, но я полагаю, что очень важно, чтобы команда не возвращала управление терминалу, пока текстовый файл не будет закрыт.


Сменить ветку, не теряя текущих незакоммиченных правок


Иногда можно просто сменить ветку, но иногда возникают конфликты. Я знаю два варианта:


1) Сделать временный коммит
2) git stash, сменить ветку, ..., вернуть ветку, git stash pop


Первый вариант надежнее, второй удобнее (имхо).


Посмотреть, что я уже наредактировал


git diff

Показывает ваши изменения относительно текущего коммита + stage (важное уточнение). Замечание: в дифф не попадают новые файлы


Посмотреть, что я добавил в stage


git diff --cached

Замечание: сюда новые файлы попадают.


Удалить лишние файлы


Т.е. файлы, которые не относятся к репозиторию


git clean -df

  • -d — удаляет еще и директории
  • -f — обязательная опция, без нее гит попросту откажется что-либо удалять (уж не знаю, зачем она)

Отменить последний коммит


У меня на это дело есть alias в баше:


alias git-uncommit='git reset --soft $(git log --format=%H -2 | tail -1)'

git reset --soft <commit/branch/tag> переносит ветку на коммит, но код не меняет. Разница заносится в stage.


$(<whatever>) — баш выполняет содержимое скобочек и подставляет результат выполнения вместо всего выражения. Например, cat $(ls | tail -1) выдаст содержимое последнего файла из ls.


git log --format=%H -2 выдаст хеши двух последних коммитов.


В общем, вся команда сводится к тому, что текущая ветка переносится на один коммит назад, а изменения, внесенные коммитом, попадают в stage


upd. Как многие справедливо заметили, того же результата можно добиться намного проще: git reset --soft HEAD~ — данная команда берет предыдущий коммит от "головы" и "ресетит" гит на него. В свое оправдание могу сказать, что этому алиасу пара лет и в то время я не знал о том, что такое HEAD и тем более HEAD~


Объединить несколько коммитов


Когда я работаю на своей ветке, периодически я делаю несколько коммитов, которые совсем не имеют смысла по отдельности (а делаю я это просто для того, чтобы коммитить почаще и не терять мысль), поэтому перед вливанием их в мастер имеет смысл их объединить


Решение:


Интерактивный rebase!


git rebase -i master

Это откроет текстовый редактор, в котором списком будут указаны коммиты.
Вы можете:


  • Менять порядок их применения (очень часто пригождается)
  • "Сквошить" — объединять несколько коммитов в один
  • редактировать — гит будет останавливаться, чтобы вы могли делать изменения с помощью --amend
  • менять сообщение — в общем-то, частный случай редактирования
  • не применять коммит в принципе

В данной ситуации нужно взять нужные коммиты, расставить их друг за другом и всем, кроме первого, поставить пометку squash.


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


Добавить что-нибудь в предыдущий коммит


git add <forgotten changes>
git commit --amend

Еще стоит упомянуть:


git commit --amend --no-edit # Не редактировать сообщение
git commit --amend -m 'my commit message' # работает так, как вы ожидаете 

Добавить изменения в старый коммит (когда для --amend уже поздно)


Ситуация:


3 коммита назад допустил опечатку, не хочу, чтобы это позорище кто-то увидел отдельным коммитом.


Решение:


Интерактивный rebase!


git rebase -i HEAD~3

Лично я в указанной ситуации (а у меня она часто возникает) делаю так: создаю коммит, где в сообщении добавляю префикс[to_squash], заканчиваю работу над веткой, делаю полный ребейз ветки на мастер (git rebase -i master) и переношу этот коммит под тот, к которому данная правка относится, с пометкой s (squash).


Закоммитить части файла по отдельности


Коммиты желательно делать максимально простыми (антоним слову "сложными"). Хочу вот я на гитхабе посмотреть, какая история у файла hello_world.rb, смотрю историю, а там среди прочих коммит "create super-booper feature", в котором в файле hello_world.rb у одной переменной изменено имя, хотя она к фиче совсем отношения не имеет. Лучше было бы наличие коммита "rename variable x to y in hello_world.rb".


Собственно, например, у меня есть код:


def kvadrat(x)
  x * x
end

puts kvadrat(n)

Мне нужно добавить фичу: выводить удвоенное n. Изи! Но пока я пишу фичу, на автомате меняю некрасивое имя функции.


Пишем:


def square(x)
  x * x
end

def double(x)
  x + x
end

puts square(n)
puts double(n)

Как теперь коммитить? Можно быстро вернуть старое название, закоммитить новый функционал, а потом уже переименовать, но это не всегда уместно, т.к. изменения могут быть достаточно крупными. Можно честно признать, что коммит сложный и написать сообщение в духе "добавил фичу + переименовал метод", но мы ведь стараемся делать коммиты простыми, верно?


Но у гита есть отличная команда:


git add -p

Она интерактивная. Поочередно берет изменения кусками (hunk) и спрашивает, что с данным куском делать: игнорировать, добавить, изменить и добавить. Третий вариант достаточно мощный, можно по отдельности добавлять изменения даже в рамках одной строчки (kvadrat(x) + kub(x) => square(x) + cube(x) в 2 коммита).


Я не буду приводить пример, просто зайдите в любой ваш проект с гитом, отредактируйте пару файлов в разных местах и введите эту команду. Иногда лучше один раз попробовать, чем сто раз услышать (при работе команды можно ввести ? для краткой справки)


Заслуживают внимания


  • git reflog — меня это спасло, когда я случайно удалил ветку, не смерджив и не запушив ее
  • git rebase -i — в посте указан лишь частный случай применения.
  • git log --graph — просто он забавный. Не знаю, есть ли практическое применение.
  • git cherry-pick <commit> — пытается применить изменения коммита к текущему
  • Дополните?

Outro


Я указал здесь всего-лишь парочку "трюков" работы с git, но их я использую на ежедневной основе.


Смысл данного поста (помимо того, чтобы ублажить свое ЧСВ и оставить заметку для себя самого) в том, чтобы еще раз подчеркнуть известную (относительно) фразу: Know your tools!.


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

Tags:
Hubs:
+95
Comments 195
Comments Comments 195

Articles