Heavy Watal

Git — 分散型バージョン管理システム

https://git-scm.com/

Gitは分散型バージョン管理システムの代表格。 プログラムのソースコードはもちろんのこと、 研究ノートや論文の原稿などあらゆるテキストの管理に使える。

GitHubはGitをより便利に使うためのオンラインサービス。 個人的なリポジトリ置き場としてはもちろんのこと、 ほかの人と共有・協力してプロジェクトを進めるプラットフォームとしても使える。

GitのライバルとしてMercurialもあるが、 BitBucket (GitHubのライバル) がGit対応した今となってはMercurialを積極的に使う理由は無い気がする。 むしろAtomにおける差分表示など、 Gitでなければ得られない恩恵が大きくなってきている。

基本

準備

手元の変更を外に伝える

working directory (working tree)
手元のファイルの変更はまだリポジトリに登録されていない
add
staging area (index)
次のコミットに含めるファイルをマークする段階
commit
local repository
変更履歴が .git/ 内に記録されている
push
remote repository
GitHubなど別マシンのリポジトリに反映

外部の変更を手元に取り込む

remote repository
fetch
local repository
変更が .git/ に取り込まれたが、見えてるファイルには反映されてない
checkout or merge
working directory
手元のファイルが最新版に同期されている

用語

blob
git内部で1つのファイルを指すオブジェクトで、add時に作られる。 ファイル名などのメタデータは持たず、 ファイルの内容にのみ依存したハッシュIDを持つ。
tree
git内部で1つのディレクトリを指すオブジェクトで、commitした時に作られる。 blobやファイル名などのメタデータに依存したハッシュIDを持ち、 その変化は親に伝播する。
commit
git内部でroot treeのsnapshotを指すオブジェクト。 root treeのハッシュID、著者、コメントなどの情報を持つ。 動詞としては、staging areaの情報をひとつのcommitとしてリポジトリに登録することを指す。
repository
commitの履歴を保持する拠点。 git init で手元に新規作成するか、git clone でリモートから複製する。
origin
remoteリポジトリの典型的なshortname。 clone時に自動的に追加され、 push先やfetch元を省略したときにデフォルトで使われる。 git remote -v で確認。
master
デフォルトのブランチの典型的な名前。
HEAD, @
現在checkoutしているbranch/commitを指すポインタ。 基本的にはmasterの最新commitを指していることが多い。 1つ前は HEAD^HEAD~、 2つ前は HEAD^^HEAD~~HEAD~2。 (HEAD^2merge で複数の親がある場合の2番目)

zshのEXTENDED_GLOBが有効になってる場合は HEAD^ がパターン扱いされてエラーになるので、 HEAD\^ のようにエスケープするか unsetopt NOMATCH しておいたほうがいい。

よく使うコマンド

reset

git reset <DESTINATION>HEAD の位置を戻す処理で、 オプションによってindexとworing treeもそこに合わせるように変更される。 --soft なら HEAD 移動のみ。 --mixed なら移動した HEAD にindexも合わせる。 --hard なら移動した HEAD にindexとworiking treeも合わせる。 直前の動作を取り消す用途に絞って使うのが無難:

# commit直後、それを取り消す (indexとworkingはそのまま)
git reset --soft HEAD^

# add直後、それを取り消す (workingとHEADはそのまま)
git reset --mixed HEAD

# 変更したファイルをHEADの状態に戻す (DANGEROUS!)
git reset --hard HEAD

# reset直後、それを取り消す
git reset --hard ORIG_HEAD

# divergedになってしまった手元のbranchを破棄 (DANGEROUS!)
git reset --hard origin/master

直前のcommitをちょっと修正したいだけなら git commit --amend が簡単。

diff

差分を表示:

# HEAD vs working (staging前のファイルが対象)
git diff

# HEAD vs index (staging済みcommit前のファイルが対象)
git diff --staged

# HEAD vs working+index (commit前の全ファイルが対象)
git diff HEAD

# 特定コミットの変更点 (diffじゃない...)
git show [revision]

rm, clean

tracking対象から外して忘れさせる(手元のファイルはそのまま):

git rm --cached <file>

.gitignore で無視されてるuntrackedファイルを消す:

git clean -fdX

無視されていないuntrackedファイルも消したい場合は小文字の -fdx (危険)。

Submodule

既存のリポジトリをsubmoduleとして追加する

git submodule add https://github.com/mbostock/d3.git

# ブランチを指定する場合:
git submodule add -b gitsubmodule_https https://github.com/heavywatal/x18n.git

gh-pagesで公開する場合は参照プロトコルを git:// ではなく https:// にする必要がある。

submoduleを含むメインリポジトリを使い始めるとき

最初にclone/fetchしてきた時submoduleたちは空なのでまず:

git submodule update --init

# 使いたいbranchがmasterではない場合は --remote
git submodule update --init --remote x18n

# 歴史があって重いリポジトリはshallowに
git submodule update --init --depth=5 d3

submoduleを更新

  1. 更新分をまとめて取得:

    git submodule foreach git fetch
    
  2. 好きなとこまでチェックアウト:

    cd d3/
    git checkout v3.5.6
    
  3. メインリポジトリでその変更をコミット:

    cd ..
    git commit
    

GitHub Pages

https://help.github.com/articles/user-organization-and-project-pages/

ユーザーサイトを作る

  1. USERNAME.github.io という名前のリポジトリをGitHub上で作成
  2. 公開したいウェブサイトをmasterブランチとしてpush
  3. https://USERNAME.github.io にアクセスしてみる。

例えば本ウェブサイトは heavywatal.github.io というリポジトリの sourceブランチでMarkdownテキストを書き、 Hugo で変換・生成したHTMLファイルをmasterブランチに書き出している。

GitHubが勝手にJekyll処理しようとすることがあるので、 .nojekyll という空ファイルを作っておく。

プロジェクトサイトを作る

https://help.github.com/articles/configuring-a-publishing-source-for-github-pages/

リポジトリの内容を https://USERNAME.github.io/PROJECT/ に公開することができる。 方法は複数あり、リポジトリの設定画面から選択できる。

  1. gh-pages ブランチの内容を公開 (古い方法)
  2. master ブランチの内容を公開
  3. master ブランチの /docs ディレクトリのみを公開

前は1番の方法しかなくてブランチの扱いがやや面倒だったが、 今ではmasterだけで簡単に済ませられるようになった。

Pull Request (PR)

基本の流れ

例えば USER さんの PROJECT のコード修正に貢献する場合。

  1. github.com/USER/PROJECT のForkボタンで自分のGitHubリポジトリに取り込む
  2. forkした自分のリポジトリからローカルにclone:

    git clone git@github.com:heavywatal/PROJECT.git
    cd PROJECT/
    
  3. 大元のリポジトリにupstreamという名前をつけておく:

    git remote add upstream git://github.com/USER/PROJECT.git
    
  4. PR用のブランチを切って移動:

    git checkout -b fix-typo
    
  5. コードを変更してcommit:

    emacs README.md
    git diff
    git commit -a -m "Fix typo in README.md"
    
  6. この間にupstreamで更新があったら、それをデフォルトブランチ越しに取り込む:

    git checkout master
    git fetch upstream
    git merge upstream/master
    git push origin/master
    git checkout fix-typo
    git rebase -i master
    
  7. 自分のリポジトリにpush:

    git push [-f] origin fix-typo
    
  8. GitHub上に出現する”Compare & pull request”ボタンを押す

  9. 差分を確認し、コメント欄を埋めて提出

  10. 修正を求められたらそのブランチで変更し、自分のリポジトリにpushすればPRにも反映される

  11. マージされたらブランチを消す

問題と対処

用済みブランチの掃除

PRマージ済みのリモートブランチを消したい。 明示的に消すオプションを付けるか、空ブランチをpushするか:

% git push --delete origin issue-42
% git push origin :issue-42

別のマシンやGitHub PR画面のボタンから消したりすると、 消したはずのリモートブランチの記録が手元のリポジトリに残ることがある。 まず確認:

% git branch -a
% git remote show origin

こうしたstaleなブランチを刈り取る方法には二通りある:

% git fetch --prune
% git remote prune origin

それでも消えないローカルブランチは手動で消す:

% git branch -d issue-666
error: The branch 'issue-666' is not fully merged.
If you are sure you want to delete it, run 'git branch -D issue-666'.

マージ済みのブランチじゃないと上記のように怒ってくれる。 消してもいい確信があればオプションを大文字 -D にして強制削除。

detached HEAD からの復帰

submoduleなどをいじってると意図せずdetached HEAD状態になることがある。 その状態でcommitしてしまった変更をmasterに反映したい。

  1. pushしようとして怒られて気付く

    % git push
    fatal: You are not currently on a branch
    % git status
    HEAD detached from *******
    
  2. masterに戻ると道筋を示してくれる:

    % git checkout master
    Warning: you are leaving 2 comits behind, not connected to
    any of your branches
    If you want to keep them by creating a new branch, this may be a good time
    to do so with:
    
    git branch <new-branch-name> *******
    
  3. 言われたとおりbranchを作ってmerge

    % git branch detached *******
    % git merge detached
    
  4. 不要になったbranchを消す

    % git branch -d detached