開発未経験が書く　VCSの概念&Gitまとめ

#この記事について
この記事は、開発未経験の人間がインプットした内容が書かれています。
実際に開発経験を積まないと身につかない、かといって疎かにしたら後々痛い目にあう、といったジレンマを少しでも解消するために、「頭の中を言語化し、解釈違いを指摘してもらう」という手段を取ることとしました。
気になる点がありましたらツッコミをいただけると嬉しいです。

#記事の内容
VCSの基礎知識。Gitの運用の流れ。個人開発と複数人での開発の違いと注意点。

#インプット教材
introduction-to-vcs: https://github.com/masaru-b-cl/introduction-to-vcs
introduction-to-git: https://github.com/yamataku29/introduction-to-git

#開発環境

```
git version 2.21.0
```

#VSCとは
VCS（ヴァージョン管理システム/Version Control System）。ファイルやディレクトリの変更をバージョンとして管理する概念のこと。
####VSCの強み
* 履歴が残る
    * 作業した内容を記録として残せる。
    * その時点の状態に戻すことができる。
    * 別の作業をするために分岐することができる。
* 競合を防ぐ<br>
他の作業者の変更内容を消して自分が行った変更内容を反映することができないので、競合による不具合の発生を防ぐ。

####気をつけるべきこと
以下の項目に注意し履歴を残す。

* 時間ではなく作業単位で行う
* 変更する理由を明記する（変更内容は自動で記録されるが、変更した理由は明記しないとわからない）
* 動かないものは履歴に残さない（あくまで成果物として残す）
* 一つの履歴に一つの変更（複数の履歴を一度に残すと、それぞれの変更箇所がどの仕様によるものかが分からない）

#Gitとは
VCSの一つで分散VCSと呼ばれている。作業者全員が一つのリポジトリを共有するVCSに対し、分散VCSは各人にレポジトリがあり、作業者間で変更内容を共有したりできる。また、競合が発生したらエラーで教えてくれる機能がある。

#Git運用
##リポジトリの作成から最初のコミットまで

* my_first_workspaceという作業ディレクトリに対応するリポジトリは、 my_first_workspace/.git に作られる

```
$ mkdir introduction-to-git
$ cd introduction-to-git/
$ mkdir my_first_workspace
$ cd ./my_first_workspace/
$ git init
Initialized empty Git repository in /Users/username/introduction-to-git/my_first_workspace/.git/
$ ls -a
.	..	.git ←これ
```

* 作業ディレクトリで作業しただけでは、コミットしたときに Git はその変更内容をリポジトリに登録していいのかどうかわからないので、`git add` などのコマンドをつかって、変更したファイルをstage areaに置く(staging)

nyan.txtを新規作成し、`git status`でgitの状態を確認。

```
$ vim nyan.txt
$ git status
On branch master

No commits yet  "まだコミットされていない"

Untracked files "追跡されていないファイル":
  (use "git add <file>..." to include in what will be committed)
　"git add <file>を使ってコミットできる"
	nyan.txt

nothing added to commit but untracked files present (use "git add" to track)
"コミットできるものはないが、追跡されてないファイルがある"
```

`git add`を実行し、再度gitの状態を確認。
一度ステージングされたファイルを却下したい場合は、`git rm --cached`などのコマンドを使ってstage areaから下ろす（unstageing）ことができる。

```
$ git add nyan.txt
$ git status
On branch master

No commits yet

Changes to be committed:  "コミットできる変更箇所"
  (use "git rm --cached <file>..." to unstage)
  "use git rm --caches <file> でunstage"
	new file:   nyan.txt
```

git commit を行うと、エディタが立ち上がるのでコミットメッセージを書いて保存、終了する。すると、stage areaに上がっていた変更内容がコミットメッセージとともにリポジトリに反映される。このとき、staging されていたファイルの内容とコミットメッセージは「コミットオブジェクト」としてリポジトリ内に保存されている。


コミットメッセージを入力してコミット実行。その後、gitの状態確認。

```
$ git commit -m "nyam.txt 新規作成"
[master (root-commit) ef83816] nyam.txt 新規作成
 1 file changed, 1 insertion(+)  "1ファイル変更、1行変更"
 create mode 100644 nyan.txt

$ git status
On branch master
nothing to commit, working tree clean  "コミットするものはない、稼働ツリーはクリーンな状態"
```

##コミットされるときの内部の動き
コミット時、リポジトリには新しい「コミックオブジェクト」が生成される。
コミックオブジェクトには、ステージングされたファイルの内容　＋　コミットメッセージ　が格納されている。
コミットオブジェクトにはIDがあり、`git log`等のコマンドで確認できる。
`commit`の後に続く長いコードがIDである。

```
$ git log       
commit ef83816cf5a8f0f5e18cf6b2cf692d0fd1bd114b (HEAD -> master)
Author: YUUSUKE <yuusuke@example.com>
Date:   Thu Feb 25 10:31:46 2021 +0900

    nyam.txt 新規作成

```

##コミットの親子関係

* コミットには親子関係がある
変更前のコミットが親、変更後のコミットが子、という関係がある。親の内容と子の内容の差分が変更点として記録されている。

* コミットオブジェクトには「親コミットはどれか」という情報と、「この瞬間のファイルの状態」という情報が入っている。

nyan.txtの中身のテキストを「nyan」から「mew」へ変更。その後、`git log --graph`で確認。
親コミット（変更前のコミット）と比べると、"nyan"というテキストの行が無くなり、"mew"というテキストの行が増えた（＝変更された）。

```
$ git commit -am "nyan.txt の中身をmewに変更"
[master ae3cd98] nyan.txt の中身をmewに変更
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git log
commit ae3cd98b4d5f6b5100d4cb2f46ee94576e75cc02 (HEAD -> master)
Author: YUUSUKE <yuusuke@example.com>
Date:   Thu Feb 25 10:55:41 2021 +0900

    nyan.txt の中身をmewに変更

commit 87bdcd5c72a7c50fe412977535507120d032abe7
Author: YUUSUKE <yuusuke@example.com>
Date:   Thu Feb 25 10:31:46 2021 +0900

    nyan.txt 新規作成
```
* これらのふたつの情報を比べることによって、Gitはいつでも「その2つのコミットの間にどのような変更が行われたか」を知ることができる。

##gitで管理しているファイルのリネーム、移動
* ファイルのリネームとは、名前を変更しているだけのように見えるが、実際の動きは変更前のファイルを削除し、変更後のファイルを新しく作成している。また、ファイルの移動も、移動元にあるファイルが消え、移動先にファイルが作成されている。
* ファイルをリネームや移動を行う際は、すでにgitの管理下にあるファイルについては本来のコマンドの前にgitを入れる。そうすると、「gitの管理下のファイルを操作する」という処理になり、直接コミットができる。gitを入れ忘れると、変更後のファイルがUntrack（gitの管理下にない）なファイルとみなされ、再度stagingが必要になる。

nyan.txtとwan.txtのファイル名を変更。

```
$ git mv nyan.txt mew.txt   gitを入れて操作するとそのままstagingされる。
$ git mv wan.txt baw.txt
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	renamed:    wan.txt -> baw.txt
	renamed:    nyan.txt -> mew.txt

$ git commit -m "ファイル名変更"
[master 656f0db] ファイル名変更
 2 files changed, 0 insertions(+), 0 deletions(-)
 rename wan.txt => baw.txt (100%)
 rename nyan.txt => mew.txt (100%)
```

##brunch -ブランチ-

* git のブランチは「コミットオブジェクトを指し示すポインター」である
 
コミットオブジェクトを一覧表示する。一番上の（最新の）コミットオブジェクトにmasterブランチが指し示されている。

```
$ git log --oneline
656f0db (HEAD -> master) ファイル名変更
bb7330b 削除依頼が来たためboo.txtを削除
d42d145 wan.txt, boo.txt作成
ae3cd98 nyan.txt の中身をmewに変更
87bdcd5 nyan.txt 新規作成
```
* リポジトリには最初から「master」というブランチが存在している。
リポジトリを作成した時にGitが自動で作成している。
* HEAD という特殊なブランチも存在していて、このブランチは「現在選択しているブランチ」を指している
* `git branch new_branch_name` とすることで、新しいブランチを new_bnrach_name という名前で作成することができる。作成時に現在選択しているブランチが指しているのと同じコミットオブジェクトを指す。
 
新しいブランチmy_first_branchを作成し移動する。ログを確認すると、現在指定しているブランチであることを示す「HEAD」が新しいブランチを指している。

```
$ git branch my_first_branch   ブランチ作成
$ git checkout my_first_branch　　ブランチに移動
Switched to branch 'my_first_branch'
$ git branch　　ブランチの一覧と現在指定しているブランチを表示
  master
* my_first_branch

$ git log --oneline
656f0db (HEAD -> my_first_branch, master) ファイル名変更
bb7330b 削除依頼が来たためboo.txtを削除
d42d145 wan.txt, boo.txt作成
ae3cd98 nyan.txt の中身をmewに変更
87bdcd5 nyan.txt 新規作成
```

* 新しくコミットすると、現在のブランチがそのコミットオブジェクトを指すようになり、コミット前に指していたコミットオブジェクトが親となる。

masterブランチ、my_first_branchブランチでそれぞれ作業を実施。作業したコミットオブジェクトにそれぞれのブランチが指されている。また、「ファイル名変更」のコミットオブジェクトが親となり、そこから分岐しているのがわかる。

```
$ git graph
* bd85cc0  (HEAD -> my_first_branch) 猫の鳴き声を増やした
| * 03e1092  (master) 犬の鳴き声を追加
|/  
* 656f0db  ファイル名変更
* bb7330b  削除依頼が来たためboo.txtを削除
* d42d145  wan.txt, boo.txt作成
* ae3cd98  nyan.txt の中身をmewに変更
* 87bdcd5  nyan.txt 新規作成
```

##marge -マージ-
* `git merge branch_name` で、今選択されているブランチに branch_name という名前のブランチのコミット内容を取り込むことができる

masterブランチが指しているコミットオブジェクトにmy_first_branchブランチが指しているコミットオブジェクトをマージする。

```
$ git co master
Switched to branch 'master'
$ git merge my_first_branch
Merge made by the 'recursive' strategy.
 mew.txt | 1 +
 1 file changed, 1 insertion(+)
```

* マージするときには「マージコミット」という特殊なコミットが行われ、マージコミットのコミットオブジェクトは親をふたつ持つ

「犬の鳴き声を追加」と「猫の鳴き声を増やした」の2つのコミットオブジェクトの内容が混ざったマージオブジェクトが完成。

```
$ git graph
*   5965361  (HEAD -> master) Merge branch 'my_first_branch'
|\  
| * bd85cc0  (my_first_branch) 猫の鳴き声を増やした
* | 03e1092  犬の鳴き声を追加
|/  
* 656f0db  ファイル名変更
* bb7330b  削除依頼が来たためboo.txtを削除
* d42d145  wan.txt, boo.txt作成
* ae3cd98  nyan.txt の中身をmewに変更
* 87bdcd5  nyan.txt 新規作成
```

* `git branch -d branch_name` とすることで、branch_name という名前のブランチを消すことができる

```
$ git br -d my_first_branch
Deleted branch my_first_branch (was bd85cc0).

$ git br
* master
```

* masterブランチはデータの最終リリースとして保存した方が良い。なにか作業を行うときはmaster以外のブランチで作業する。
 
新しい作業用ディレクトリmy_second_workspaceを作成。
猫の愛でるファイルと猫を嫌うファイルを作成しコミット（master）。
修正する必要があるため、新しいブランチunify_stylesを作成し、そこで修正する。

```
$ mkdir my_second_workspace
$ cd my_second_workspace/
$ git init
Initialized empty Git repository in /Users/username/introduction-to-git/my_second_workspace/.git/

$ vim cat_lover_said.txt
$ vim cat_hater_said.txt
$ git add .
$ git c
[master (root-commit) 4d50277] 猫好きの話と犬好きの話を作成
 2 files changed, 32 insertions(+)
 create mode 100644 cat_hater_said.txt
 create mode 100644 cat_lover_said.txt

$ git br unify_styles
$ git co unify_styles
Switched to branch 'unify_styles'

$ vim cat_lover_said.txt
$ vim cat_hater_said.txt
$ git c -am "文体を統一"
[unify_styles a6a4ce3] 文体を統一
 2 files changed, 6 insertions(+), 9 deletions(-)

```

* 「分岐」してないブランチをマージするときには Fast-foward という手法が取られる。Fast-foward だと、マージコミットは作られず、たんにブランチが進められる

2つのファイルの中身を見ると、文中に「頭おかしい」という表現があり、修正が必要。
新しいブランチhotfixを作成し、そこで修正。修正後masterブランチにマージ。
ログを確認すると、単にmasterブランチが移動しているのがわかる。

```
$ git co -b hotfix
Switched to a new branch 'hotfix'
$ git br
* hotfix
  master
  unify_styles

$ vim cat_hater_said.txt 
$ vim cat_lover_said.txt 
$ git c -am "頭がおかしいという表現はまずいので修正"
[hotfix 9cfca6a] 頭がおかしいという表現はまずいので修正
 2 files changed, 3 insertions(+), 3 deletions(-)

$ git graph
* 9cfca6a  (HEAD -> hotfix) 頭がおかしいという表現はまずいので修正
| * a6a4ce3  (unify_styles) 文体を統一
|/  
* 4d50277  (master) 猫好きの話と犬好きの話を作成

$ git co master
Switched to branch 'master'
$ git merge hotfix
Updating 4d50277..3aee339
Fast-forward
 cat_hater_said.txt | 2 +-
 cat_lover_said.txt | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

$ git graph
* 3aee339  (HEAD -> master, hotfix) 頭がおかしいという表現はまずいので修正
| * a6a4ce3  (unify_styles) 文体を統一
|/  
* 4d50277  猫好きの話と犬好きの話を作成

```
 
* Fast-foward したくないときには `--no-ff` というオプションを付ける

`git reset --hard commitID`で過去のコミットオブジェクトに戻る。
オプションをつけて再度マージする。

```
$ git reset --hard 4d50277
HEAD is now at 4d50277 猫好きの話と犬好きの話を作成

$ git graph
* 3aee339  (hotfix) 頭がおかしいという表現はまずいので修正
| * a6a4ce3  (unify_styles) 文体を統一
|/  
* 4d50277  (HEAD -> master) 猫好きの話と犬好きの話を作成

$ git merge --no-ff hotfix
Merge made by the 'recursive' strategy.
 cat_hater_said.txt | 2 +-
 cat_lover_said.txt | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

$ git graph
*   175944b  (HEAD -> master) Merge branch 'hotfix'
|\  
| * 3aee339  (hotfix) 頭がおかしいという表現はまずいので修正
|/  
| * a6a4ce3  (unify_styles) 文体を統一
|/  
* 4d50277  猫好きの話と犬好きの話を作成

```

* マージしたときに競合（conflict）が発生した場合には、手動でマージする

unify_stylesブランチでファイルを修正しコミット。
masterに戻り、unify_stylesをマージしようとしたらエラーが起きた。
unify_stylesブランチで修正したファイルの行とmasterにマージされたhotfixブランチで修正したファイルの行が同じであるため、競合が発生。

```
$ git co unify_styles
Switched to branch 'unify_styles'
$ vim cat_hater_said.txt 
$ git c -am ""ヘッヘッヘッヘッ" という表現は残すようにした"
[unify_styles 6119bc1] ヘッヘッヘッヘッ という表現は残すようにした
 1 file changed, 2 insertions(+), 1 deletion(-)

$ git co master
Switched to branch 'master'

$ git merge unify_styles
Auto-merging cat_lover_said.txt
CONFLICT (content): Merge conflict in cat_lover_said.txt
Auto-merging cat_hater_said.txt
CONFLICT (content): Merge conflict in cat_hater_said.txt
Automatic merge failed; fix conflicts and then commit the result.
```

ファイルを手動で修正しgit statusで確認。

```
$ vim cat_hater_said.txt 
$ vim cat_lover_said.txt 

$ git status
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:   マージされていないパス
  (use "git add <file>..." to mark resolution)  

	both modified:   cat_hater_said.txt
	both modified:   cat_lover_said.txt
```

`add`&`commit`でマージ完了。マージされていない状態の時はコミットすればマージも完了される。

```
$ git add .
$ git c
[master 70b3611] Merge branch 'unify_styles'

$ git graph
*   70b3611  (HEAD -> master) Merge branch 'unify_styles'
|\  
| * 6119bc1  (unify_styles) ヘッヘッヘッヘッ という表現は残すようにした
* |   175944b  Merge branch 'hotfix'
|\ \  
| * | 3aee339  頭がおかしいという表現はまずいので修正
|/ /  
| * a6a4ce3  文体を統一
|/  
* 4d50277  猫好きの話と犬好きの話を作成
```

##リモートリポジトリ
* 共有リポジトリを作成し運用する場合、自分のリポジトリのデータを反映したり、逆に共有リポジトリのデータを反映させるためには、共有リポジトリをリモートリポジトリとして登録する必要がある。
  * たかしさんとゆうすけさんのそれぞれの作業ディレクトリを作成し、お互いのデータを共有するためのリポジトリを作成。先にたかしさんの作業ディレクトリと作業ファイル、masterブランチとは別の開発用ブランチを作成し、そのクローンを共有リポジトリにする。
  * 共有リポジトリのクローンをゆうすけさんのディレクトリに作成。
  * ブランチまでは複製されていないため新しく作る必要があるが、リモートリポジトリのブランチorigin/branch_nameと関連付けないと反映したりできない。

```
gitを初期化し、ユーザー登録
$ mkdir takahashi
$ cd takahashi/
$ git init
Initialized empty Git repository in /Users/kudokoki/introduction-to-git/my_third_workspace/takahashi/.git/
$ git config --local user.name Takahashi
$ git config --local user.email takahashi@example.com

ファイル作成
$ vim cat_lovar_said.txt
$ git add .
$ git c
[master (root-commit) a38177b] 猫好きの話を追加
 1 file changed, 15 insertions(+)
 create mode 100644 cat_lovar_said.txt

開発用ブランチ
$ git br development

たかしさんのディレクトリをクローンし、共有リポジトリにする。
git clone <クローンしたいデータ> <クローンデータのパス>
共有リポジトリは直接いじらないので作業ディレクトリは不要。
--bare オプションはリポジトリのみを複製できる。
$ cd ../
$ git clone --bare ./takahashi/ ./shared_repo.git
Cloning into bare repository './shared_repo.git'...
done.

共有リポジトリのクローンをゆうすけさんのディレクトリに生成
--bareをつけずにクローンすると作業ディレクトリも生成される
$ git clone ./shared_repo.git/ ./yuusuke
Cloning into './yuusuke'...
done.

developmentブランチはクローンされない
$ git br
* master
$ git graph
a38177b  (HEAD -> master, origin/master, origin/development, origin/HEAD) 猫好きの話を追加

developmentブランチをリモートリポジトリのブランチと関連付けて作成
git branch <作成したいブランチ名> <関連付けたいブランチ名>
クローンした時に、デフォルトのリモート登録名はoriginになっている
$ git br development origin/development
Branch 'development' set up to track remote branch 'development' from 'origin'.
```
* クローン元のリポジトリはクローン先のそれと関連付けられていないため、別途登録する必要がある。
  * リモートリポジトリの登録
  * リモートリポジトリのブランチを生成
  * リモートリポジトリのブランチと関連付ける

```
リモートリポジトリの登録
git remote add <登録名> <リモート先のパス>
$ git remote add origin ../shared_repo.git
$ git remote
origin

ブランチ作成
git fetch <登録名>　でリモートレポジトリのブランチを呼び出して生成
$ git fetch origin
From ../shared_repo
 * [new branch]      development -> origin/development
 * [new branch]      master      -> origin/master

リモートリポジトリのブランチと関連付ける
git branch --set-upstream-to=<リモートリポジトリのブランチ> <関連付けたいブランチ>
$ git branch --set-upstream-to=origin/master master
Branch 'master' set up to track remote branch 'master' from 'origin'.
$ git branch --set-upstream-to=origin/development development
Branch 'development' set up to track remote branch 'development' from 'origin'.
```

##pushとpull
* リモートリポジトリに手元の変更を反映したいなら、`git push`
* 追跡ブランチに対しての変更はそのまま `git push`
新しいブランチを作りたいなら `git push ＜リモートリポジトリの名前＞ ＜手元のブランチ＞:＜リモートのブランチの名前＞`
* ブランチを削除したいなら `git push ＜リモートリポジトリの名前＞ :＜リモートのブランチの名前＞`
  * リモートリポジトリから手元に変更を持ってきたいなら `git fetch`
  * リモートリポジトリのコミットとブランチを持ってくる
  * リモートリポジトリのブランチはリモートブランチとして作られる
* fetch のついでに merge も一緒にやりたいなら、`git pull`
* `git pull`を成功するには以下の条件を満たす必要がある。
  * リモートリポジトリが、bare リポジトリである
  * リモートリポジトリへの書き込み権限を持っている
  * 手元のリモートブランチがリモートリポジトリに「追いついて」いる
  * すでにリモートリポジトリに push 済みのコミットが、手元のリポジトリで改変されていない