Пользователь
0,0
рейтинг
2 ноября 2012 в 17:39

Разработка → Машина времени в git из песочницы

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

В этой статье я хочу рассказать о командах git checkout и git reset с ключами --soft и --hard.

Итак, начнём краткий ликбез по машине времени, предоставляемой git'ом. Сперва проиллюстрируем историю:



Здесь кружочками обозначены коммиты. Чем правее коммит, тем он новее. Коммит с хэшем 6e04e..-это самый первый коммит. Одно из основных понятий, которое стоит уяснить себе новичку, — это указатели на коммиты, а точнее некоторое «прозвище» того или иного коммита. Их тьма тьмущая, например: HEAD, master, FETCH_HEAD, ORIG_HEAD и т.д. Это я перечислил крупицу стандартных прозвищ. Их можно создавать и самим, но об этом впереди.

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



Указатель HEAD в нашем случае указывает на master, а master — на коммит d79fb… Архиважно понять, что текущее состояние неизменённых файлов, находящихся под контролем версий, есть тот коммит, на который указывает HEAD. То есть, если HEAD будет указывать на коммит с хэшем 6e04e.., то файлы окажутся в первоначальном своём состоянии. Для «движения» указателя HEAD существует команда: git checkout . Те, кто знаком хоть чуть-чуть с git'ом, узнали в этой команде переключение на другую ветку. Всё совершенно верно — при переключении на другую ветку мы просто переносим указатель HEAD на последний коммит ветки.

Перенос указателя HEAD (git checkout)


Откат по истории коммитов:



После завершения операции checkout мы будем находиться в состоянии, в котором были два коммита назад. Это всё прекрасно — мы сделали шажок в прошлое, что-то там подглядели, но как вернуться назад? Я вот, например, не обладаю сверхпамятью, и не помню хэш самого последнего коммита (тот, который самый правый — d79fb..). Если написать git log, то увидим историю, состоящую из трёх коммитов:
[user@localhost project]$ git log --pretty=oneline
6741a69bd121c295413be95d7597cd7409e713a0 add unit test
b3e74f50c3cc48e6b335014b6dc7e301b382a903 add readme
6e04e39d0952a2d6022502d56aaa05d5a064bea Initial commit

Неужели мы потеряли всю историю? Как узнать самый «новый» коммит? Это не проблема — есть выход, и их несколько:

  1. Написать команду git log --all. Данная команда напечатает нам всю историю, вплоть до современности, т.е. в нашем случае историю из пяти коммитов:
    [user@localhost project]$ git log --pretty=oneline --all
    d79fb5688af71b4577f450919535e7177e9d74e8 fix bug
    478927e3a088d3cec489ca8810eaaca97c6ce0ff documentation
    6741a69bd121c295413be95d7597cd7409e713a0 add unit test
    b3e74f50c3cc48e6b335014b6dc7e301b382a903 add readme
    6e04ee39d0952a2d6022502d56aaa05d5a064bea Initial commit
    

    Далее остаётся скопировать нужный нам хэш и вновь запустить машину времени: git checkout. Но данный способ не рекомендую, так как он требует слишком много действий.
  2. Git позволяет отслеживать все изменения указателя HEAD. Это возможно командой git reflog, но это уже не для новичков и используется не для поставленных нами целей. Самое грамотное — это поступить следующим образом:
  3. Вспомнить, что указатель master указывает на самый свеженький коммит. Таким образом, возврат в исходное состояние выполняется одной командой: git checkout master. Вуа-ля:




Для прояснения механизма git checkout создадим новую ветку devel:
[user@localhost project]$ git checkout -b devel

*флаг -b означает, что необходимо создать ветку с указанным именем и сразу переключится на неё.
Проиллюстрируем совершённое нами действие:



Заметим, что указатель HEAD указывает на вершину ветки devel.

Породим в новой ветке несколько коммитов. История репозитория будет выглядеть следующим образом:



Возвращение в ветку master происходит также безболезненно:
[user@localhost project]$ git checkout master




Итак, запоминаем первый пункт:
  • Комнда git checkout передвигает указатель HEAD

Перенос указателя на вершину ветки (git reset ...)


Кроме того, git позволяет двигать не только HEAD, но и континеты указатели на вершины веток. Для этого существует команда git reset с ключами либо --soft, либо --hard.
  • Ключ --hard означает, что мы теряем текущее состояние файлов и приобретаем состояние того коммита, куда был сделан reset.
  • Ключ --soft означает, что мы НЕ теряем текущее состояние проекта, но указатель на текущую ветку уже передвинут, т.е. git status нам выдаст разницу между текущим состоянием проекта (от которого мы сделали reset) и тем, на который мы сделали reset.

В обоих случаях появляется «прозвище» для коммита, с которого был совершён reset — ORIG_HEAD.

git reset --hard HEAD~2:


git reset --soft HEAD~2:


ORIG_HEAD полезен для редактирования неверных коммитов на локальной машине (!). Предположим, что мы хотим объединить два последних коммита в единый. Для этого, сохраняя текущее состояние файлов, переводим указатель master на два коммита назад:
[user@localhost project]$ git reset --soft  HEAD~2

Посмотрим на изменения:
[user@localhost project]$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#	ЧТО-ТО ТАМ ДЛЯ КОММИТА
#

Ну а теперь сделаем трюк — объединяем коммиты
[user@localhost project]$ git commit -c ORIG_HEAD

Вводим сообщение, сохраняемся. Теперь наша история выглядит вот так:



Важное замечание — ORIG_HEAD по-прежнему указывает на коммит d79fb… Если мы сейчас выполним команду git checkout ORIG_HEAD, то мы получим так называемое состояние detach HEAD. Оно характеризуется тем, что HEAD указывает не на вершину ветки, а просто на коммит. HEAD всегда должен указывать только на вершину какой-либо ветки!



Чтобы «выйти» из состояния detach HEAD достаточно просто переключиться на какую-либо ветку или создать новую ветку командой git checkout -b new_branch_name
Итак, запоминаем второй пункт:
  • git reset с ключами --soft или --hard двигает указатель на вершину ветки, а вместе с ним и указатель HEAD.

И самое главное! Самая частая операция из вышеперечисленных при работе с git`ом — это переключение между ветками. Все остальные рассмотренные случаи встречаются редко, но тем не менее необходимо понимать всё, что происходит при их использовании!

Удачных вам путешествий по истории своего репозитория!

При подготовке материала использовались следующие источники:
Самый лучший manual — книга: ProGit
Наглядная справка по git: A Visual Git Reference (Русская версия)

UPD:
В комментариях посоветовали ещё один полезный ресурс по git`у: githowto

P.S. Благодарю за инвайт и всем желаю приятных выходных!
Сергей @yse
карма
42,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (39)

  • +3
    Гит отличная штука. За прогит — отдельное спасибо авторам. Здорово помог, когда начинал пользоваться git-ом
    • +1
      Да, книга великолепна. После прочтения появляетя не просто набор интрукция и workflow, а понимание механизмов работы git'а.
  • +2
    Я бы в ссылки еще добавил: githowto.com/ru
    • 0
      Спасибо за совет. Раньше не знал этого ресурса. Возьму на заметку.
  • +1
    Спасибо за статью. И отдельное спасибо за иллюстрации.
    • +1
      Пожалуйста =) я эти картинки сперва рисовал на бумаге, объясняя коллегам команды, затем надоело — решил написать статью и кидать ссылку.
  • +1
    Погубить историю в гите — это надо попотеть. :)
    git reflog поможет заблудившимся. Главное — не усердствовать с git gc.
  • +1
    gitk --all
  • +3
    Отличная статья на помнимание указателей с отличными иллюстрациями, спасибо!
  • +1
    И в самом конце статьи нужно напомнить про GitFlow. Теория и практика.
  • +4
    Для редактирования локальной истории (считаю это дело полезным, чтобы во внешнем репе была чисто и красиво) использую git rebase. В частности, для описанного примера со слиянием пары комитов в один удобен интерактивный rebase:

    $ git rebase -i HEAD~2

    и далее в редакторе помечаем последний комит буквой s (squash). А вот git reset так давно не использовал, что успел забыть, зачем он нужен. Впрочем, картинка в посте вполне освежает память.
  • 0
    какие-то еще операции, кроме git reset влияют на ORIG_HEAD и что произойдет с веткой коммитов на который указывал ORIG_HEAD после того, как он изменится?
    • +1
      ORIG_HEAD появляется после так называемых «опасных» команда, к которым и относится git reset с набором определённых флагов.
      А ветка коммитов, на который указывал ORIG_HEAD никуда не девается — я на самом деле немного слукавил, сказав, что возможно потерять историю. Все «неоприходаванные» коммиты (читай — не принадлежащие ни одной ветке) можно выцепить командой git log --all, либо git reflog. git reflog, если в кратце, отражает историю движения указателя HEAD. Обладая этой информацией вполне можно восстановить «потерянные» коммиты, но, если честно, извините, слегка геморрой. Лучше этого избегать.
  • 0
    Как Я понимаю это все меняет только указатели, но не сами файлы? или Я запутался?
    • 0
      Да, Вы запутались. Со сменой указателей (а конкретно — указателя HEAD) меняется и состояние файлов (за исключением git reset --soft). Файлы приобретают то состояние, куда указывает HEAD. Это важный и немного странный на первый взгляд момент, который обычно и вводит в заблуждение новичков.
  • –1
    Из всех этих howto я понял, что git — такая штука, на изучение которой придется потратить приличное время. Нельзя просто так взять и начать использовать git.
    • 0
      Можно начать использовать GIT в «визуальном режиме» в любимой IDE, а потом уже разбираться с нюансами и доп.возможностями. Это будет лучше, чем вообще не начинать.
    • 0
      Как раз начать использовать можно сразу и довольно просто.
      Я в свое время так и перешел с svn на git когда в очередном проекте все на него перевели.
      В гите конечно куча функций, но их вас никто не обязывает изучать. И вы можете вполне пользоваться базовой функциональностью, не задумываясь про остальное.
      Например, я регулярно пользуюсь git rebase для слияниея коммитов перез отправкой их на публичный репо.
      А у этой команды есть опция --onto про которую я знаю из мануалов, но никак не могу запомнить что она делает, т.к. она мне ни разу не нужна была. Что говорить про остальные опции о которых я вообще не подозреваю :)
      И ничего — живу :)

      Кстати из описанных команд, для новичков я считаю нужно знать только
      git checkout ветка (переключиться на ветку)
      git checkout -b ветка (создать ветку из текущей и переключиться)
      git reset --hard (отменить все незакоммиченное)
      git reset --hard HEAD~n (отменить все незакоммиченное и откатиться на n коммитов)

      А git reset --soft это честно говоря уже из разряда извращений, тем более пример приведенный для этой опции надуманный, гораздо проще и надежнее сливать коммиты через git rebase -i HEAD~n (и не только сливать, но и переупорядочивать, менять комменты, выборочно удалять и прочее)

      • 0
        Вместо git reset --hard отмену можно делать с помощью «git stash», «git stash apply» делать никто не обязывает :-)
        • 0
          Можно.
          Только зачем замусоривать репо файлами которые не нужны?
          stash предназначена для совсем другого — для сохранения, а не удаления.
          • 0
            Замусоривание минимальное, да и перетирается в следующий раз. Я не говорю что это оптимальный путь, просто как вариант.
            • +1
              Что значит «перетирается в следующий раз»? Хранилище у stash организовано на основе стека. И при очередном git stash мы добавляем в стек новое состояние. И если не чистить stash, то стек будет только расти.
              • 0
                Не стек, а лог.
          • 0
            git это как tex, или vim — один и тот же результат получить несколькими путями. Это из-за гибкости. Из-за гибкости, кстати, и геморрой при первичном знакомстве. Отсюда формула — за гибкость приходится платить геморроем при обучении =) Зато если привынуть — можно горы сворачивать =) Мощны инструменты.
  • +1
    Наглядная справка по git: A Visual Git Reference (Русская версия)

    Ссылка на русскую версию ведет на английскую, правильная ссылка: marklodato.github.com/visual-git-guide/index-ru.html
    • 0
      Спасибо, поправил.
  • 0
    Как-то сложновато всё, имхо. А штуки типа detach HEAD производят впечатление текущей абстракции. Но сама статья супер, спасибо, теперь более или менее разобрался.
  • +1
    >> «master указывает на самый старший коммит в ветке под названием master „
    >> “Вспомнить, что указатель master указывает на самый свеженький коммит.»

    Так самый старший или самый младший?
    • 0
      гм, великий и могучий русский язык. С одной стороны Вы правы, безусловно. Но вот с другой стороны — более старший коммит — более старшая ревизия по номеру, а не по возрасту =) вот такая вот дилемма
  • 0
    Подскажите плз толковый git GUI. Сам всегда использую HG + TortoiseHg, но сейчас возникла необходимость работы с GitHub — после TortoiseHG хочется чего нибудь такого же удобного.
    • 0
      Используйте то, к чему привыкли, только Git+TortoiseGit
      • 0
        попробовал… к моему сожалению tortoiseHg и tortoiseGit схожи только префиксом «tortoise» — очень странно что при практически одинаковой функциональности Git и Hg, эти tortoise клиенты так сильно отличаются.
    • 0
      Еще есть нативный клиент GitHub
      • 0
        Извините, но это не GUI клиент, а какое-то гламурное убожество… самый базовый набор команд завернутый в неюзабельный интерфейс.
    • 0
      http://git-scm.com/downloads/guis
      На мой взгляд, самый удачный — Tower.

      P.S. Маленький совет: настройте себе удобную консоль и используете её. Все мои знакомые, которые пользуются разными git GUI, испытывают проблемы с каким-нибудь функционалом гита. Гит сделан для удобной работы в консоли, так что любой гуи — это ограничение.
  • 0
    А как можно проделывать подобные вещи с bare репозиторием?
    • 0
      а bare-репозиторий ничем от «обычного» не отличается в этом плане. Но тут есть два тонких момента:
      1) Если с этого репозитория никто ничего не брал и брать не планирует, т.е. с него не было сделано клонов, тогда можно гонять указатель по всей истории как вздумается. НО:
      2) Если кто-то клонировал этот репозиторий, тогда откатывать нет никакого смысла, ибо когда тот, кто отклонировал, будет в него «пушить», то он всё равно восстановит этим действием откаченную историю. Для того, чтобы отменить тот или иной коммит в истории bare-репозитория необходимо породить новый коммит. Обычно для этого используют git revert

      я надеюсь правильно понял Ваш вопрос.
  • 0
    я о том, что bare репозиторий вроде как не воспринимает прямые команды
    %mkdir temp
    %cd temp
    %git init --bare
    Initialized empty Git repository in /www/github/data/temp/
    %git status
    fatal: This operation must be run in a work tree
    • 0
      Как я понимаю, git status обращается к файлам, находящимся под контролем. В bare-репозитории их, естесственно нет — там только так называемые snapshots файловой системы. К файлам обращается и git checkout и git reset --hard, но git reset --soft спокойно двигает веточку, так как результат этой операции лишь изменение значений указателей, а не самой файловой системы. Но опять же повторю, в bare-репозитории этим лучше не заниматься по указанной мною выше причине.

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