type t (* void *)

ソフトウエアのこととか

git: 独りで使うgit入門

この時期大学四年生は卒論の時期で、今まで論文やサーベイだったのが 自分で手を動かすフェイズになっていることでしょう。 その際にバックアップというのは重要になります。

例えば研究用のソースコードを改造していく途中でプログラムを壊してしまって 元に戻したい、別に実装を試してみたくなる場合というのはあると思います。 また壊しても元に戻せるという安心感があればガンガンプログラムを変えていくことができます。 証明や卒論のtexファイルも同様に管理できます。

その際に使えるgitというバージョン管理ソフトの使い方を紹介します。 gitのことはどこかで聞いたことがあるでしょうが、 特にこの記事では一人で使う場合に特化して、手っ取り早く卒研に使えるように説明を絞ります。
(gitは多くのプログラマが使っている道具なので、他の使い方は調べれば簡単にわかるでしょう)

gitでできること

一人での作業で有用なところに限って紹介すると、

  • ある状態のスナップショットを(永遠に)とっておくことができる
  • 保存した状態に、好きなタイミングで戻せる
  • ソフトウエアのいろいろなバージョンの状態に名前をつけてとっておくことできる
  • 変更した履歴をわかりやすく見ることができる

はじめに

  • git initはおまじない。gitを使いはじめるコマンドで.gitファイルを作ります。
  • 初めてgitを使うならば、初期設定をしておく
    参考:1.5 使い始める - 最初のGitの構成

      git config --global user.name "YOUR_NAME"
      git config --global user.email "YOUR_EMAIL_ADDRESS"
    

ファイルの管理

gitにおいてファイルというのは以下のように区分されます

  • 管理されないファイル
  • 管理されているが、変更されてないファイル
  • 管理され、変更されているがステージされていない(次のコミットに含まれない)ファイル
  • 管理され、変更されておりステージされている(次のコミットに変更が含まれる)ファイル

そのため主にgitでの管理では以下のような流れが基本になります

  • ファイルをgit add FILE_NAMEで管理対象にする
  • 変更したファイルコミットするために、git stage FILE_NAMEでステージする
  • git commit -m "COMMIT_MESSAGE"でコミットする
    (メッセージの内容は任意だが、どんな変更をしたのかわかりやすいメッセージが良い)

用語

  • ブランチ
    バージョンみたいなもの。例えば1つのソフトウエアでも複数バージョンに分けて 管理したいときブランチを分けておくことをgitではやる。
  • コミット
    レポジトリのスナップショットを取るようなもの。 1度コミットしたものは破壊する操作をしない限り失われることはない。
  • ステージする
    変更を次のコミットに含めるように指示すること。

演習をしよう

チュートリアル形式でgitのひとりぼっちでの使い方を説明します。 順番に見てください!

演習1: 最初のコミット

実際に演習してみましょう。~/git-practiceというディレクトリを作ります。

[~/] cd ~/
[~/] mkdir git-practice
[~/] cd git-practice

初期化をして、ディレクトリの状態を見てみます。.gitディレクトリが作られているのがわかりますね。

[~/git-practice] git init
[~/git-practice] ls -al
> .
> ..
> .git

ここに新たなファイルを追加してみます。hello.mldata.mlいうファイルを作り以下の内容を書き込みます。 catコマンドで中身を確認していますので、演習するときは出力されているmlプログラムだけを入力してください。 ちなみにできたらコンパイルできることをocamlopt data.ml hello.ml確認しておきましょう。

[~/git-practice] cat ./hello.ml
> let () = List.iter print_int Data.l

[~/git-practice] cat ./data.ml
> let l = [1;2;3]

[~/git-practice] ocamlopt data.ml hello.ml

現在どういう状況なのか、git statusコマンドで確認してみます。

[~/git-practice] git status
> On branch master
>  
> Initial commit
>  
> Untracked files:
>   (use "git add <file>..." to include in what will be committed)
>  
>     a.out
>     data.cmi
>     data.cmt
>     data.cmx
>     data.ml
>     data.o
>     hello.cmi
>     hello.cmt
>     hello.cmx
>     hello.ml
>     hello.ml~
>     hello.o
>  
> nothing added to commit but untracked files present (use "git add" to track)

このメッセージはたくさんの事を教えてくれます。

  • 今のブランチはmasterというブランチであること
  • Untrackedなファイル(管理していないファイル)の一覧
  • 現在コミットするべき内容はないということ

さて、いよいよこの2つのファイルを管理対象にしてみましょう。 上の状態で確かめてみると、ocamloptコマンドのおかげで中間生成物(.oファイルや.cm?ファイル)がたくさん作られていることがわかりますが、今管理したいのは.mlファイルだけです。ついでに状態も確認してみます。

ファイルを管理対象に加えるためにgit add <file> ...というコマンドを使いましょう!

[~/git-practice] git add ./hello.ml ./data.ml
[~/git-practice] git status
> On branch master
>  
> Initial commit
>  
> Changes to be committed:
>   (use "git rm --cached <file>..." to unstage)
>  
>     new file:   data.ml
>     new file:   hello.ml
>  
> Untracked files:
>   (use "git add <file>..." to include in what will be committed)
>  
>     a.out
>     data.cmi
>     data.cmt
>     data.cmx
>     data.o
>     hello.cmi
>     hello.cmt
>     hello.cmx
>     hello.ml~
>     hello.o

さて、メッセージの変化している部分に注目してみましょう("Changes to be commited:"のあたり)。 これが教えてくれているのは

  • 新しいファイルdata.mlhello.mlが次のコミットに含まれること

ということです。今gitが認識しているのは空の状態です。 それに2つのファイルを追加する、という変更が次のコミットで新たになされますよということを教えてくれています。

さて、いよいよこの変更をコミットしてみましょう。git commitというコマンドを用います。

git commit -m "initial commit"
> 2 files changed, 2 insertions(+)
> create mode 100644 data.ml
> create mode 100644 hello.ml

git commitだけ入力して見るとエディタが立ち上がりコミットメッセージを入力する画面になるはずです。 私にはそれは煩わしいので-mオプションをつけてパラメータとして コミットメッセージを入力したほうが便利だなぁと思っています。

再度状態を確認してみましょう。

[~/git-practice] git status
> On branch master
> Untracked files:
>   (use "git add <file>..." to include in what will be committed)
>  
>     a.out
>     data.cmi
>     data.cmt
>     data.cmx
>     data.o
>     hello.cmi
>     hello.cmt
>     hello.cmx
>     hello.ml~
>     hello.o
>  
> nothing added to commit but untracked files present (use "git add" to track)

今までの変更をすべてコミットした後なので、"to be commited"なファイルなどは表示されません。 また"nothing added to commit ..."からわかる通りコミットすべき変更も知らないよって言っています。

演習2: ファイルを変更していく

さて、ここからファイルを変更した時にどうすればいいのか手順を学びます。 data.mlの持っているデータが[1;2;3]では味気ないので[fact 1; fact 2; fact 3]を保持するように変更してみます。

[~/git-practice] cat data.ml
> let rec fact n =
>   if n = 1 then 1 else n * fact (n - 1)
>  
> let l = [fact 1; fact 2; fact 3]

さて、この変更をセーブした後git statusで状態を確認してみましょう。

[~/git-practice] git status
> On branch master
> Changes not staged for commit:
>   (use "git add <file>..." to update what will be committed)
>   (use "git checkout -- <file>..." to discard changes in working directory)
>  
>     modified:   data.ml
>  
> Untracked files:
>   (use "git add <file>..." to include in what will be committed)
>  
>     a.out
>     data.cmi
>     data.cmt
>     data.cmx
>     data.ml~
>     data.o
>     hello.cmi
>     hello.cmt
>     hello.cmx
>     hello.ml~
>     hello.o
>  
> no changes added to commit (use "git add" and/or "git commit -a")

このメッセージを読み解けるでしょうか。 まず"Changes not staged for commit:"のあたりを読みましょう。 このメッセージは変更はされているのだけど次のコミットには含まれない変更があるよ、という事を教えてくれています。

これをステージする方法もこのメッセージは教えてくれています。git stage <file> ...というコマンドですね。 ちなみにgit add <file>でもOKです。全く同じ意味なのですが私はstageを使う方が好きです。

[~/git-practice] git add ./data.ml
[~/git-practice] git status
> On branch master
> Changes to be committed:
>   (use "git reset HEAD <file>..." to unstage)
>  
>     modified:   data.ml
>  
> Untracked files:
>   (use "git add <file>..." to include in what will be committed)
>  
>     a.out
>     data.cmi
>     data.cmt
>     data.cmx
>     data.ml~
>     data.o
>     hello.cmi
>     hello.cmt
>     hello.cmx
>     hello.ml~
>     hello.o

状態が変化しました!"Changes not staged for commit"から"Changes to be committed"になっています。 更に"no changes added to commit ..."という文もなくなり、次にコミットする変更を認識している状態になっています。

これをコミットしてみましょう。

[~/git-practice] git commit -m "change Data.l to print factrial of 1, 2 and 3"
> [master d3365c9] change Data.l to print factrial of 1, 2 and 3
>  1 file changed, 4 insertions(+), 1 deletion(-)

この後の状態を確認しておきましょう。

演習3: ログを見る

参考:2.3 Git の基本 - コミット履歴の閲覧

今までのコミットの履歴を見てみましょう。
ちなみに下のコマンドで--no-pagerが付いているのはEmacs上のshell-modeで実行するためです。 端末で実行する人はこのオプションは必要ありません。

[~/git-practice] git --no-pager log
> commit d3365c97d09109c366d8d7f15f5060402fd82f37
> Author: nomaddo <nomaddo@example.com>
> Date:   Fri Dec 11 15:40:10 2015 +0900
>  
>     change Data.l to print factrial of 1, 2 and 3
>  
> commit a02e4107793d30cabab75bdd94d700ee7a19b5aa
> Author: nomaddo <nomaddo@example.com>
> Date:   Fri Dec 11 15:24:30 2015 +0900
>  
>     initial commit

今までの変更履歴を見ることができます。今まで行った2つのコミットメッセージが見えているのが確認できるでしょうか。 diffも一緒に見たい場合は-pオプションを一緒につけましょう。

[~/git-practice] git --no-pager log -p
> commit d3365c97d09109c366d8d7f15f5060402fd82f37
> Author: nomaddo <nomaddo@example.com>
> Date:   Fri Dec 11 15:40:10 2015 +0900
>  
>     change Data.l to print factrial of 1, 2 and 3
>  
> diff --git a/data.ml b/data.ml
> index 1d8ca5a..4fefe56 100644
> --- a/data.ml
> +++ b/data.ml
> @@ -1 +1,4 @@
> -let l = [1;2;3]
> +let rec fact n =
> +  if n = 1 then 1 else n * fact (n - 1)
> +
> +let l = [fact 1; fact 2; fact 3]
>  
> commit a02e4107793d30cabab75bdd94d700ee7a19b5aa
> Author: nomaddo <nomaddo@example.com>
> Date:   Fri Dec 11 15:24:30 2015 +0900
>  
>     initial commit
>  
> diff --git a/data.ml b/data.ml
> new file mode 100644
> index 0000000..1d8ca5a
> --- /dev/null
> +++ b/data.ml
> @@ -0,0 +1 @@
> +let l = [1;2;3]
> diff --git a/hello.ml b/hello.ml
> new file mode 100644
> index 0000000..f9e9456
> --- /dev/null
> +++ b/hello.ml
> @@ -0,0 +1 @@
> +let () = List.iter print_int Data.l

プレーンテキストではやや見づらいと思いますが、端末で実行すると色がついた形で出力され、 かなり見やすさは向上するので端末でやるのがおすすめです。

演習4: 更にコミット(おさらい)

さて、演習2の状態から更に少し変更してコミットしてみましょう。 今まではprint_intを使っていましたがプリントして改行するようにするためPrintf.printf "%d\n"に変更します。

[~/git-practice] cat ./hello.ml
> let () = List.iter (Printf.printf "%d\n") Data.l

更に今までは毎回ocamlopt data.ml hello.mlと入力しなければなりませんでした。 これは少し不便なのでMakefileを書くことにしましょう。

[~/git-practice] cat ./Makefile 
> OCAMLOPT=ocamlopt
> SOURCES=data.ml hello.ml
>  
> default: $(SOURCES)
>     $(OCAMLOPT) $(SOURCES)

分割コンパイルもしない中々へぼいMakefileですが:) 一応出来ました。 状態を確認した後、これの変更をステージしてコミットしましょう。

本当は基本的には1つの変更につき1つのコミットをするのが良いらしいですが、、、 今回はMakefileを追加したという変更と開業をプリントの際にするようにしたという関係ない変更を一緒にコミットしてしまいます。

[~/git-practice] git status
> On branch master
> Changes not staged for commit:
>   (use "git add <file>..." to update what will be committed)
>   (use "git checkout -- <file>..." to discard changes in working directory)
>  
>     modified:   hello.ml
>  
> Untracked files:
>   (use "git add <file>..." to include in what will be committed)
>  
>     Makefile
>     a.out
>     data.cmi
>     data.cmt
>     data.cmx
>     data.ml~
>     data.o
>     hello.cmi
>     hello.cmt
>     hello.cmx
>     hello.ml~
>     hello.o
>  
> no changes added to commit (use "git add" and/or "git commit -a")

[~/git-practice] git add Makefile hello.ml
[~/git-practice] git status
> On branch master
> Changes to be committed:
>   (use "git reset HEAD <file>..." to unstage)
>  
>     new file:   Makefile
>     modified:   hello.ml
>  
> Untracked files:
>   (use "git add <file>..." to include in what will be committed)
>  
>     a.out
>     data.cmi
>     data.cmt
>     data.cmx
>     data.ml~
>     data.o
>     hello.cmi
>     hello.cmt
>     hello.cmx
>     hello.ml~
>     hello.o

[~/git-practice] git commit -m "add Makefile && change to print with line-breaks"
[master c2537e5] add Makefile && change to print with line-breaks
> 2 files changed, 6 insertions(+), 1 deletion(-)
> create mode 100644 Makefile

上の操作の意味はわかったでしょうか?わからなければ演習2をもう一度確認してください。

演習5: 失敗した変更をなかったことにする

演習4の状態から、前の状態に戻ってみましょう。
まずはファイルを編集しているうちにファイルが壊れてしまい、以前コミットした状態に戻したくなった時のコマンドを紹介します。

[~/git-practice] cat ./hello.ml
> (* 変更しているうちに型エラーが取れなくなってしまった!戻したい! *)
> let () = List.map (Printf.printf "%s\n") Data.l

hello.mlを編集しているうちにエラーが取れなくなってしまいました。 これを直前のコミットの状態にまで戻します。

[~/git-practice] git checkout ./hello.ml

[~/git-practice] cat ./hello.ml
> let () = List.iter (Printf.printf "%d\n") Data.l

git checkoutは基本的にはブランチを切り替えるための 機能ですがファイル名を指定すると指定ファイルを最新のコミットの状態まで戻す機能があります。

演習6: 過去に戻って別のブランチを作る

さて、このコミットをする前に戻りたい時があると思います。 やっぱり階乗じゃなくてsum nを求めるようにすればよかった、などです。 今のプロジェクトはとても小さいので全部直してコミットすればいいと思いますが、 これが巨大な変更をした時ではそうはいきません。元に戻る機能が必要です。

[~/git-practice] git checkout master~2
> Note: checking out 'master~2'.
>  
> You are in 'detached HEAD' state. You can look around, make experimental
> changes and commit them, and you can discard any commits you make in this
> state without impacting any branches by performing another checkout.
>  
> If you want to create a new branch to retain commits you create, you may
> do so (now or later) by using -b with the checkout command again. Example:
>  
>   git checkout -b new_branch_name
>  
> HEAD is now at a02e410... initial commit

[~/git-practice] cat hello.ml
> let () = List.iter print_int Data.l

[~/git-practice] cat ./data.ml
> let l = [1;2;3]

git checkout master~2の意味は、masterブランチの最新版から2つ前のコミットまで戻れというものです。 そのため最初にコミットした状態まで現在の状態は戻ってきました(catの出力を見てください)。 1つ前ならばgit checkout master~1ですし、数字の部分を任意の数に変えられます。

注意するべきことは、今いる状態には名前がついていないということです。 いまmasterと言った時、最新のコミットの状態のことを指します。最新のコミットの状態には名前があるわけです。 名無しの状態を変更し新たにコミットした時、そのコミットを指す名前がありません。 そのためその変更した状態には辿れなくなってしまうわけです。 そのことをワーニングメッセージは教えてくれています。

f:id:no_maddojp:20151211171819j:plain:w400

変更した後、そのため新たにブランチを作成して名前をつけてあげましょう。

[~/git-practice] cat ./data.ml
> (* 変更したよー *)
> let rec sum n =
>   if n = 0 then 0 else n + sum (n - 1)
>  
> let l = [sum 1;sum 2; sum 3]

[~/git-practice] git branch
> * (detached from a02e410)
>   master

[~/git-practice] git branch version-sum

[~/git-practice] git checkout version-sum
> M data.ml
> Switched to branch 'version-sum'

まずgit branchコマンドにより現在どんなブランチが存在するのか確認しました。 次にgit branch <name>により新しいversion-sumという名前のブランチを作成しました。 これはあくまで作成しただけなので、git checkout version-sumによりブランチを移動しています。

さて、現在の状況を確認してみましょう。 masterブランチにいたと書いてあった部分が、version-sumになっていることがわかるでしょうか。

[~/git-practice] git status
> On branch version-sum
> Changes not staged for commit:
>   (use "git add <file>..." to update what will be committed)
>   (use "git checkout -- <file>..." to discard changes in working directory)
>  
>     modified:   data.ml
>  
> Untracked files:
>   (use "git add <file>..." to include in what will be committed)
>  
>     a.out
>     data.cmi
>     data.cmt
>     data.cmx
>     data.ml~
>     data.o
>     hello.cmi
>     hello.cmt
>     hello.cmx
>     hello.ml~
>     hello.o
>  
> no changes added to commit (use "git add" and/or "git commit -a")

この変更をステージしてコミットしてみましょう。

[~/git-practice] git commit -m "add sum function for Data.l"
> [version-sum d1f2cb6] add sum function for Data.l
>  1 file changed, 5 insertions(+), 1 deletion(-)

git branchで現在の状態を確認してみます。

[~/git-practice] git branch
>   master
> * version-sum

上で実行した時には、masterブランチしか存在していませんでしたが version-sumというブランチが存在して、現在自分の状態はそこにいるよということがわかります。

f:id:no_maddojp:20151211171831j:plain:w400

次にまたmasterに戻りたくなってきました。git checkout masterを叩いてmasterブランチに戻りましょう。

'''bash
[~/git-practice] git checkout master
> Switched to branch 'master'

[~/git-practice] cat ./data.ml 
> let rec fact n =
>   if n = 1 then 1 else n * fact (n - 1)
>  
> let l = [fact 1; fact 2; fact 3]

[~/git-practice] cat ./hello.ml
> let () = List.iter (Printf.printf "%d\n") Data.l    

演習7: branchを切り替えられない時

コミットしていない変更があるときにはブランチを切り替えられません。その際には

  • コミットしてしまう
  • 変更を元に戻して(git checkout .)からブランチを切り替える
  • git stashを活用する

git stashに関しては説明を割愛します。 ドキュメントを読んでください

おまけ: magitを通して操作する

ここまで読んだ方は、emacsからgitを操作できると便利そうだなぁという気持ちになったと思います。 magitという拡張を通して今までやってきたことを行いましょう。

magit-status

M-x magit-statusからほぼ端末上でgit statusを叩いたのと同じ表示がEmacsバッファ上に出せます。 C-nC-pで上下移動して

  • unsagedな、またはuntruckなファイルの上でsでステージする
  • stagedなファイルの上でuでアンステージする
  • 変更したファイルの上でM-x magit-discardで変更を最新のコミットまで戻す

f:id:no_maddojp:20151211221157j:plain

magit-log

M-x magit-logからgit logよりも見やすい?感じでログを確認できます。C-nC-pで上下移動してEnter押すとそのコミットのdiffを表示できます。

f:id:no_maddojp:20151211221207p:plain

magit-commit

M-x magit-commitからEmacsインターフェイスを通して コミットすることができます。magit-statusの場面からc cでもOKです。

  • C-c C-cで入力完了
  • C-c C-kでやっぱりやめる

f:id:no_maddojp:20151211221216p:plain

magit-branch-manager

Emacsのバッファからブランチを切り替えられます。C-nC-pで上下移動してEnterでブランチ切り替えです。

f:id:no_maddojp:20151211221227p:plain

あとはマニュアル読んでください。

注意すべきはmagitは確かにgitの煩わしい操作をだいぶ楽にしてくれますが、それでも起こっていることはgitCUIで叩いた時と一緒だということです。なので、楽にはしてくれますがgitの仕組みの理解というのは相変わらず必要とされます。

TODO:

  • github, bitbucket との連携の仕方
  • 間違ったcommitを取り消すには?
  • git-guitterの紹介