def test_the_beauty_of_commits(basedir): wt = os.path.join(basedir, 'test_the_beauty_of_commits') init_dir(wt) os.chdir(wt) write_file(wt, 'greeting', 'Hello, world!\n') git_init(wt) git_add(wt, 'greeting') git_commit(wt, 'Added greeting') """ masterブランチのHEADつまり最新のものとして参照されているコミットを調べよう """ o = subprocess.run("git branch -v".split(), stdout=PIPE, stderr=STDOUT) # print_git_msg(o) msg = o.stdout.decode('ascii').strip() """ * master 444a7a7 Added my greeting """ commit_hash = msg.split()[2] # e.g, commit_hash == '444a7a7' # commitオブジェクトのhash値の先頭7桁が得られた assert re.match(r'\w{7}', commit_hash) """ commitオブジェクトのhash値を使って ワーキングツリーをリセットすることができる。 エリアスHEADはここで指定されたcommitオブジェクトを指すように変更される。 """ o = subprocess.run("git reset --hard".split() + [commit_hash], stdout=PIPE, stderr=STDOUT) # print_git_msg(o) # HEAD is now at 2c495c1 Added my greeting """ git reset --hard commitId はワーキングツリーのなかに現在ある すべての変更内容を消去する。 commitのIDを指定してワーキングツリーを戻す方法がもうひとつある。 git checkout だ。こちらはワーキングツリーの変更を消去しない。 またHEADが指すcommitIDはgit checkoutによって変更されない。 """ o = subprocess.run("git checkout".split() + [commit_hash], stdout=PIPE, stderr=STDOUT)
def test_introducing_the_blob(basedir): wt = os.path.join(basedir, 'test_introducing_the_blob') init_dir(wt) os.chdir(wt) write_file(wt, 'greeting', 'Hello, world!\n') git_init(wt) git_add(wt, 'greeting') git_commit(wt, 'Added greeting') # output = subprocess.run('git cat-file -t af5626b'.split(), stdout=PIPE, stderr=STDOUT) msg = output.stdout.decode("ascii").strip() assert 'blob' in msg # output = subprocess.run('git cat-file blob af5626b'.split(), stdout=PIPE, stderr=STDOUT) msg = output.stdout.decode("ascii").strip() assert 'Hello, world!' in msg
def test_what_if_file_under_subdir_was_added(basedir): """ fileがblobオブジェクトとして表されるのはわかった。 basedir直下のfileについては疑問はない。 ではサブフォルダの下にあるfileについてはどうなのか? というのもフォルダ(あるいはディレクトリと読んでもいいが)はblobにならないからだ。 サブフォルダの下にあるfileをindexにaddしたとき、indexがどういう状態になるのか? つまりサブフォルダがindexのなかにではどう表現されるのか? またサブフォルダの下にあるfileの変更を含むindexをcommitしたとき、 commitオブジェクトのなかでサブフォルダはどのように表現されるのか? コミットする直前のindexからcommitオブジェクトが作られる。 indexの形とcommitオブジェクトの形が、サブフォルダをどう表現するかという点において、 同じなのか違うのか?違うならどう違っているのか? """ wt = os.path.join(basedir, 'test_what_if_file_under_subdir_was_added') init_dir(wt) os.chdir(wt) git_init(wt) write_file(wt, 'README.md', '# README please\n') write_file(wt, '.gitignore', '*~\n') write_file(wt, 'src/greeting', 'Hello, world!\n') write_file(wt, 'src/hello.pl', 'print(\"hello\")\n') git_add(wt, '.') # git commitする前にindexの内容をprintしよう # `git ls-files --stage`コマンドを実行すると、stageにファイルが4つ登録されていることがわかる o = subprocess.run("git ls-files --stage".split(), stdout=PIPE, stderr=STDOUT) print("\n> git ls-files --stage") print_git_msg(o) """ 100644 b25c15b81fae06e1c55946ac6270bfdb293870e8 0 .gitignore 100644 27ac415058027193f9f7ffdc5b47a192225340d9 0 README.md 100644 af5626b4a114abcb82d63db7c8082c3c4756e51b 0 src/greeting 100644 11b15b1a4584b08fa423a57964bdbf018b0da0d5 0 src/hello.pl """ # ファイルのパスが `src/greeting` のようにフォルダを含めて書かれている。いいかえれば # サブフォルダ `src` が単独でindexのなかで1行を占めるということがない。このことに注意しよう。 # この4行はいづれもblobオブジェクトだ。 # さてコミットしよう git_commit(wt, "initial commit") # masterブランチのHEADが指すcommitオブジェクトそれ自体の内容をprintしてみよう o = subprocess.run("git cat-file -p HEAD".split(), stdout=PIPE, stderr=STDOUT) print("\n> git cat-file -p HEAD") print_git_msg(o) # masterブランチのHEADが指すcommitオブジェクトが指しているtreeオブジェクトの内容をprintしてみよう tree_hash = o.stdout.decode('ascii').splitlines()[0].split()[1] o = subprocess.run(['git', 'cat-file', '-p', tree_hash], stdout=PIPE, stderr=STDOUT) # o = subprocess.run("git cat-file -p master^{tree}".split(), stdout=PIPE, stderr=STDOUT) # The master^{tree} syntax specifies the tree object that is # pointed to by the last commit on your master branch. print("\n> git cat-file -p {}".format(tree_hash)) print_git_msg(o) """ 100644 blob b25c15b81fae06e1c55946ac6270bfdb293870e8 .gitignore 100644 blob 27ac415058027193f9f7ffdc5b47a192225340d9 README.md 040000 tree a393d373123524366b80788ba2ec12b426459279 src """ # 見よ! # サブフォルダ `src` に対応する1行がある。それはblobオブジェクトではなくてtreeオブジェクトだ。 # "git commit"コマンドを実行したときサブフォルダ `src` に対応するtreeオブジェクトが作成されたのだ。 # このtreeオブジェクトの中には`src`フォルダに含まれる2つのファイルに対応するblobが記録されているだろう。 # そのことを確かめてみよう。 for line in o.stdout.decode('ascii').splitlines(): if line.split()[1] == 'tree': tree_hash = line.split()[2] output = subprocess.run(['git', 'cat-file', '-p', tree_hash], stdout=PIPE, stderr=STDOUT) print("\n> git cat-file -p {}".format(tree_hash)) print_git_msg(output) """ 100644 blob af5626b4a114abcb82d63db7c8082c3c4756e51b greeting 100644 blob 11b15b1a4584b08fa423a57964bdbf018b0da0d5 hello.pl """ # サブディレクトリ src に対応するtreeオブジェクトのなかで、ふたつのファイルgreetingとhello.plの # パスがサブディレクトリ名を除外したファイル名のみになっていることに注目しよう。 # indexにはblobオブジェクトが4つあった。 # git commitを実行したらcommitオブジェクト1つができた。 # そしてtreeオブジェクトが2つできた。 # ひとつは ./README.md ファイルが位置する ルートのフォルダに対応するtreeで、 # もうひとつは ./src/greeting ファイルが位置するサブフォルダ src に対応するtreeだ。 # commitオブジェクトはルートフォルダに対応するtreeへリンクし、 # ルートフォルダに対応するtreeオブジェクトからsrcフォルダに対応するtreeオブジェクトへ # リンクが形成される。 # そして4つのblobはそれぞれファイルが属するフォルダに対応するtreeオブジェクトからリンクされる。 # けっきょくcommitオブジェクトからtreeノードの連鎖を経由して4つのblobへアクセスする # のに必要な情報が保持されていることがわかる。 # srcディレクトリの名前をsourceに変更してみよう。何が起こるだろうか? print("> mv src source") os.rename("src", "source") git_add(wt, '.') # git commitする前にindexの内容をprintしよう # `git ls-files --stage`コマンドを実行すると、stageにファイルが4つ登録されていることがわかる o = subprocess.run("git ls-files --stage".split(), stdout=PIPE, stderr=STDOUT) print("\n> git ls-files --stage") print_git_msg(o) """ 100644 b25c15b81fae06e1c55946ac6270bfdb293870e8 0 .gitignore 100644 27ac415058027193f9f7ffdc5b47a192225340d9 0 README.md 100644 af5626b4a114abcb82d63db7c8082c3c4756e51b 0 source/greeting 100644 11b15b1a4584b08fa423a57964bdbf018b0da0d5 0 source/hello.pl """ # なぜなら # ここで git status コマンドを実行してみよう。今回の変更点が絞り込まれて表示される。 o = subprocess.run("git status".split(), stdout=PIPE, stderr=STDOUT) print("\n> git status") print_git_msg(o) """ On branch master Changes to be committed: (use "git restore --staged <file>..." to unstage) renamed: src/greeting -> source/greeting renamed: src/hello.pl -> source/hello.pl """ # indexをcat-fileしたなかにsource/greetingのblobとsource/hello.plのblobが # 含まれているのは当然だ。だってgit statusにそう表示されるくらいなのだから。 # ところがindexをよくみると、今回変更しなかった .gitignore と README.md のblob # も含まれている。僕は驚いた。 # git statusコマンドはindexに単純にprintするコマンドではなかった。 # # ひとつのindexのなかにはgit addコマンドが実行された時点において、ワーキングツリーの # なかに存在していたすべてのファイルのblobのhash値が列挙されるのだ。ワーキングツリーの # なかにファイルが1000個あったら、git addコマンドを実行するとindexには1000個分の # blobが列挙されるのだ。 # git statusコマンドを実行したとき added xxxx とか renamed yyyy -> zzzz という # メッセージがほんの数行応答された場合でも、indexをcat-fileしてみると1000個分のblob # がそこに列挙されているのだ。 # こうなっているとは、僕はいまのいままで知らなかった。 # ええい、先に進もう。commitしてしまえ。 git_commit(wt, "renamed the src directory to source") # masterブランチのHEADが指すcommitオブジェクトそれ自体の内容をprintしてみよう o = subprocess.run("git cat-file -p HEAD".split(), stdout=PIPE, stderr=STDOUT) print("\n> git cat-file -p HEAD") print_git_msg(o) # masterブランチのHEADが指すcommitオブジェクトが指しているtreeオブジェクトの内容をprintしてみよう tree_hash = o.stdout.decode('ascii').splitlines()[0].split()[1] o = subprocess.run(['git', 'cat-file', '-p', tree_hash], stdout=PIPE, stderr=STDOUT) print("\n> git cat-file -p {}".format(tree_hash)) print_git_msg(o) """
def test_blobs_are_stored_in_trees(basedir): wt = os.path.join(basedir, 'test_blobs_are_stored_in_trees') init_dir(wt) os.chdir(wt) write_file(wt, 'greeting', 'Hello, world!\n') git_init(wt) git_add(wt, 'greeting') git_commit(wt, 'Added greeting') # """ Gitはファイルの構造と名前を表現するために、blobをtreeへ leaf nodeとしてくっつける。とてもたくさんのtreeがあるだろう。 どのtreeに目的のblobがあるのかを見つけることは難しい。しかし さっき作ったblobをポイントするtreeは、たった今つくったcommit つまりHEADが保持しているtreeのなかにあるはずだ。だから git ls-tree HEAD とやれ。するとgreetingファイルのblobをポイントするtreeが みつかるはずだ。 """ output = subprocess.run('git ls-tree HEAD'.split(), stdout=PIPE, stderr=STDOUT) # print_git_msg(output) # 100644 blob af5626b4a114abcb82d63db7c8082c3c4756e51b greeting msg = output.stdout.decode('ascii').strip() """ このコミットはgreetingをリポジトリに追加したことを記録している。 このコミットはtreeをひとつ含み、そのtreeはleaf nodeをひとつ持っている。 そのleaf nodeはgreetingのblobを指している。 """ assert '100644' in msg assert 'blob' in msg assert 'af5626b4a114abcb82d63db7c8082c3c4756e51b' in msg assert 'greeting' in msg # """上記のようにgit ls-tree HEADを実行することにより HEADコミットによって参照されているtreeの内容を見ることができた。 しかしそのtreeがどういう名前でレポジトリに存在しているかはまだ 見ることができていない。git rev-parse HEADを実行することで treeオブジェクトを見つけることができる。 """ output = subprocess.run('git rev-parse HEAD'.split(), stdout=PIPE, stderr=STDOUT) # print_git_msg(output) # 9429e3ec347cc51565026b3adbecd37be4f92601 # treeオブジェクトのhash値は一定ではない。 # タイミングによりけりでさまざまなhash値になりうる。 output = subprocess.run('git cat-file -t HEAD'.split(), stdout=PIPE, stderr=STDOUT) # print_git_msg(output) """commit""" """ cat-fileコマンドに -t オプションを指定すると 引数に指定されたオブジェクトの内容ではなくてオブジェクトのtypeが 表示される。HEADは常にcommitオブジェクトだ。 """ output = subprocess.run('git cat-file commit HEAD'.split(), stdout=PIPE, stderr=STDOUT) # print_git_msg(output) """ tree 0563f77d884e4f79ce95117e2d686d7d6e282887 author kazurayam <*****@*****.**> 1621389261 +0900 committer kazurayam <*****@*****.**> 1621389261 +0900 """ """ git cat-file commit HEADは、HEADというエリアスが指している commitオブジェクトの内容を表示する。上記の例ではこのcommitには treeオブジェクトが1つだけ含まれていてそのtreeのhash値は0563..である。 ひとつのコミットにtreeオブジェクトが2つ以上含まれることもざらにある。 commitオブジェクトにはそのcommitを作った人の名前とコミットを作成した 日時も記録されている。名前と日時が可変なので、commitオブジェクトの hash値はさまざまな値をとりうる。 いっぽうtreeオブジェクトのhash値(上記の例では 0563f77..)は 一定だ。なぜ一定なのか?treeオブジェクトのhash値はtreeオブジェクト の中身によって決まる。いまみているtreeオブジェクトの中身は下記のとおりだ。 `100644 blob af5626b4a114abcb82d63db7c8082c3c4756e51b greeting` ファイルを同じ名前(greeting)にして同じ内容を書き込んだなら、 greetingファイルのblobのhash値が同一になるはずで、 その結果としてtreeオブジェクトのhashは一定になる。 hash値が0563f77..であるオブジェクトがたしかにHEADが指している treeオブジェクトであることを確かめてみよう。 """ output = subprocess.run('git ls-tree 0563f77'.split(), stdout=PIPE, stderr=STDOUT) # print_git_msg(output) # 100644 blob af5626b4a114abcb82d63db7c8082c3c4756e51b greeting """ わたしのリポジトリはこの時点でただひとつのcommitを含んでおり、 このcommitは1個のblobを持つ1個のtreeを参照している。 だから .git/objectsディレクトリのなかにオブジェクトが3個あるはずだ。 このことをfindコマンドで確かめてみよう。 """ output = subprocess.run('find .git/objects -type f'.split(), stdout=PIPE, stderr=STDOUT) # print_git_msg(output) # .git/objects/05/63f77d884e4f79ce95117e2d686d7d6e282887 # .git/objects/54/9d175982bea318c6eba58ac5046f947f00eba8 # .git/objects/af/5626b4a114abcb82d63db7c8082c3c4756e51b """ たしかに.git/objectsのしたにオブジェクトが3つあった。 そして3つのオブジェクトのhash値は上記の例で現れた値にほかならない。 3つのオブジェクトがどういうtypeのオブジェクトであるか、確かめておこう。 """ output = subprocess.run('git cat-file -t 0563f77'.split(), stdout=PIPE, stderr=STDOUT) # print_git_msg(output) # commitオブジェクトのhash値は一定でない。同じgreetingファイルをcommitしたのでも、コミットした時刻が # 違っていればいるからだ。だから下記のように549d175という固定文字を指定してcommitオブジェクトをcat-file # しようとするときっとエラーになる。だからコメントアウトした。 # output = subprocess.run('git cat-file -t 549d175'.split(), stdout=PIPE, stderr=STDOUT) # msg = output.stdout.decode("ascii").strip() # print("\n{}\n".format(msg)) # output = subprocess.run('git cat-file -t af5626b'.split(), stdout=PIPE, stderr=STDOUT) # print_git_msg(output) """ git show HEADコマンドでHEADというエリアスが指すcommitオブジェクトの 内容を調べられる。やってみよう。 """ output = subprocess.run('git show HEAD'.split(), stdout=PIPE, stderr=STDOUT) # print_git_msg(output) '''commit ea920998ab1630d9d92a4be618a5fdcfd428f657 Author: kazurayam <*****@*****.**> Date: Wed May 19 18:16:30 2021 +0900 Added my greeting diff --git a/greeting b/greeting new file mode 100644 index 0000000..af5626b --- /dev/null +++ b/greeting @@ -0,0 +1 @@ +Hello, world! ''' """ git show HEADコマンドが応答したテキストの1行目をみれば HEADに対応するcommitオブジェクトのhash値がわかる。そのhash値を引数として git cat-file commit <hash> を実行してみよう。そのcommitオブジェクトの中身を見ることができる。 """ commit_hash = output.stdout.decode("ascii").splitlines()[0].split(' ')[1] output = subprocess.run( "git cat-file commit {}".format(commit_hash).split(), stdout=PIPE, stderr=STDOUT) # print_git_msg(output)\ """tree 0563f77d884e4f79ce95117e2d686d7d6e282887
def write_add_commit_file(wt, path, text, msg, verbose=False): write_file(wt, path=path, text=text) git_add(wt, path, verbose=verbose) git_commit(wt, msg, verbose=verbose)