この時期大学四年生は卒論の時期で、今まで論文やサーベイだったのが 自分で手を動かすフェイズになっていることでしょう。 その際にバックアップというのは重要になります。
例えば研究用のソースコードを改造していく途中でプログラムを壊してしまって 元に戻したい、別に実装を試してみたくなる場合というのはあると思います。 また壊しても元に戻せるという安心感があればガンガンプログラムを変えていくことができます。 証明や卒論の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.ml
とdata.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.ml
とhello.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: ログを見る
今までのコミットの履歴を見てみましょう。
ちなみに下のコマンドで--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
と言った時、最新のコミットの状態のことを指します。最新のコミットの状態には名前があるわけです。
名無しの状態を変更し新たにコミットした時、そのコミットを指す名前がありません。
そのためその変更した状態には辿れなくなってしまうわけです。
そのことをワーニングメッセージは教えてくれています。
変更した後、そのため新たにブランチを作成して名前をつけてあげましょう。
[~/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
というブランチが存在して、現在自分の状態はそこにいるよということがわかります。
次にまた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-n
とC-p
で上下移動して
- unsagedな、またはuntruckなファイルの上で
s
でステージする - stagedなファイルの上で
u
でアンステージする - 変更したファイルの上で
M-x magit-discard
で変更を最新のコミットまで戻す
magit-log
M-x magit-log
からgit log
よりも見やすい?感じでログを確認できます。C-n
とC-p
で上下移動してEnter押すとそのコミットのdiffを表示できます。
magit-commit
M-x magit-commit
からEmacsのインターフェイスを通して
コミットすることができます。magit-status
の場面からc c
でもOKです。
C-c C-c
で入力完了C-c C-k
でやっぱりやめる
magit-branch-manager
Emacsのバッファからブランチを切り替えられます。C-n
とC-p
で上下移動してEnterでブランチ切り替えです。
あとはマニュアル読んでください。
注意すべきはmagit
は確かにgit
の煩わしい操作をだいぶ楽にしてくれますが、それでも起こっていることはgit
をCUIで叩いた時と一緒だということです。なので、楽にはしてくれますがgit
の仕組みの理解というのは相変わらず必要とされます。
TODO:
- github, bitbucket との連携の仕方
- 間違ったcommitを取り消すには?
- git-guitterの紹介