【Git】そろそろGitをしっかり理解しておきたい時向けの勘所を狙って説明してみるチャレンジ


このページでは、本質を端的に説明しようと思います。

以下が気になる人向け

Git初心者にはそこまで必要ではないと思うことはありますが、どちらかというとGit初心者のIT技術者が、どこで何をどう変えているの?という不安を解消することを目的に、以下がわかるまで整理してみました。

  • コマンドやデータ構造の意図
  • データ構造
  • コマンドが操作するデータ

 

ボリューム・読破時間

小冊子 30Pぐらい。 80分ぐらいかな?


説明内容

設計思想である目的ベースで
肝心な基本のデータ構造を抑えることを目標とします。

後半はデータ構造やデータの変更処理に関する情報を集めた時の設計や機能内容を参考資料として残しています。 
設計思想→  データ構造  →  コマンドが処理する内容(内部構造・中心となるデータ設計視点)

で記載しています。

あとは、
おまけ(設計思想から見たコマンドや状態の解釈の仕方例や注目すべきポイント)が途中にあって

最後はまとめ

※意図を飛ばしたい方は中盤ぐらいから読んでください。

大学の教授の教えで、「基礎とは簡単なことをさすのではなく、ベースとなる土台を指す言葉」というものは大事にしています。

   という言い訳をして  Let's GO!

 

 

 

Git コマンドは、必ずマニュアルを確認し、理解したもので対応してください。

 

 

 

git とは

前提条件として


ソース管理システム(SCM)とは
ソフトウェアを作るために、複数人🚶🚶🏽🚶🏿‍♂️でソースを作成し
それを融合するために管理するツールとする。




そういう条件下では


リーナス氏が結論付けたベースの考え方

マージが難しいことを認識し、基本マージはせず、
分散型オレオレプロジェクト👍🏼で進める形式が基本だ。

→分散にすることで
→ 自由に自分がわかる範囲で、好きな時に好きなだけ、好きなように開発できる。マージに関する手間や、それに向けてのブランチの構造についてはもう一考したいす。

リーナストーバルス氏がGitで本当に目指した大事なものの勘所(1/6)

 

 

 

 

 

使用想定ケースと設計方針のバックグランド

 

想定ケース・
バックグランド
目的 方針・設計・データ構造 実装された
コマンド
・ちょっと 開発してみる
・テストしてみる
・マージ確認
作業ディレクトリの内容を自由に変更したい
・戻せる
気楽に戻せること
コミット状態を復元できる。
コミットごとの実ファイルを取得できるようにした。
※1
・ちょっと 開発してみる
・テストしてみる
ブランチができる (省略)
素早い作業準備 ワーキング上で作業 ・開発
・テスト
・マージ、コンフリクト+テスト  ※2
分散バッチの開発と
取込み
作業ディレクトリの内容を自由に変更したい コミットごとの
スナップショットを利用できる
・各人が開発
・気軽なブランチ名や命名規則を使用
・環境を囲う

オレオレリポジトリ

分散ローカルレポジトリ

3wayマージ
※3
・コミットの履歴から分岐地点をたどれるように管理する
・ファイル単位、ファイル内で重ならないのであれば ツールで自動対応可能
git history管理
rebase
cherry-pick
データ破損のリスク いつでも気付けるように
(テスト時、マージ時の作業時間は節約したい)
diffではなく、ファイルはハッシュ値で管理し、時間の節約をする。
マージのコンフリクト対応 部分部分を確認したい 3wayマージのように、なるべく局所化して開発、テスト可能にする。※4
テスト 環境の構築 ワーキングディレクトリの整備 checkout
環境切り替えスピード コミットごとのファイルの実態を直に確保
※5
コミット単位のID
小さめの、管理単位:
ファイル単位で管理し、行単位で差分を取らない。
環境切り替えやりやすさ コミットの名前を付ける→ブランチ、タグ、ヘッダ
データ・プログラム確認ミス プログラミングミス対策 運用で対処:テスト/diff
HDDの破損🖥️対策 ハッシュ値を活用して検知
ファイル管理ミス対策 一部運用やハッシュ値で、差分チェックする
コミットしたつもり対策 ステージングで管理
(ステージング環境へ上げたつもりは、ファイル管理ミス欄のように対応)
変更したことを忘れていた対策 ステージング、diff
不要な変更が残っている対策 ステージング、diff
優秀な人でも間違う対策 ステージング、diff、テスト
想定外、意識外の影響、考慮漏れ対策 テスト

優秀でない人対策
基本 GIT外、テスト、ソース確認、コミュニケーションで

※1:コミットの管理方法について

ファイル名だけでは、このファイルという指定ができない。時間軸、ブランチなどの情報を載せるため、ファイル📄+時間🕓+ブランチ🌲の情報を表現できるインデックスを作る。→ファイルが多くても名前で重複せずに詳細な差分検知のできるハッシュと、コミット単位の管理方式ハッシュで対応する。

コミット単位の情報と、そのコミットがさすファイルの情報をつなげておく
人間がわかりやすい、ラベル名を付けられるようにする→HEAD(作業ベース), MASTER(メインブランチ)、ブランチ名(独自の試行状態)。

いつでもどこでも気軽にUNDOができるというのは、とても心理的にもリスクが少なく、分散と、ブランチと、マージがしやすくコミット単位を管理できていることにかかっている。

※2   ワーキングディレクトリで直接作業

svn,やcvsでもある程度やっているが、SCMがない場合
通常、あるディレクトリで new フォルダを管理したり、日付で圧縮されたファイルを管理したり、その下に bk や workフォルダを掘ったりして管理することが良くある。
一方Gitでは 一旦フォルダ構成を決めたところで、checkout , commit , diff , develop, test すべてがローカルで自分が見えてコントロールできる範囲でできるようになった。

コンフリクト💥が起きた時も、両方コミットされているため、ゆっくりとしっかり作業ができるし、コミット単位で差分やブランチベースまでの履歴も管理されているので、環境の構築が容易。相手側に依頼することもそれほど極端に難しいことではなくなってきている。

bare👶🏻レポジトリは、ワーキングディレクトリがない。
おそらくはソースなどを、システムで管理したい場合、github とか、shellでぶん回してコントロールするときに使うと思う。

※3   パッチの取り込み

ウォーターフォールではあまりないケースだが、OSSでは、バッチの順序が変わってしまうため、
なるべく変更範囲を局所的にしたい。そのため、3wayマージという方法と、コミットごとに変更範囲をたどれるようにしている。 補足2でもうちょっと グラフィカル(文字絵だけど)に説明

2wayの単純なdiff差分では、個別パッチの適用が難しい(変更差分の情報が一緒くたになっているから→共通の祖先の情報が必要)

2wayでは、分岐した2つの差分を取ると、自分が修正していない点も差分に表れることがあるように、変更の差異がどちらかに表れて差異情報が消えてしまう。つまり、変更されたとみなされる範囲が、広がってしまうためマージでコンフリクトしやすくなる。3wayのように共通の祖先があれば、どちらのブランチの流れでの、追加なのか、削除なのか、移動なのかたどれる。

#22 Gitメンテナ 濱野 純:小飼弾のアルファギークに逢いたい♥|gihyo.jp … 技術評論社

version control - Why is a 3-way merge advantageous over a 2-way merge? - Stack Overflow

マージ

最終的には中央で、行う必要があり、
スケールするためには、信頼のおける人を見つけていくことが必要になってくる。

そうすると、コミットに関して 特にOSSの分野では
色々な意味での好み(作法、テクニック、記載、タイミング、構造化)、
感情的な部分が出てきてしまう。

マージの困難さの一つはここにある。

また、別の理由からもブランチではなく、マージを重要視している。
SVNがそもそもダメだといった理由は、「難しいのはブランチしてコピーすることではなく
マージするときにおこることなので、簡単なブランチにフォーカスを当てるのではなく
マージにフォーカスを当てないと、プロジェクトが進まないといった大きな障害になりえる」と
リーナス氏は認識しています。

20億行のコードを保存し、毎日4万5000回のコミットを発行しているGoogleが、単一のリポジトリで全社のソースコードを管理している理由 - Publickey

分散バージョン管理で間違いないって、ベイビー - The Joel on Software Translation Project

分散バージョン管理では、マージは簡単で、うまく機能する。だから安定版ブランチと開発版ブランチを作ったり、あるいはデプロイ前にQAチームがテストするための長期間存続するブランチを作ったり、ちょっと新しいアイデアを試して具合を見るための短期的なブランチを作ったりすることができる。

 

※4 コンフリクトとが起きた時は、担当者にやってもらう

同ファイル内で不明であれば、運用上パッチ対応者に、再度マージしたいベースで再修正してもらう。  つまり 部分レポジトリやリモートとの関係性を保持する。

 

※5コミットごとのスナップショットを取り出せる

変更のない同じファイルについては、コミットごとの参照ポインタを持つことで、レポジトリサイズの拡大を抑制

コミット単位にスナップショットを取り出せる(さらにワーキングディレクトリに展開)
→checkout の機能
→データ構造は、実ファイルへの参照を取る形で、別コミットがあっても、ファイルの増加を抑える。
→マージを行うために、 より手間の少ない 3wayマージをするため、コミットの親関係をすべて保存する。
→マージを行うため、コミットごとの変更点を正確に把握して、マージの労力を減らす(スナップショットを取り出せることと矛盾してそうだが、コミット情報、実ファイルへの参照リンクによる実態の共有保存、ハッシュ値による際の高速チェックを利用して、それぞれの流れでの差分を例えば分岐し始めたオリジナルからたどれるようになっている)

行単位ではなく、ファイル単位なのはコミットを軸としたデータ構造だから。だからリリース版の管理がやりやすい。ほかのSCMではディレクトリ構造を中心としたデータ構造に引きずられているのでファイル中心の取り扱いになり、ファイル操作はしやすくとも版管理の手間が大きい。

補足1:コミットの発想の転換

その時々の動くリビジョンがスナップショットなので、スナップショット単位で管理できていれば、テストや確認、開発もろもろが素直に対応できる。一方ファイルに対して、コミット情報を付けた場合、そのコミット情報に合うファイルの情報を拾ってくるのは、とても大変な作業。

たしか、subversionやcvsなどは リビジョンのタイミングのAのファイルが3世代目とわかったらAファイルの3世代目を端から探して取り出すイメージ。

リビジョンを付けるときは、全ファイルに、n世代目だと情報を付けるイメージ。

 

水平から、垂直への転換。

 

 

補足2:個別パッチができようできるように分割するための下準備とデータ

また、スナップショットだけれども、履歴を管理することで、それぞれの変更の流れがよりつかめるようになる。2way/3wayコミットを通じて確認してみると。

Original
[a][b][c][d][e][f]

Patch X -> Original   : Original から  パッチXを適用
[a][b][c][d][X][f]

Main -> Original
[A][B][C][d][e][f]

 

上記の場合、2wayの差分を取ると[A][B][C][X]の差分が出てしまう。
しかし、履歴をたどれば、PatchXをmergeして取り込むとき、 Original との差分[X]なので
[X]部分だけ注目して取り込めばいいことになる。

これがあるおかげで、3way方式はマージのコンフリクトが起こりにくくなる。
誰が何を目的でいつ修正したかもわかるため、依頼したり話を聞きやすい。

また、subversionのようにファイル単位でコミットをとってしまうと、
すべての変更([a][b][c]を取り込むべきか)に対して
再確認してからでないとmeargeしていいかわからない形になる。
([A][B][C]を修正した人でさえ、毎回常に[a][b][c]を完全に覚えていられるわけではない。)


 

補足3:担保方針

gitはいったん、それぞれの意図(履歴から流れ)をくみ取れるため、マージをして
時間のかかっていたマージ確認を省き
最終的に、ステージング確認・テスト確認で担保を取る想定・方針となる。

まさにこれが cherry-pick, rebaseの機能で、 前身であるbitkeeperから改良した点でもある。これによりパッチの到着の前後性が起きても、比較的問題なく取り込めるケースが増大した。パッチを当てた側も例えコンフリクトが起きようとも、小さな自分が理解している範囲だけ確認すればいいので、最新版にマージする労力はとても小さい。

 

 

 

データ構造

gitはルートディレクトリから相対パスでデータを持っているようなので、ルートディレクトリを移動させても影響はないっぽい。何回かやってみて今のところ問題ない。

gitは 3つの状態を持つ

  • ワーキング中のソース
  • ステージング(cacheまたは index)対象となったソース
  • コミット 完了したソース




gitは ワーキングディレクトリで作業する。

 

  1. git checkout すると ワーキングディレクトリとステージング情報(コミットしたスナップショット情報が全ファイル展開される)を上書きする。
  2. git add すると コミット領域にファイルを登録して、ステージング情報を書き換える
  3. git commit すると、カレントのブランチ(HEADの指していた先の master や ブランチ)の情報が indexの情報に置き換えられ、今指していたカレントの情報を祖先情報として保持して親子関係の情報を更新する。→後述のコミットオブジェクト
※どの情報を書き換えるかは後述(データに対する処理影響範囲)

ソースを構成するディレクトリやプロジェクトのトップディレクトリで git initを初期化すると
そこがワーキングディレクトリのトップディレクトリとなり、トップディレクトリのみにある.gitという管理用フォルダ内に情報が保存される。

ワーキングディレクトリで作業開始するとき、checkoutして開始すると
ワーキングディレクトリをgitが書き換えてくれる/書き換えてしまう。

 

 

📑ラベル(masterなど)

trunkという概念はなく、branchという概念で統一されており、最初のデフォルトブランチはmasterになる。
ブランチのラベル参照は そのブランチの最新のコミットを指すラベル。masterでチェックアウト(HEAD情報も書き換えられる)で作業し、commitするとmaster ブランチが延びる。
先ほどのところでもし、testブランチを切り同じ場所を指していた場合に、checkout test(HEAD情報も書き換えられる) し、commitすると testブランチが延びる。
両方をマージする場合も同様にカレント(HEADの指す先のブランチが延びる)

参照リンクであるため、数千のブランチがあっても、速度にほとんど影響しません。

 

 

📑ラベル(HEAD)

checkoutしたコミットバージョンを HEADというタグが指す。currentというような現在作業中のベースコミットを指すイメージ。同様に、メインブランチをチェックアウトするとさらにHEADというタグは、メインブランチのラベルを示す形になる。別のブランチに git branch testBranchや git checkout HEAD^ と切り替えると、ワーキングディレクトリのファイル📁とHEADラベルがそのラベルの指すコミット状態に書き換わる。

全ての操作はカレントのHEADブランチに対しての操作が基本となる。

逆に今何のブランチを指しているかは、HEADの指す先を見ればわかる。

HEADを動かすときは、checkoutコマンドを使い、現在操作しているcomitを切り替える。

 

 

gitは  4つの領域を持つ。

  • ワーキングディレクトリ
  • ステージング(cacheまたは index)
  • コミット
  • 一時退避(stash)

ワーキングディレクトリ以外は、 .gitフォルダの GitObject内に保存される。
ステージング情報もリファレンス情報として、Gitコミットオブジェクトを指す形で保持
リモート情報、タグ情報、ブランチ情報も、リファレンスという形で  .git/refs/の中に保持されている。
カレントの保持情報も checkoutすると HEADのリファレンス先が変更になり、commitすると HEADの指した 例えば masterのリファレンス情報が子コミットを追加した形で書き換わる。

 

 

 

gitはコミット単位

   コミット単位に、関連するそのコミットの範囲、時間でのファイル情報を管理。
     一方CVS や 昔の SVN はファイル単位で管理し、ファイル単位に変更情報や、コミット情報を保存。

 

コミット情報の意図

関連付けつつ、コミット単位に疎結合、ファイルへも疎結合をひと手間かけてとったおかげで、容量とスピードのバランスが取れつつも、必要な情報にアクセスできている。ここにデータ構造のセンスが表れている。つまり、①コミット単位に素早く移動できつつ、コンテキスト単位(ブランチ分岐元から最終コミットまでの範囲)をたどれることで②意図を表現し、ファイル単位の適度な分割で小さく管理できているためコンフリクト範囲が小さくcherry-pickのような③パッチ適用向けに効率が良く対応できる。また後述する blobや ハッシュ値の対象範囲を見ても、無駄が少なくて感心する。

差分部分を staging時に登録しておき、関係するファイルへのリンク情報をcommit時に更新して保持する。これで、branchや作業向けのベースコミットを変更するときは必要なファイルに対し素早く変更できるようになる。同時にコミットの流れや、バックグラウンドや3wayマージ用のために、親のコミット情報へのリンク、コミットログ、コミッター情報を保存する。

Gitを操作するとき、branchやrebase,resetの時など、どこのコミット状態になるのか、どこのコミットを操作しているのか(HEAD)を軸に考えることになる。

 

 

コミット情報(コミットオブジェクト)の種類

  •  一つ前のコミット情報へのリンク(例えば  間接的に最新のmaster から 一つ前のmasterへの参照など)
  • 今回のコミットでのディレクトリファイル構造(tree)
  • 実ファイルへのリンク情報(blob へのハッシュ値)
  • コミットログ、コミッターなど

を持つ。

Git - Gitオブジェクトより/ cat-file

カレントヘッダやブランチ、タグの情報は持っていない。

「親コミット」と 「tree」情報は1階層分だがすべて繋がっているので、すべての関係項目へは高速にたどれる

「ファイル」情報は、実ファイル(GitObject)へのリンクなので、同じものは複製されず、どのリモートリポジトリからであっても共通であり、サーバによる影響がない。

「コミットとログ、コミッター」情報は、コミット単位に戻るときやマージでコミュニケーションをとるときに、コンテキストやバックグラウンドをつかむために必要。ITでは直接支援できないここに仕事やプロジェクトへの理解のセンスが求められる。

同様に、ブランチしたときや、リモートと連携した時も、コミット単位で情報をもつ。

実ファイルを持っているが、共通のコミット元とハッシュ値で比較することで、高速に差分チェック対象を絞り込める。

 

 

 

GitObjectで重要な3つ+1

  • tree🌲  ディレクトリ階層を表現
  • blob   実ファイル
  • commit 情報(コミットオブジェクト)
  • tag 🏷️


tree:ディレクトリの関係性(1階層分のみ)  サブディレクトリとして、treeも示せる

blob  blobとハッシュ値の構成を見ても、無駄が少なくて手感心する。treeがさす先 hash値の上2ケタがGitオブジェクト管理上のディレクトリ名、3桁目以降がファイル名になっている。

誰が修正したかという情報はblobにはなく、コミットオブジェクトにある。

Git - Gitオブジェクト / Git - Gitの参照 / cat-filedir / Resolve-Path

タグ🏷️オブジェクト

ここにあるタグは、オブジェクトを作る注釈版タグ(objectを作らない時は tagは直接commitを指し、LightWeight版と言われる)。署名付きタグは、OSSなどオープンな時に使うかもしれない。

一時的に個人的にタグを打つのでなければ、意図を書いてタグオブジェクトを作り共有する。

たとえタグが指しているコミットがHEADであっても どちらの種類のタグもコミットに合わせて移動することはない。

Git - タグ

 

 

 

Gitは 以下の情報を格納している

  • GitObject(Gitオブジェクトデータベース).git/objects
    • pack
    • info
  • Git Refrence   .git/refs
    • .git/refs/heads    ローカルブランチ群
    • .git/refs/remote  リモート追跡ブランチ群
    • .git/refs/tags      タグ群
    • master  .git/refs/heads/master
    • stash  .git/refs/stash
    • .git/refs/log
  • logs  .git/logs/   master,HEADなど参照履歴ログなどがある。
  • Git コミット情報
  • Git index   .git/index
  • HEAD  .git/HEAD
  • hooks  commit/ checkout時などに 連係するプログラム
  • info   exclude設定など
  • Git config 設定ファイル
  • description , COMMIT_EDITMSG : Webと連携するとき

 

 

4つの主要なデータについて補足する。

 

 

INDEX

indexの中身を確認するコマンド

ls-files

パーミッションとファイルパスがある。
上位3桁はファイルの種類で、下位3桁がパーミッション。
パーミッションはLinuxでいう権限らしい?
0のところは3wayマージでどこがコンフリクトしたかの情報(1-3)に使われるらしい。

上3桁? オブジェクト種別
100 ファイル blob
40 tree
不明 その他

indexを上書くときは ワーキングディレクトリのファイル単位では add または、コミットベースでは  checkoutで行う。

 

 

HEAD

HEADの中身 (master checkout 後)   現在作業環境上のブランチを指す  master とか、origin/masterとか

tag🏷️

tagの中身

Git - Gitの参照

 

 

remotes

2つに分かれて書かれる。

  1. .git/configの remoteセクション
  2. .git/refs/remotes/

 

remote情報は .git/configの remoteセクションに書かれる。
こちらの情報は、上記と違って、リモートサーバのリモートレポジトリ接続情報とローカルリポジトリとの関係性が記載されている。urlには git:やhttps:プロトコルなどいろいろなフォーマット指定ができる。

見てわかる通り詳細な情報ではないため、ブランチ名がリモートで消えていた場合、ローカルで残っていた場合、場合によってうまく接続できない(と思う)。

Git - 参照仕様(Refspec)




refs/remotes/の中身(リモートリポジトリ用の追跡ブランチ)
最後の1行

こちらは主にローカルリポジトリ管理用(ローカルリポジトリ内の remoteの参照情報を管理)
(トラッキングレポジトリについては次章で後述する)

Git - Gitの参照

 

記述内容を見て想像するに、fetchと clone時は configのファイルが、 pullとclone時はレポジトリとその参照ファイルが更新される(後述)。

pushの時も同じように処理する。

origin/masterなどのラベルは、HEADの考えに近く、リモートレポジトリと同期するようにpush/pull時などに動き、commit時には動かない。

Git - リモートブランチ

 

 

 

 

データに対する処理影響範囲

操作がどのデータに影響するのか、ざっと表で関連付けてみた。

commitの影響の仕方はcheckoutに近い動作になるが、ワーキングディレクトリにある内容は実質として同じものだからcommit/pushはワーキングディレクトリに変更が出ない。

そのほかの merge, rebaseなどの影響の仕方は checkoutに近い動作となり、一部を除いて基本的にはワーキングディレクトリを書き換える動作になる。

個別はオプションによっても変わるため、各コマンドのリファレンスを参照。

下記表は、基本的には master の最新にHEAD がある状況下で、パラメータの少ないよくあるパターンのコマンドで確認した内容にしている。

 

log , diff , showは更新しないので怖くない。

削除系の操作は working directoryに対する操作と branchや commitを永久追放する操作(branchしてすぐcommitせずにcheckoutするとか addせずに更新するとか、commitまで行っていない新規オブジェクト)です。commitがつながっている限り、残る。

第一の親のコミット履歴が重要視されるので、多と連携する場合や試したい場合は、masterブランチでこまめにコミットせず、試したいことはブランチを切って、はっきりしてからmasterブランチと連携すれば影響範囲やノイズは減らせると思う。

stashはまさにローカルのみなので、それほど怖くないはず。

 

 

コマンド ワーキング
ディレクトリ
index レポジトリ
オブジェクト
(右記のremote~を除く)
commit
(local)
HEAD remote
tracking
object
その他
checkout 上書き 上書き コピー元 (更新)
add コピー元 上書き 追加
commit 変更なし 変更なし
addフェーズで処理済み
追加 変更なし リファレンスが更新するときもある
branch 更新しない 更新しない refs更新
reset 更新
(hard)
更新
(hard/mixed)
コピー元 更新
(soft,
mixed,
hard)
merge 更新? 更新? コピー元更新 更新 更新
rebase 更新 更新 コピー元 更新 更新
cherrypick 変更なし
(きれいにしておくこと)
更新 コピー元 更新 更新
stash 更新 更新 更新 ワーキングディレクトリから退避
fetch 変更なし 更新 config更新
pull 更新? 更新? 更新 更新 更新 更新
clone 更新 更新 更新 更新 更新 更新 config更新
push 更新 リモートレポジトリ更新

空欄も変更なし

 

上記表の補足事項を追記する。

commit 

  ヘッダの指しているmaster やbranchの指している先のcommitオブジェクト参照を変える
  commit は add済みのものが対象で、add後修正したものは commitされないし、ワーキングディレクトリはcheckoutのように上書きされない。

 

add

コミット対象だったものが、削除されていたり更新されていた時に、検知するため、checkout時点で、全ファイルのリンク情報が上書きされる。

コマンド 直接的な動作内容
git add ファイルの削除情報を登録するコマンド
git rm  削除ファイル情報を登録
git mv 移動/リネームファイル情報を登録

※checkout , resetの副作用でcommit単位による上書く形で、indexの内容を取り消すことも可能

 

branch 

作成時はワーキングディレクトリは更新されない。checkout -bの時はもちろん、checkoutの動作と同じ。

git branch new_branch  master

masterにラベルを付けるのではなく、masterのcommit先をベースにラベル付けをするイメージ。

ワーキングディレクトリで対象となるbranchの切り替えは、実際はcommit情報を展開することだから、branchコマンドではなく、 checkoutコマンドで行う。もちろんHEADの参照先も変わる。

最新でるブランチ参照先を変更したいときは、次のresetを使う。

 

 

reset 

checkoutと同じ動作になる。ただしロールバックの意味があるため、完全ロールバックであればcheckout, workを残しておきたい場合 mixed,、今のaddした情報をそのままに 履歴を戻って別の流れにしたいときは softにする。
前に戻す(undo)だけではなくredoのように進ませることもできる。

 

checkout/resetの違い

checkout は目的のコミットへ HEADのみ位置を修正して、 commit/stagingの準備をする。
resetは ブランチの最新の定義を書き換えるため、HEADの先のラベルごと、参照(commitハッシュ、ブランチ、ラベルなど)を指定して目的のコミットへ移動する。 commit/stagingの準備はオプションで対応(ワーキングディレクトリを更新されたくない時など)する。

 

 

rebase

branch名が指定されていたら、最初にそのブランチ名でcheckoutしてから、個々のパッチをmergeし始める。

rebase,reset , --amendで 自分のコミットでないものを変な風に勝手に変更したら基本まずい。remoteのpushやpull request想定しているのであれば、最後にまとめた結果のみ渡すようにしたほうがいい。(ローカルのレポジトリを強引に変えるrebaseじゃないほうがいい。masterは remoteと同じままで、 localmasterなものに自由にコミットしていくイメージ)

rebaseはサブブランチをmasterに対してするもので、masterを patch や fixのブランチにrebaseはしないと思う。

まるで幹線道路を通る高速を過疎地の行き止まりに向けてキューカーブするようなイメージだから。それでどうするのか?その先どうするのかと思う。普通のサブブランチからmasterへのrebase + mergeが素直なコミットの流れに見える。pull/push時のリスクにも近づくし。

 

 

merge

FastForward

git merge で 現在のcommitが merge対象の子要素であれば自分が既に含まれているので、オブジェクトマージするようなコミット操作は不要。

HEAD'とその指すラベルがあれば)が書き換わるだけ(Fast-Forward)。
そうでなければ、マージコミットとして伸びる。

FFはrebaseと同様な効果があり、実質ブランチの情報の内、取り込まれるブランチのコミット情報がmasterにはなく、ブランチを消してしまうと、ブランチ経由のコミット情報が消える。

 

commit情報が消えるとき

mergeすればcommitタイミングが残る(コミットオブジェクトが増える)。rebaseはコミットタイミングが残らない。rebaseは途中のコンフリクトを一つずつ解消するイメージ。
mergeは最後の方までまるっとまとめて比較して解消する(同じところが修正されると2度3度とコンフリクトする)。

 

  cherry-pick

merge, cherry-pick, rebaseも類似関係がある。
mergeは2つのcommitの流れを統一させる。
rebaseは今いる位置までの流れを途中まで統一させる。
cherry-pickは対象の1コミットだけ合わせる。これも3方向マージで、
X――Y――-Z
        \
            A ― B

Z上で、Bをcherry-pickするときは、(B-Y)-(A-Y) = (B-A)なら、Zに取り込め、Bを消すようにresetする。
そうでない場合コンフリクトする。
根元を合わせたいときは rebase , 最終地点(最新)を合わせる場合 merge(後述)

後半だけ合わせるのは cherry-pickを個別に適用する。

 

 

commit,pull,clone,pushも類似関係がある。

commitはステージングで準備したオブジェクトをコミット化する。
pull, cloneは リモートからもらったコミット情報をローカルにコミットとして取り込む。
pushはローカルのコミット情報をリモートのレポジトリへコミットして取り込ませる(後述)。

リモートと名前がついても操作は、"remote"という名のローカルにあるレポジトリに対して操作する。

追跡レポジトリ

名称 存在場所 ローカルでの
参照例
ローカルでの
設定場所
リモートレポジトリ リモートにある .git/config
リモートトラッキングレポジトリ ローカルにある origin/master .git/refs
トラッキングレポジトリ ローカルにある master .git/refs

fetch時点では、リモートトラッキングレポジトリに  リモートから情報も含めてコピーされる。
リモートトラッキングレポジトリは、read-onlyであり、変更しないことが推奨されている。この時点では、ローカルに混ざらないように、分離した形での単なるコピーであり、状態などの情報をローカルで使用するためのものである。実際はおそらくリファレンスは分離しているが実態のオブジェクトは同じところに格納されていると思う。容量的にも(同じ実態であれば、リンクなので増えないが)ごっそり持ってきている。

 

push/fetch時の情報更新タイミング

トラッキングレポジトリを介して、ユーザは変更を行い、pushをするといったん直接リモートレポジトリを更新してから、リモートトラッキングレポジトリの内容をfetchして更新する。

fetchした時点では上述のように configにエントリが作られるまでだが、以下のようなコマンドで
トラッキングレポジトリをリモートトラッキングレポジトリと関連付けると、pushの時に自動で直接リモートレポジトリに更新をかけられる。

トラッキングレポジトリは 重複しなければ serverfixで作成される。
トラッキングレポジトリをリモートトラッキングレポジトリに結び付けることで、リモートのリポジトリと結びつけられ、以後、Gitが参照の指定先を解決するとき自動で解決できるようになる。

git checkout --track  remoteone/serverfix  

トラッキングレポジトリは stで作成される。
git checkout -b  st   remoteone/serverfix

Git - Remote Branches   / Git - git-fetch Documentation
"Tracking Branches" And "Remote-Tracking Branches" | GitGuys - GitGuys
git - Sync Branch and staying up to date with a project using GitHub for Windows - Stack Overflow
version control - Git: What is a tracking branch? - Stack Overflow

 

 

clone と init

cloneは initと似たような初期化処理を行う。

 

pull = fetch + merge

cloneの時は違うが、pullの時は、masterを個別に指定しなければ、上述のようなfetchの仕方を行い、カレントの追跡レポジトリでマージまで行う。

clone

分散レポジトリなので checkoutではなく、cloneで取り込む。

 

 

 

 

オブジェクトに対する操作方法


どこのコミット状態になるのか、どこのコミットを操作しているのか(HEAD)を軸に考えることになる。

 

 

補足

 ガベージコレクション―gc

2つの機能があるらしい。

Git - Packfile  / Git - メンテナンスとデータリカバリ
巨大なリポジトリ を Git で上手く扱う方法 | Atlassian Blogs

   stagingした(gitオブジェクトで登録)けれどコミットしなかったファイルや、のちにコミットの流れから外れたような参照されていないオブジェクトを消去する。→結果 レポジトリサイズが小さくなることがある。

zipなどの一般的なファイル内の辞書式方式ではなく、ファイル間の差分をとりデルタ差分で圧縮する。その際、ファイル名とファイルサイズをチェックし オブジェクトは packフォルダはいかにまとめられる。  最新の方法ほどすぐ取り出せるような差分管理方式を取り、似たようなファイルが来た時のレポジトリ容量増加を抑える機能として実装されている。

差分符号化 - Wikipedia

 

 

Gitのマージ

gitは 同ファイルで変更範囲が重なったときにコンフリクトとして検出する。3wayマージ方式をとっているため、さらに差異が比較的小さくなるため、コンフリクトの可能性は抑えられる。また同じファイル内でもコンフリクトが発生しないように、テスト確認での動作担保を前提としている。

Git - 高度なマージ手法

 

 

マージのコンフリクトの仕方

コンフリクト発生!焦らず利用する4つのコマンド #git - Qiita

※コンフリクト下ファイルに  <<<<<<があるのでそこのあたりを確認して修正し、addする。

 

 

pull request

Git外(GitHubなど)の仕組みで、 修正した内容を、pullで取り込んでもらう方法。
自分からpushできない環境を持つ管理者に対して、提案・お願いをする形になる。

 

 

 

gitのハッシュ値

計算対象

Gitのハッシュ値は、4つのGitObject Typeと ファイルサイズ、+実ファイルの内容から生成して、
その後圧縮してまとめているらしい。上記の内容に変化があればハッシュ値は変わる。

ファイル名が入っていないのは気になるポイント。ファイル名が変わってもハッシュ値は変わらないということでもある。

ハッシュ値が変わるときを理解すれば、ファイル容量が増えるとき、mergeやrebaseやcherry-pickしても変わらない時だとわかる。

他人が持っていようが、コミットが変わろうが、しばらくほかのリモートレポジトリにしかなかろうが、同じ内容であればどの人のコミットでもハッシュ値は変わらないということになる。
(いずれバッティングするが(先勝ち:後から登録したものは消える))

Git - Gitオブジェクト

 

sparse度(重複可能性)

SH1 は 160bit = 40桁。上位2ケタはディレクトリ名となって、38ケタがファイル名。

2017年Googleが発表して最近また話題になりましたが、2^63という値は、
セキュリティ強度なので関係ない。

2^160 = (2^10)^3)^5*2^10 ≒ 10^48 (1極(ごく)) 
スーパーコンピュータの京という単位が 10^16なので、
1京のファイルを1京回コミットすると、
1京分の1の確率でバッティングしちゃうかもという確率。
実際は、そこまできれいに分散しないのでもうちょっと狭いと思う。

 



実際、
1万ファイル10万回コミットを10万プロジェクトやっても 10^(4+5+5)=10^14、
ゆとりを1000倍とっても 10^17 なので、
100穣(じょう)分の1の確率でバッティングするかもレベルで

Git - リビジョンの選択

しかし、そんなことはまず起こりえないということを知っておくべきでしょう。SHA-1 ダイジェストの大きさは 20 バイト (160 ビット) 。ランダムなハッシュ値がつけられた中で、たった一つの衝突が 50% の確率で発生するために必要なオブジェクトの数は約 2^80 となる (衝突の可能性の計算式は p = (n(n-1)/2) * (1/2^160) です)。 2^80 は、ほぼ 1.2 x 10^24 、つまり一兆二千億のそのまた一兆倍。 これは、地球上にあるすべての砂粒の数の千二百倍にあたる。


個人的には、1%超えて1回でもバッティングしたらプロジェクトは破綻する。そのためその時プロジェクトを中断して、管理範囲を分割CONTROLする必要があると思っている。


前8ケタで、2^64 ≒ 100京 *4 なので、 
バッティングするのは、1兆どころか、100京分の1くらいの確率。

 

 

トピックブランチ

masterが商用と同じ環境で  developが長めの大規模開発用、topicはその時に一時的に試したり、開発したりと何らかのミニイベントが発生した時などに使う、運用上の話。

 

リビジョンパラメータのリファレンス解決順序

refs > refs/tags > refs/heads  > refs/remotes   > refs/HEAD

gitrevisions(7)

同じ基準で解決してそうなので、
  直指定のファイル > 直指定のブランチ > HEADの順序になるから
空指定は HEADになりそう。

indexを指定する時は一つ目、2つ目を空にするか、--cachedと明示する。

 

途中サブディレクトリに、pullできるか?

Gitはcommit単位で考える。
shallow cloneができるが、depth 指定になるため、最後のプロジェクト部分のフォルダであれば切り出せるかもしれない。

Gitのデータ構造上、影響範囲が大きいため、しばらくは対応できないと思う。

 

diff

(基本前の---が変更前側、後ろ側が+++の変更後側)

 

stage upもれ確認(新規を除く対象項目のみ)

git diff ( working + indexの比較)
addしていなくても差分が出る。
addしていなければ、 ”HEADとの差”と同意

修正内容の確認

git diff HEAD  (working, HEADの比較)

 

コミットする内容の確認

git diff --cached   (index HEADの比較)

 

diffの比較するもの

git diff   コミット1  コミット2   コミット間の比較

つまり git diff   [デフォルト:index  ] [デフォルト :workingディレクトリ]と比較する。
working を指定するときに、2つ目を空にする。

 

 

表記

記号 使用例 説明
^  HEAD^ HEADの一つ前のコミット
~n  A~1 直親のn個前。メインコミットの流れに沿う
^n A^2 1個上の階層の2番目の親
--  git diff -- ファイルパスA

以後のファイルは  そのまま解釈してほしい時

調査中

..  B..C  は  Bを除いた C。
C  ^B と同意
... B...C 共通の祖先から前(親)を除く
^ ^<rev> 除く
--not と同意

gitrevisions(7)

 

 

 

Gitの急所と対策

  • プロジェクトの分割ができない。
    • →極端に大きなプロジェクトは作らず、また何でもかんでも関連付けないで管理する。
  • アクセスコントロールなど権限管理ができない。
    • →githubや attlassian製品を使う。
  • バイナリの扱いや、ファイル圧縮に弱い
    • →実態への参照リンクの他、 ガベージコレクションやデルタ圧縮などで、ある程度はできています。
    • →みんなで共有するレポジトリの主要ラインに入れる前に、ローカルやブランチで吟味して容量を抑えておく。
  • gitはコマンドバッチの集合なので、データ構造の影響や副作用の把握が一見難しい、windows や GUIが今一歩サポートされていない
  • 日ごろのマージをスムーズにするため、コミット履歴を全部ローカルに確保しようとする
    • → (詳しくないので) shallow  cloneとか?

 

 

 

 

長いこと、お疲れ様です。

さいごにまとめを。

 

 

まとめ

Git は自分専用のローカルレポジトリを軸に、自分が理解できる範囲で、好きなように、好きなタイミングで更新(テスト、開発、確認、試行)ができる。

Git は comit単位のスナップショットで情報を持っているので、コミット操作を基本の軸として、ワーキングディレクトリやindexや gitObjectはどのコミット状態になるのか/したいのかを考え、また今操作しているコミットブランチ(HEADの具体的指し占めているもの)は何を指しているかを意識して使う。

git操作の話をするときは、①commit単位でやりたいことを明確にし、②現在のコミット位置をまず前提で話す。

 

操作のまとめ

  • 基本 checkoutを使って、branch/commit を移動する(workingfolderの上書きに注意)
  • ブランチのポインターが示す先を変えたいときは reset
    サブブランチの流れを メインブランチの流れに沿わせてupdateしたいときは rebase
  • 取り込みたいときにはmerge/clone 。分散リポジトリなので、”合体させたい”ではなく主体のある取り込みたいを意味する。
Delicious にシェア
Digg にシェア
reddit にシェア
LinkedIn にシェア
LINEで送る
email this
Pocket




コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です