Git

gitにハマらないために

各用語

  • リポジトリ:コードを管理する単位。管理するソースコードが1つのディレクトリにまとまっており、そのディレクトリ内のファイルの変更履歴が管理される。
  • コミット:コードの変更をまとめて確定したもの。変更は複数のファイルにまたがるときもある。
  • ステージ:「次にこれをコミットします」と選ばれた変更のひとまとまり。
  • ブランチ:コードの変更の流れが分岐した枝のこと。複数の開発者で同時に開発するための機能。
  • HEAD:各ブランチの最後のコミット。
  • masterブランチ:一番メインのブランチ。一般的に、共同開発している場合、masterブランチに直接変更を加えるのはよくない。
  • トピックブランチ:master以外のブランチ。masterから分岐して造られる。bug_fixとかrefactorとかadd_new_featureとか(さらにはもっと詳しく)開発トピックごとに作られるのが正しい。
  • マージ:他のブランチの変更をいまいるブランチに取り込む操作。共通の分岐点(ブランチベース)以後の変更が取り込まれる。取り込まれる側(参照側)には何も変更はない。マージも1つのコミットになる。
  • プルリクエスト(これはgithub用語):(主にmasterに)自分のブランチを取り込んで(マージして)くださいと依頼する。この時のマージはレビューが入り、web上で行われることが多い。

途中でgitをやめたくなったら

git管理にあるディレクトリの最上位(ルートディレクトリと呼ぶ)に.git/というディレクトリがある(通常、隠れている)。
この中に全ての履歴や情報が入っており、gitはこれを認識している。
この.git/ディレクトリを削除してしまえば、そこはgit管理化からはずれる。(もちろん、変更履歴も何もかも全て消える。手元のファイルはそのまま。)

初めてのgit

自分用のgitの設定 (git config)

ユーザ名とeメール

git config --global user.name {myusername}
git config --global user.email {myemail@address.com}

UIの設定(オススメ)

git config --global color.ui auto
git config --global color.diff auto
git config --global color.status auto
git config --global color.branch auto
git config --global core.quotepath false

便利系エイリアス(これもオススメ)

git config --global alias.loga "log --oneline --tags --graph --decorate --all"

これを入れておくと、git logaでgranchのグラフが見える。

リポジトリの作成

git init もしくは git clone {URL}

  • 手元のディレクトリをgitリポジトリにする場合にはgit init
  • すでにあるリポジトリをコピーするならgit clone
  • githubで新しく空リポジトリを作り、手元のディレクトリにgit cloneするのも良い(空っぽのリポジトリを今のディレクトリにcloneしても、手元のファイルは消されない)

最初のコミット

touch test.c ファイルを作成
git add test.c ステージング:これからコミットにのせる変更を選択。
git commit -m "first commit"   メッセージ付きでコミット

なおここまで順にやってくると、masterブランチにコミットしているはず。

共同開発(リモートにpushするまで)

リモートの追加

git remoteで追加する。
というか追加しなくても使えるので、「エイリアスを登録するコマンド」と捉えた方が正確。
なお、originというリモートだけは特別で、省略したときのデフォルト名になる。
どうせリモートリポジトリは一つだろうから、とりあえず登録しておこう。

git remote add origin git@github.com:yourname/your_project.git

これで、 git@github.com:yourname/your_project.git と originが等価。

一般的な開発の流儀

これは基本とされる割にはどこにも書いていない。

まず、masterからbranchを作成

git checkout -b new_feature

次に、作成したbranch上で開発、commitする。

echo "p 'hello world'" >> hello.rb
git add hello.rb
git commit -m "added hello.rb"

自分がつくったbranchなので遠慮なくpushする(最初は上手くいかない。すぐ後のupstreamで説明する)。

git push

(githubなど)webインタフェースから、masterにマージしてもらうための申請をする(プルリクエスト)。

upstreamとは

各ローカルブランチに、リモート側として対応付いたブランチをupstreamブランチという。
upstreamブランチにしかpushできない。push時に対応付けする。

git push --set-upstream origin new_feature

ローカルとブランチ名が一緒である必要はない(一緒の方がわかりやすいが)

git push --set-upstream origin my_new_feature

などでも可能。

なお、とりあえずgit pushを叩くと、上記コマンドを提示してくれる。
面倒な人はそれをコピペするとよい。
--set-upstreamのショートオプションは-uだが、ゆえにロングを表記した)

pushできない場合

(すでにupstreamブランチが存在する場合)upstreamブランチ上の変更が今いるローカルブランチに含まれていない場合、pushできない。

自分だけがpushしてるはずなのに、そんなことあるもんかと思うが、upstreamブランチは、作った人専用というわけではない。
うっかり他の人と同じ名前を使ってしまうと、衝突の危険性がある(なので、bug_fixのような単純な名前はやめよう)。
逆に、あえて同じリモートブランチで一緒に作業することもある。

ともあれ、pushしようとしている先のupstreamブランチ上のすべての変更を取り込んだ状態(fast-forwardと呼ぶ)にしないと、pushできないので、これを修正する必要がある。

共同開発(他の人が編集してpushしていた時の解決)

マージ

マージそのものはリモート関係ない。ローカルブランチ同士でもできる。
あくまで、相手側(指定したブランチ)の変更を、自分側(今いるブランチ)に取り込むこと。

マージをすると
- 手元のファイルが変更される
- コミットされる(マージコミットと呼ばれる)
- マージされたということがgitの記録に残る(これがとても大事。)

なお、ここでの「変更」というのは、「共通の分岐点」からの変更である。

共通の分岐点に対して、相手側も自分側も変更を加えていた場合、コンフリクトが起こる。
コンフリクトは自動で解決される場合もあるが、手動で解決しなければならない場合もある。

fetchとpull

  • fetchはリモート(upstream)ブランチからデータを取ってくる。
  • pullfetchmergeを同時に行う。

リモートブランチとトラッキングブランチ

  • fetchでupstreamブランチのデータをダウンロードする先がトラッキングブランチ。
  • つまりトラッキング(追跡)ブランチは、ローカルにある、リモートブランチのコピー(のようなもの)。
  • トラッキングブランチに対してコミットすることはできない。
  • fetchコマンドを叩くと、リモートブランチをトラッキングブランチに持ってくる(トラッキングをリモートと同期する)。
  • これを自分のところにmergeする。
  • pullコマンドを叩くと、fetchとmergeを同時にやってくれる。
  • pullがうまくいく(コンフリクトも解決される)と、今度はpushできるようになる。
  • マージはトラッキング → ローカル → リモート の三角形の形で行われる。

トラブルシューティング

pushができない場合

とにかく、git log --oneline --tags --graph --decorate --all (先ほど追加した場合は、git loga)を頻繁に確認しよう。

  • git reset HEAD^でうっかりHEADを戻していたりしないか? → pushでは葉の先にコミットを積む方向にしか進められない

  • うっかりミスgit-promptを入れよう。常に状態がわかる。

  • 最後の手段としてgit push -fというのがある。今の自分が正しいとして強制的にpushする。他の人が同じリモートブランチを参照している場合、一貫性が取れず状態がおかしくなるので、めっちゃ怒られるだろう。(push -fを禁止しているレポジトリもある。それくらいのご法度である)

コミットに関してよくある混乱

コミットは「差分」なのか、コミット時点での「スナップショット」なのか。

  • 内部処理では、「スナップショット」を保存している。
  • けれども、たいていの操作(merge, rebase, cherry-pick)では差分を扱っていると捉えた方が理解しやすい。
  • checkoutで別のコミットからファイルを持ってくるときだけは、「スナップショット」と考える必要がある。

間違えやすいコマンド

git reset

「ステージングを解除する」機能と、「HEADを過去のcommitまで移動する」(つまり、コミットを取り消す)という機能が混ざったように見える。
(実際は、ステージングとHEADを同時に移動する機能らしい。引数がないと、両者を現在のHEADに移動する(つまりHEADそのものは移動せず、ステージングだけ移動=解除となる))

両者とも、--hardをつけると実際のファイルまで変更するので、間違えると危険。

git add test.c   # test.cの変更がステージングされる。
git reset        # ステージングが解除される
git reset HEAD^        # HEADを一つ前のコミットに移動する。

git checkout

branchの切り替えと、別のbranchからのファイルを持ってくるのが混ざっている。
ファイルパスをつけるとファイルを持ってくる。

git checkout master   # ブランチがmasterに移動する
git checkout master hello.rb        # hello.rbというファイルをmasterから持ってくるだけ。ブランチは移動しない。