Linux上でのGitコマンド練習

はじめに_

バージョン管理システム(Version Control System, VCS)とは、コンピュータ上で作成、編集されるファイルの変更履歴を管理するためのソフトウェアのこと。

VCSは以下のような状況を解決するために使用される。

  • 間違ってファイルを消してしまった
  • 変更を保存した後に、変更前の方が良いことに気付いた
  • 複数人でファイルを編集しているとき、どの変更を誰が行ったのかわからない

代表的なVCSには以下のようなものがある。

集中と分散_

VCSでは、変更履歴(リビジョン、Revision。ファイルの編集前と編集後の差分、編集日時、編集者、および、編集に対する編集者のコメント)をリポジトリと呼ばれるデータベースに格納して保管している。

現在のVCSはリポジトリの管理方法から集中型と分散型の2つの種類に分類できる。

集中型VCSでは、1つのリポジトリを1箇所で管理する。作業ディレクトリのファイル/ディレクトリの変更履歴をリポジトリに保存するのが「コミット (commit)」と呼ばれる操作である。リポジトリから(変更履歴を反映させた)ファイルやディレクトリを取り出すのが「チェックアウト (checkout)」と呼ばれる操作である。

一方、分散型VCSは、複数のリポジトリを複数の箇所で分散的に管理する。分散VCSではリポジトリ間で格納している変更履歴の同期をとることができる。ローカルリポジトリの変更履歴をリモートリポジトリ(別の場所にあるリポジトリ)へ送るのが「プッシュ (push)」という操作であり、その逆が「プル (pull)」という操作である。リモートリポジトリをローカルにコピーするのが「クローン (clone)」という操作である。

分散型バージョン管理ソフトウェアGit_

Gitは分散VCSの一種である。Gitは、Linux kernelのソースコード管理のために提案・開発されているバージョン管理システムであり、ここ2~3年ではもっともよく利用されている分散VCSである。

リポジトリ間の通信方式として以下のものを用意している。

  • ファイルシステム(同一計算機上の場合)
  • SSH (Secure Shell)
  • Git専用プロトコル(認証機能なし)
  • HTTP/HTTPS(WebDAV)(pushするためにはWebDAVが必要)

ネットワークに接続できないときでも、リポジトリに変更履歴を保存できるという利点の他に、集中VCSでは大変な作業であったブランチ管理を簡単に行えるという利点がある。

ブランチ管理_

変更履歴の分岐のことを「ブランチ (branch)」という。Gitにおいてはブランチはブランチを管理する操作名でもある。

ブランチを1つにまとめる操作を「マージ(merge)」という。ブランチ間で変更履歴に矛盾がない場合は、マージは問題なく成功するが、矛盾がある場合(同じ部分に異なる変更がほどこされている場合)は、「衝突 (conflict)」が発生したとみなされ、編集者による衝突の解決が必要となる。

Gitの良く使われるコマンドとリポジトリ、インデックス、作業ツリーの関係_

Gitコマンドの練習_

以下の作業はGitがインストール済み(サーチパスも通してある)のLinuxでの作業と仮定する。

まず、作業ディレクトリを作ります。

% cd
% mkdir GitSandbox
% cd GitSandbox

リポジトリの作成 (init, config)_

gitには「作業ツリー+リポジトリ」と「リポジトリのみ」の作成方法がある。まず、頻繁に利用する「作業ツリー+リポジトリ」を作成する。

% mkdir firstrepo
% cd firstrepo
% git init

つづいて、このリポジトリにおけるユーザ名とメールアドレスを設定します。Gitでは編集履歴に「誰が」編集したのかを記録するため、ユーザ名とメールアドレスの設定が不可欠である。自分の名前と自分のメールアドレスに置き換えて入力すること。

% git config user.name "Yuichi Goto"
% git config user.email gotoh@aise.ics.saitama-u.ac.jp

このリポジトリだけでなく、このコンピュータのアカウントで共通に設定したい場合は global オプションを使う。

% git config --global user.name "Yuichi Goto"
% git config --global user.email gotoh@aise.ics.saitama-u.ac.jp

ファイルの追加 (add)_

変更履歴をリポジトリに保管するために、Indexに保管対象のファイルを明示的に指定する必要がある。これは add コマンドを使う。以下の例では、新たに作成した Readme.md をGitでの管理対象にし、かつ、Indexへ登録している。

% touch Readme.md

% git status   (現在の変更対象、Indexへの追加状況を表示するコマンド)
# On branch master
#
# Initial commit
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#	Readme.md
nothing added to commit but untracked files present (use "git add" to track)

% git add Readme.md
% git status
# On branch master
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
#
#	new file:   Readme.md
#

既に管理対象になっているファイルであっても、変更をほどこしたならば add コマンドで Indexに追加する必要がある(後述)

変更履歴のリポジトリへの保存(commit)_

Index に列挙されているファイルの変更履歴をリポジトリへ保存する。この操作を commit という。Gitでは commit を行う場合に必ずコメントを登録しなければならない。今回はコメントとして「First commit.」と入力する。

% git commit (コメント追加のためのエディタが起動する)
[master (root-commit) febc49e] First Commit.
 0 files changed
 create mode 100644 Readme.md

commitにオプションをつけることで、addコマンドを省いたり、コメントをエディタを開かずにつけることができる。

% git commit (コメント追加のためのエディタが起動する)
% git commit -m "コメント" (一言コメントであれば-mオプションをつけると便利)
% git commit -a (既に管理対象になっており、かつ、編集済のファイルをコミットする)
% git commit -a -m "コメント" (-m と -a は同時に使うこともできる)

たとえば、Readme.mdを以下のように編集する。

## Introduction

Hello, World!

## Conclustion

Bye, bye!

すると、ファイルに編集済みであるが、Indexに追加されていない状態になっているのがわかる。

% 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:   Readme.md
#
no changes added to commit (use "git add" and/or "git commit -a")

本来は以下のようにコマンドを実行し、変更履歴をリポジトリに格納すべきである。

% git add Readme.md
% git commit

上をまとめて実行すると以下のようになる。

% git commit -a -m "Second commit."
[master d8dd1fc] Second commit.
 1 file changed, 7 insertions(+)

変更履歴の閲覧(log)_

変更履歴を閲覧する場合は log コマンドを使う。「commit」の横に表示されているのがハッシュキー。リビジョンを区別するためのIDとして使われている。「Author」が編集者、「Date」が編集日時、そして、その下には commit 実行時のコメントが表示される。

% git log
commit d8dd1fc15a5594bbeee12ef8a076fa0a7c8b9802
Author: Yuichi Goto <gotoh@aise.ics.saitama-u.ac.jp>
Date:   Tue Jun 2 13:17:41 2015 +0900

    Second commit.

commit febc49eac79a7c7525122e8166241060a89aa01d
Author: Yuichi Goto <gotoh@aise.ics.saitama-u.ac.jp>
Date:   Tue Jun 2 13:12:31 2015 +0900

    First Commit.

任意の時点との差分の表示(diff)_

Readme.md を以下のように変更する。

## Introduction

Hello, World!

## Section 1

An apple is a fruit.

## Conclustion

Bye, bye!

そして、Indexに登録する。

% git add Readme.md

さらにReadme.md を以下のように変更する。

## Introduction

Hello, World!

## Section 1

An apple is a fruit.

## Section 2

A dog is not a fruit.

## Conclustion

Bye, bye!

これで、Readme.md について、現在の状態、Indexに登録されている状態、リポジトリに保存されている最新のリビジョンが異なっている状況ができた。現在の当該ファイルと過去の状態の差分を調べるためには diff コマンドを利用する。オプションなしの場合は、Indexに列挙されている状態と現在の状態の差分を表示する。

% git diff Readme.md   
diff --git a/Readme.md b/Readme.md
index fe3df5c..ab37b41 100644
--- a/Readme.md
+++ b/Readme.md
@@ -6,6 +6,10 @@ Hello, World!
 
 An apple is a fruit.
 
+## Section 2
+
+A dog is not a fruit.
+
 ## Conclusion
 
 Bye, bye!

cachedオプションをつけるとIndexとHEAD(リポジトリに登録された最新の変更履歴)との差分を表示する。

% git diff --cached Readme.md
diff --git a/Readme.md b/Readme.md
index 4c4b7d0..fe3df5c 100644
--- a/Readme.md
+++ b/Readme.md
@@ -2,6 +2,10 @@
 
 Hello, World!
 
+## Section 1
+
+An apple is a fruit.
+
 ## Conclusion
 
 Bye, bye!

オプションなしで、HEADを指定すると現在のファイルとHEADの差分を表示する。

% git diff HEAD Readme.md    
diff --git a/Readme.md b/Readme.md
index 4c4b7d0..ab37b41 100644
--- a/Readme.md
+++ b/Readme.md
@@ -2,6 +2,14 @@
 
 Hello, World!
 
+## Section 1
+
+An apple is a fruit.
+
+## Section 2
+
+A dog is not a fruit.
+
 ## Conclusion
 
 Bye, bye!

任意のリビジョンと比較する場合はコミットIDを指定する。最初のコミット「febc49eac79a7c7525122e8166241060a89aa01d」時点でのReadme.txtと現在のファイルの差分を表示する。

% git log
commit d8dd1fc15a5594bbeee12ef8a076fa0a7c8b9802
Author: Yuichi Goto <gotoh@aise.ics.saitama-u.ac.jp>
Date:   Tue Jun 2 13:17:41 2015 +0900

    Second commit.

commit febc49eac79a7c7525122e8166241060a89aa01d
Author: Yuichi Goto <gotoh@aise.ics.saitama-u.ac.jp>
Date:   Tue Jun 2 13:12:31 2015 +0900

    First Commit.

% git diff febc49eac79a7c7525122e8166241060a89aa01d Readme.md 
diff --git a/Readme.md b/Readme.md
index e69de29..ab37b41 100644
--- a/Readme.md
+++ b/Readme.md
@@ -0,0 +1,15 @@
+## Introduction
+
+Hello, World!
+
+## Section 1
+
+An apple is a fruit.
+
+## Section 2
+
+A dog is not a fruit.
+
+## Conclusion
+
+Bye, bye!

一旦、commit する。

% git commit -a -m "Third commit"

ファイル名の変更、移動および削除(mv, rm)_

たとえば、新たに tmp_dir/newfile を追加するとする。

% mkdir tmp_dir
% touch tmp_dir/newfile
% git add tmp_dir/newfile
% git commit -m "Add new file."

一度、リポジトリに登録したあとにこのディレクトリ名やファイル名を変更したい、また、ファイルの場所を移動させたいときには mv コマンドを使う。

% git mv tmp_dir renamed_dir
% git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#	renamed:    tmp_dir/newfile -> renamed_dir/newfile
#

% git mv renamed_dir/newfile ./newfile
% git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#	renamed:    tmp_dir/newfile -> newfile
#

% git commit -a -m "Renamed and moved a directory and a file."

なお、Gitでは空のディレクトリは管理対象から外される。

ファイルやディレクトリを削除するときには rm コマンドを使う。

% git rm newfile
% git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#	deleted:    newfile
#

% git commit -a -m "Removed a file."

ブランチの確認、作成、名前変更、削除 (branch)_

ブランチを作成する。status コマンドのコメントで「On branch master」という表示があったが、デフォルトのブランチは「master」ブランチである。個人でソフトウェアを開発し、かつ、Gitを使って管理する場合には GitHub flowが便利だと思う。

GitHub flowは master(稼働し、公開できるブランチ), dev(開発用ブランチ), その他トピックブランチ(機能追加やバグ修正ごとに作成するブランチ)の3種類のブランチを運用するブランチ管理モデルである。

ブランチの一覧を表示する場合は branch コマンドを使う。アスタリスクがついているブランチが現在のブランチ。

% git branch 
* master

新たに dev ブランチを作成する。

% git branch dev
% git branch
  dev
* master

トピックブランチ「tmp_b」を作成する。

% git branch tmp_b
% git branch
  dev
* master
  tmp_b

ブランチ名を変えたい場合はmオプションを使う。

% git branch -m tmp_b tmp_c
% git branch
  dev
* master
  tmp_c

ブランチを削除する場合はdオプションを使う

% git branch -d tmp_c
Deleted branch tmp_c (was 7bb4c22).

% git branch
  dev
* master

ブランチ間の移動 (checkout)_

まず、masterブランチで新たにファイルを付け加える。

% touch unmarged_file
% git add unmarged_file
% git commit -m "Add a file."
% ls 
Readme.md  unmarged_file

dev ブランチに移動する。移動は checkout コマンドを使う。

% git checkout dev
Switched to branch 'dev'

% ls
Readme.md

この dev ブランチは unmarged_file を追加する前のリビジョンから分岐しているため、unmarged_file はこのブランチには含まれない。このようにGit はブランチごとに作業ツリーをリポジトリから構築する。

他ブランチの変更履歴の現ブランチへの反映 (merge)_

他ブランチの変更履歴を現ブランチへ反映させる際には merge コマンドを使う。

% git merge master
Updating 7bb4c22..caa8706
Fast-forward
 0 files changed
 create mode 100644 unmarged_file

unmarged_file の merge の綴りがまちがっているので修正してコミットする。

% git mv unmarged_file unmerged_file
% git commit -m "Modified spell miss."
% ls

master ブランチへ戻る。

% git checkout master
% git branch
% ls

dev ブランチの変更履歴をmaster ブランチへ反映させる。

% git merge dev
% ls

競合・衝突(conflict)とその解消_

unmerged_file の中身を以下のように変更する。

## May be conflict.
Hello.

コミットする。

% git commit -a -m "Edit unmerged_file."

dev ブランチへ移動する。

% git checkout dev
% git branch

unmerged_file の中身を以下のように変更する。

## May be conflict.
Good night.

コミットする。

% git commit -a -m "Edit unmerged_file."

master ブランチの変更履歴を devブランチに反映させる。すると衝突が起こっていることが報告される。

% git merge master
Auto-merging unmerged_file
CONFLICT (content): Merge conflict in unmerged_file
Automatic merge failed; fix conflicts and then commit the result.

unmerged_file は以下のようになっている。

% more unmerged_file 
## May be conflict
<<<<<<< HEAD
Good night.
=======
Hello.
>>>>>>> master

このように変更間に矛盾が発生し、Gitでは機械的に解決できないとき衝突が報告される。この場合は、人間が衝突を解消してあげないといけない。

衝突解決用ツールがインストールされている場合は mergetool コマンドで解決用ツールが立ち上がるがインストールされていない場合は以下のようなメッセージが表示される。

% git mergetool
merge tool candidates: opendiff kdiff3 tkdiff xxdiff meld tortoisemerge gvimdiff diffuse ecmerge p4merge araxis bc3 emerge vimdiff
No known merge resolution program available.

unmerged_file を以下のように修正する。

## Solved the conflict.
Good night.
Hello.

コミットする。Gitが勝手にコメントをつけてくれる。

% git commit -a

master ブランチに戻り、mergeする。

% git checkout master
% git merge dev

作業ツリーなしリポジトリの作成(init --bare)_

リポジトリ間の操作を練習するために二つ目のリポジトリを作業ツリーなしで作成する。

% cd ~/GitSandbox
% mkdir secondrepo
% cd secondrepo
% git init --bare

以後、この secondrepo をリモートリポジトリとしてリポジトリ間の同期の練習を行なう。

リモートリポジトリの設定(remote)_

ここまで、 firstrepo ではリモートリポジトリを設定していなかった。リモートリポジトリの設定は remote コマンドを用いて行なう。まず、現在の設定を確認する。今のところリモートリポジトリは設定されていないので git remote -v コマンド何も表示されない。

% cd ../firstrepo
% git remote -v

先ほど作成した secondrepo をリモートリポジトリとして設定する。リモートリポジトリは複数個設定することができる。標準で使うリポジトリは origin という名前をつけて管理することになっている。secondrepo は同一コンピュータ上に存在するのでファイルアクセスでリポジトリにアクセスする。

% git remote add origin file:///home/gotoh/GitSandbox/secondrepo

# git remote addの書式
% git remote add リポジトリ名 リモートリポジトリのURL(file://, ssh://, git://, https://, http://)

正しくリモートリポジトリが設定できているかを確認する。

% git remote -v
origin	file:///home/gotoh/GitSandbox/secondrepo (fetch)
origin	file:///home/gotoh/GitSandbox/secondrepo (push)

もし、リモートリポジトリのURL(今回の場合はリポジトリのある場所の絶対パス表記)が間違っているならば、以下のコマンドで修正する。

% git remote set-url リポジトリ名 リモートリポジトリのURL

リモートリポジトリのリポジトリ名を間違えた場合は、以下のコマンドでリポジトリ名を修正する。

% git remote rename 現在のリモートリポジトリ名 新しいリポートリポジトリ名

リモートリポジトリにローカルリポジトリの内容を反映させる(push)_

以下のように実行する。

% git push origin master
% git push origin dev

## pushの書式
% git push リモートリポジトリ名 ブランチ名

これで、firstrepoのmasterブランチとdevブランチがsecondrepo に反映された。

リモートリポジトリのコピー(clone)_

すでに存在するリモートリポジトリの内容をコピーするときに clone コマンドを使う。cloneコマンドで3つめのリポジトリを作成する。cloneコマンドは標準では、コピー元のディレクトリ名と同じ名前のディレクトリ名でコピーしたリポジトリを作成してしまう(つまり、この例題だと GitSandbox 以下でcloneコマンドを実行すると失敗してしまう)。なので、この例題では作業用ディレクトリを用意している。

% cd ..
% pwd
/home/gotoh/GitSandbox

% mkdir workspace
% cd workspace
% git clone file:///home/gotoh/GitSandbox/secondrepo
% ls
% cd secondrepo

clone コマンドでコピーしてきたリポジトリでは、リモートリポジトリはコピー元のリポジトリになっている。

% git remote -v
origin	file:///home/gotoh/GitSandbox/secondrepo (fetch)
origin	file:///home/gotoh/GitSandbox/secondrepo (push)

ブランチは標準でmaster だけコピーされる。

% git branch
* master

リモートリポジトリに存在するブランチも見たい場合は、branch コマンドに -a オプションをつける。

% git branch -a  
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/dev
  remotes/origin/master

リモートリポジトリに存在するブランチに対して checkout するとローカルにリモートのブランチがコピーされる。

% git checkout dev
Branch dev set up to track remote branch dev from origin.
Switched to a new branch 'dev'

Readme.mdにSection 3を付け加える。

## Introduction

Hello, world!

## Section 1

An apple is a fruit.

## Section 2

A dog is not a fruit.

## Section 3

Everything is a fruit.

## Conclusion

Bye, bye!

commitして、リモートリポジトリへpushする。

% git commit -a -m "Edit a file in third repository."
% git push    
Counting objects: 5, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 359 bytes, done.
Total 3 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
To file:///home/gotoh/GitSandbox/secondrepo
   2dda54e..ff211e8  dev -> dev

リモートリポジトリの内容をローカルリポジトリへ反映させる(pull)_

ここまでで、secondrepoの内容は firstrepo よりもリビジョンが進んでいる。そこで、secondrepo の内容を firstrepo へ反映させる。リモートリポジトリの内容の反映は pull コマンドを用いる。

% cd ~/GitSandbox/firstrepo
% git branch
  dev
* master

% git pull origin master
From file:///home/gotoh/GitSandbox/secondrepo
 * branch            master     -> FETCH_HEAD
Already up-to-date.

# pull コマンドの書式
% git pull リモートリポジトリ名 リモートリポジトリのブランチ名

現在、secondrepoとfirstrepoの内容は devブランチにおいてのみ違うため、masterブランチをpullしても何も変更はおこらない。

devブランチの変更をローカルリポジトリへ反映させる。

% git pull origin dev
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From file:///home/gotoh/GitSandbox/secondrepo
 * branch            dev        -> FETCH_HEAD
Updating 2dda54e..ff211e8
Fast-forward
 Readme.md |    4 ++++
 1 file changed, 4 insertions(+)

これで、2つ目のリポジトリを経由して、1つ目と3つ目のリポジトリの同期を行なうことができた。

ファイルの各行がどのコミットで変更されたのかを調べる(blame)_

どの行がどのコミット(リビジョン)で変更されたのかを調べる場合は blame コマンドを使う。

% git blame Readme.md             
a42166dc (Yuichi Goto 2015-06-02 23:37:05 +0900  1) ## Introduction
a42166dc (Yuichi Goto 2015-06-02 23:37:05 +0900  2) 
a42166dc (Yuichi Goto 2015-06-02 23:37:05 +0900  3) Hello, world!
a42166dc (Yuichi Goto 2015-06-02 23:37:05 +0900  4) 
fdca3887 (Yuichi Goto 2015-06-02 23:37:58 +0900  5) ## Section 1
fdca3887 (Yuichi Goto 2015-06-02 23:37:58 +0900  6) 
fdca3887 (Yuichi Goto 2015-06-02 23:37:58 +0900  7) An apple is a fruit.
fdca3887 (Yuichi Goto 2015-06-02 23:37:58 +0900  8) 
fdca3887 (Yuichi Goto 2015-06-02 23:37:58 +0900  9) ## Section 2
fdca3887 (Yuichi Goto 2015-06-02 23:37:58 +0900 10) 
fdca3887 (Yuichi Goto 2015-06-02 23:37:58 +0900 11) A dog is not a fruit.
fdca3887 (Yuichi Goto 2015-06-02 23:37:58 +0900 12) 
ff211e81 (Yuichi Goto 2015-06-03 00:24:15 +0900 13) ## Section 3
ff211e81 (Yuichi Goto 2015-06-03 00:24:15 +0900 14) 
ff211e81 (Yuichi Goto 2015-06-03 00:24:15 +0900 15) Everything is a fruit.
ff211e81 (Yuichi Goto 2015-06-03 00:24:15 +0900 16) 
a42166dc (Yuichi Goto 2015-06-02 23:37:05 +0900 17) ## Conclusion
a42166dc (Yuichi Goto 2015-06-02 23:37:05 +0900 18) 
a42166dc (Yuichi Goto 2015-06-02 23:37:05 +0900 19) Bye, bye!

ブランチをグラフ形式で見る(log --graph)_

どこで分岐してどこで合流したのかなど見ることができる。

% git log --graph --pretty=oneline
* ff211e81eb30414b7f63355db6d73ac09f97a74e Edit a file in third repository.
*   2dda54e6dd1369dad35fe8249180319ae7665f4f Merge branch 'master' into dev
|\  
| * 27220c536364a58dbcbedf87698929f522b46070 Edit unmerged_file.
* | c5dec9fb718a142d1e0792244313e5f2b7dee398 Edit unmerged_file.
|/  
* d22ba348daf8e6f207910c781f15bcff3db888b0 Modified spell miss.
* 9822d48618705a63ff18184d79678115977ddd31 Add a file.
* 33680717af4a062aa513273081046ebcb700fd1c Removed a file.
* 236ce3388b51c7676e4d0353651a1f724fd0d9ff Renamed and moved a directory and a f
* 145ad6d3f1cbee739b646412aff68cff9ebe8bbf Add new file.
* fdca38879780ec51f8ef919a96284ff1f68c09f1 Third commit.
* a42166dcbeb3dae2cb2f24941d51a8f3c13c2dd4 Second commit.
* e3df413968a558694d0901e827b6617d5dde8b80 First commit.

戻る_