write ahead log

ロールフォワード用

VSCodeで拡張子txtもMarkdownとしてプレビューしたい

まず設定ファイルを開く.

ctrl+,

から右上のアイコン「設定(JSON)を開く」をクリックしてconfig.jsonを開く.

config.jsonを開いて以下を追加.

    "files.associations": {
        "*.txt": "markdown"
    }

社内文書だと.md知らない人も多いので.txtの方が無難だったりする.

配管を作って学ぶGit

この記事はGit Advent Calendar 2019の記事です.

————————————————————

最近, 仕事でコードを書いてない...

そんな所にPro Gitをちょうど読み終わったので, Gitを作ることにしました.

toy-gitという個人プロジェクトです.

このプロジェクトの最終目標は自作のGitコマンド(の極小サブセット)で自身のREADMEをコミットする事です.

この記事は作成途中のメモ書きとブックマークを整理したものです.

Gitは(ごく一部だけなら)割と簡単に作れて楽しいので, 興味がある人はぜひ作ってみてほしいなと思います.

配管とは何ぞや

Pro Gitの最後の方の章に10.1 Gitの内側という章があります.

ここに配管についての説明があります.少し長いですが引用しましょう.

本書では、checkout や branch、remote などの約30のコマンドを用いて、Gitの使い方を説明しています。 一方、Gitには低レベルの処理を行うためのコマンドも沢山あります。これは、Gitが元々は、完全にユーザフレンドリーなVCSというよりも、VCSのためのツールキットだったことが理由です。これらのコマンドは、UNIX流につなぎ合わせたり、スクリプトから呼んだりすることを主眼において設計されています。 これらのコマンドは、通常 “ 配管(plumbing)” コマンドと呼ばれています。対して、よりユーザフレンドリーなコマンドは “ 磁器(porcelain)” コマンドと呼ばれています。

配管はGitを作るためのGitコマンドといった感じです.

実際, linusの最初のコミットを見ると, 普段使うコマンドないじゃん...ってなります.

配管を作ろう

上述のPro Gitにはかなり詳しく内部構造が説明されています.

知ってしまったら作りたくなるものです.

今回は以下のコマンド(の一部)を作ることにします.

コマンド 機能
git init Gitリポジトリを初期化する
git hash-object ファイルからGitのBlobオブジェクトを作ってリポジトリに登録.SHA-1のハッシュを得る.
git cat-file ハッシュを指定してリポジトリからファイル内容を取り出す
git update-index indexファイルを更新(stageする)
git ls-files Gitで管理されているファイルを見る
git write-tree indexからtreeオブジェクトを作成
git commit-tree treeからcommitオブジェクトを作成

これだけ作ればコミットを行えるはずです.

つまり自分で自分をコミットできるはずです!(やった!)

ここからはPro Git - Gitの内側を読んで内部構造をある程度把握してある前提で話を進めます.

図とか作ろうかと思いましたが, どう考えてもPro Gitの方が親切なので.

initを作る

とにかくリポジトリを作らなければ始まりません.

そしてinitは真似するのが簡単です.

とりあえず

git init

として, 同じものを作るプログラムを書けばよいのです.

ご存知の通りGitは.gitというディレクトリで情報を管理します.

というわけで.gitは以下のような中身です.

HEAD
config
description
hooks/
info/
objects/
refs/

実のところ, descriptionやhooksはお遊びでやる分には不要なはず...

中身はただのディレクトリやテキストファイルなのでそっくり真似すれば簡単です.

hash-objectを作る

hash-objectはPro Git - Gitオブジェクトがそのまま参考になります.

コマンドのオプション自体は

man git-hash-object

で調べることができます.

Gitオブジェクト

ところで, Gitのオブジェクトのフォーマットは以下のようになっています.

オブジェクト = ヘッダ + データ(中身はオブジェクトの種類によって異なる)
ヘッダ = オブジェクトの種類を表すリテラル + ' '(空白スペース1文字) + オブジェクトの長さ(byte) + NULL(\x00)
ハッシュ値 = オブジェクトをsha1関数に通したもの

データはオブジェクトによって異なります.

ヘッダのリテラルは以下の通り(そのままですが)

オブジェクト ヘッダのリテラル
Blobオブジェクト blob
Treeオブジェクト tree
Commitオブジェクト commit

オブジェクトは

.git/objects/{SHA-1の上2桁}/{残りのSHA-1の値}

にzlib圧縮されて保存されます.

Blobの場合はデータはファイルの中身そのままなので, 割と作るのは簡単です.

hash-objectした内容を確認しよう

これだけ情報があれば実装できるのですが, zlib圧縮のせいで確認がちょっと面倒です.

(テスト自体は本物のGitで同じファイルをhash-objectに通せば比較できるので簡単です)

そこでzlib圧縮を解凍して表示するpythonスクリプトを用意しました.

作成したtoy-gitのリポジトリにある適当なBlobオブジェクトに適用した時の結果がこちらです.

(多分Makefileを入れたとき)

$ zlib-decode.py .git/objects/1c/e8b08a4d5015417a37399f9ae0010520e5b842
blob 334toy-git: *.go
        go build

.PHONY: test
.SILENT:
test: clean toy-git
        test/init_test.sh
        test/hash_cat_test.sh
        test/update_index_test.sh
        test/write_tree_test.sh
        test/commit_tree_test.sh

.PHONY: clean
clean:
        rm -f toy-git
        rm -rf .toy-git
        rm -rf test/.toy-git
        -unlink test/.git 2>/dev/null
        rm -rf test/.git
        rm -f test/[a-z].txt

フォーマットやデータの中身が確認できますね.

cat-fileを作る

Gitリポジトリへhash-objectでファイルを登録したら, 今度は出したくなりますね.

取り出すために次はcat-fileコマンドを作ります.

cat-fileコマンドはオブジェクトのハッシュ値を引数に渡して, リポジトリから得たファイル内容を標準出力に出力するコマンドです.

hash-objectの逆のことをしてあげればよいので

  1. SHA-1ハッシュ値から.gitのobjects以下のファイルを読み込む
  2. zlibを解凍する
  3. ヘッダのファイルタイプとかコンテンツサイズを読み込む
  4. 出力する

と結構シンプルです.

実装方法は上記hash-objectのファイルフォーマットの説明で想像できると思います.

この2つのコマンドが出来上がるとPro Gitにある説明を自前で試すことが出来るようになります.

つまり

$ echo 'test content' | toy-git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4

でリポジトリへオブジェクト(ここではtest content)を格納し,

$ toy-git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

とすることでリポジトリからオブジェクトを取得できます.

それっぽくなってちょっと気分がアがります.

update-indexを作る

Gitでコミット前にやることといえば,

git add hoge

ですね.

addでステージングしてからコミットするというのが, gitを使って最初に学ぶ事だと思います.

ステージングというのは実のところ, hash-objectでリポジトリへ入れたオブジェクトのキーを.git/indexというファイルに書き込む事です.

このindexファイルのフォーマットなのですが割とややこしいです...

そういうこともあり, 公式でしっかりとドキュメント化されています.

これを読みながら実装していきます.

が, 全部実装するのはかなりしんどいです.

そこで実装するindexファイルのバージョンを2に絞ります.
(読む限り4まであるっぽい?)

フォーマットはヘッダとインデックスエントリーという2つの部分に分かれています.

ヘッダは以下のようなフォーマットになります.

内容 ビット数
"DIRC"という文字列 32
2(バージョン番号) 32
インデックスエントリー数 32

インデックスエントリーは以下.

Linuxのシステムコールstat(2)をかなり利用しています.

あと, インデックスエントリーの並びはパス名で昇順です.

内容 ビット数
stat(2)の ctimeの秒 32
stat(2)の ctimeのナノ秒 32
stat(2)の mtimeの秒 32
stat(2)の mtimeのナノ秒 32
stat(2)の dev 32
stat(2)の ino 32
モード(後述) 32
stat(2)の uid 32
stat(2)の gid 32
ファイルサイズ(32bit分) 32
オブジェクトへのSHA-1値 160
フラグ(後述) 16
エントリへのパス名 n
パディング(後述) 1-64

モードは以下になってます.

パーミッションは実質, 実行可能かどうかしか区分けしていません.

内容 ビット数
1000 (regular file), 1010 (symbolic link) , 1110 (gitlink) 4
未使用 3
unixパーミッション(ただし0755 or 0644) シンボリックリンクの場合は0 9

フラグは以下.(面倒なのでファイル名の長さ以外は0にした)

内容 ビット数
assume-validフラグ(git assume-unchangedで使うんだと思う.たぶん) 1
extendedフラグ(バージョン2にするので0) 1
stageフラグ(たぶんマージに使うんだと思う.) 2
ファイル名の長さ 12

パディングはエントリ全体が8バイトの倍数 & NULL終端になるように調整としてNULLを入れます.

このヘッダとエントリーの2つが単にくっついているのがindexファイルです.

ちなみにMercari Engineering Blogで掲載されているDQNEOさんの記事も参考になります.

というかたぶん, DQNEOさんも作ってるんじゃないかな.

上記リンク記事にある通り, hexdumpで確認しながら作っていくのが一番良いです.

以下は自作のGitで作ったindexをhexdumpしたもの.

$ hexdump -C .toy-git/index
00000000  44 49 52 43 00 00 00 02  00 00 00 01 5d d2 3f f0  |DIRC........].?.|
00000010  22 bd b5 a2 5d d2 3f f0  22 bd b5 a2 00 00 ca 01  |"...].?.".......|
00000020  00 1b 64 8f 00 00 81 a4  00 00 03 e8 00 00 03 e8  |..d.............|
00000030  00 00 00 21 42 39 a6 27  fe 39 21 e9 be b2 49 54  |...!B9.'.9!...IT|
00000040  15 8a 9c 47 fb 56 83 ec  00 14 74 65 73 74 2d 74  |...G.V....test-t|
00000050  61 72 67 65 74 2d 66 69  6c 65 2e 74 78 74 00 00  |arget-file.txt..|
00000060  00 00 00 00                                       |....|
00000064

本物のGitでも同じファイルをステージングして, ひたすら比較してチェックします.

テストは自動化したほうがいいです.面倒くさいので.

あと, stat(2)を利用するせいでWindows対応考えるのが面倒になって, せっかくgoで書いたのに諦めました.

ls-filesを作る

update-indexでindexを作ったら, 中身が確認したくなります.

確認のためのコマンドとしてls-filesコマンドがあるのでこれを作りましょう.

オプションを考えなければ, update-indexを作った逆をやるだけなので非常に簡単です.

ヘッダを読んでインデックスエントリーを1行ずつ表示します.

上のインデックスを自作Gitで読みだしたら以下の感じになります.

当たり前なんですが, 本物のGitでも同じ結果です.

# 自作のGit
$ toy-git ls-files
test-target-file.txt

# 本物のGit
$ git ls-files
test-target-file.txt

write-treeを作る

write-treeはupdate-indexで作ったindexの内容からTreeオブジェクトを作成します.

TreeオブジェクトのフォーマットはBlobオブジェクトの時とほとんど変わりません.

つまり(くどいですが)

オブジェクト = ヘッダ + データ(中身はオブジェクトの種類によって異なる)
ヘッダ = オブジェクトの種類を表すリテラル(tree) + ' '(空白スペース1文字) + オブジェクトの長さ(byte) + NULL(\x00)
ハッシュ値 = オブジェクトをsha1関数に通したもの

でオブジェクトは

.git/objects/{SHA-1の上2桁}/{残りのSHA-1の値}

にzlib圧縮されて保存されます.

問題はデータのフォーマットです.

実際にgitで作成したTreeオブジェクトをスクリプトでzlib解凍するとこんな感じでした.

$ zlib-decode.py .git/objects/1c/595ba797654b85488a8e79fe43a1fb55832fe9
tree 48100644 test-target-file.txtB9'9!龲ITGV

おー, わかるようでわからんわ.ググろ.

ということでStackOverFlowで見つけたのでこれを参考にします.

まず, 本物のGitでcat-fileでTreeオブジェクトを見ると以下で表示されます.

$ git cat-file -p 1c595ba797654b85488a8e79fe43a1fb55832fe9
100644 blob 4239a627fe3921e9beb24954158a9c47fb5683ec    test-target-file.txt

でStackOverFlowの情報を参考にするとデータのフォーマットは以下のようです.

tree 48\0
100644 test-target-file.txt\0 4239a627fe3921e9beb24954158a9c47fb5683ec
  • パーミッション(indexと同じ)
  • ファイルパス
  • NULL
  • Tree/BlobオブジェクトへのSHA-1

treeなのでディレクトリ毎に再帰的にtreeオブジェクトを作ってやる必要があります.

あとは愚直に作るだけです.

commit-treeを作る

commit-treeは作成したTreeオブジェクトからコミットを作るコマンドです.

これが出来ればいよいよコミットができるはず.

こちらもデータが気になりますが, これはzlib解凍するとすぐにわかりました.

$ zlib-decode.py .git/objects/01/9da3c9a0c1f76613845c26533ab03af5883d7e
commit 169tree 1c595ba797654b85488a8e79fe43a1fb55832fe9
author twinbird <ixa2063@gmail.com> 1575447414 +0900
committer twinbird <ixa2063@gmail.com> 1575447414 +0900

first commit

ざっくり見ると

  • author [ユーザ名] <メールアドレス> unixタイムスタンプ タイムゾーン
  • committer [ユーザ名] <メールアドレス> unixタイムスタンプ タイムゾーン
  • 空行1つ
  • コミットコメント

という感じです.

私はユーザやメールアドレスなどは面倒なので固定で実装しました.

update-refを作る

ここまででコミットができますが, 最後に(雑に作れば簡単なので)参照も実装します.

参照って何よ?となってもPro Gitがあるから大丈夫です.というか普段見ている「master」です.

10.3 Gitの内側 - Gitの参照

上記リンクの通り, 究極は以下のコマンドで参照を切り替えることができます.

$ echo "[コミットオブジェクトのSHA1値]" > .git/refs/heads/master

コミットオブジェクトへのSHA1値をファイルに書いているだけなんですね.

オブジェクトのチェックやらなんやらを作ると面倒ですが, 上のシェルスクリプトまんまなら実装は簡単ですね. (あんまりコマンドにする意味ないけど)

自作Gitでコミットしてみよう

作成したtoy-git自身でtoy-gitのREADME.mdをコミットしてみます.

indexへ追加する.

$ ./toy-git update-index --add README.md
$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   README.md

ツリーを作ってコミットを作る.

$ ./toy-git write-tree
202612bf7938e29f491145d41efa709e0cb7cbfa
$ echo "add README (from toy-git)" | ./toy-git commit-tree 202612bf7938e29f491145d41efa709e0cb7cbfa -p 8a129ade24cfd27e2ff824666e875e97ea308fc6
1f87ec37f6d36cf7ec509966c6810790978a1742

HEADを今コミットしたコミットへ移動する.

$ ./toy-git update-ref refs/heads/master 1f87ec37f6d36cf7ec509966c6810790978a1742

ログを見る

$ git log
commit 1f87ec37f6d36cf7ec509966c6810790978a1742 (HEAD -> master)
Author: twinbird <ixa2063@gmail.com>
Date:   Thu Dec 12 12:22:59 2019 +0900

    add README (from toy-git)

commit 8a129ade24cfd27e2ff824666e875e97ea308fc6 (origin/master, origin/HEAD)
Author: twinbird <ixa2063@gmail.com>
Date:   Wed Dec 11 12:28:00 2019 +0900

    change to avoid overwriting object

diffを取る.

$ git diff HEAD~
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..055bd7c
--- /dev/null
+++ b/README.md
[略]

うまくいってそう.やったね.

本物のGitでpushしてやればGitHubでもちゃんとREADMEを見ることができます.

まとめ

こんなので作ったとは言えん!

と言われそうですが(ごめん), まぁいいじゃないですか.コミットできるし.

本気で作っていくと恐ろしく大変ですが(Packfile, pushのプロトコル, diffアルゴリズム, 3-wayマージ etc...), とにかくコミットを作るだけなら割と簡単です.

Gitの内部構造もわかるし, とても楽しいので, 好みの言語で(ちょー小さいサブセットを)年末休暇に作るのはいかがでしょうか?

git bash(mintty)からでもpg_dumpしたい

ちょっと考えればわかりそうな事だったんだけど, メモしておく.

git bashやmsys2でpg_dumpを使おうとするとWindowsには疑似端末がない影響なのか, うまく動作しない.

(とはいえ,最近は状況が変わってきたようだけど.) マイクロソフト、Windows 10にUNIX系OSと似た擬似コンソール実装

うまくいかない例

pg_dump -h localhost -p ${LocalPort} -U ${DBUser} -d ${DBName} > dumpfile.sql

上記で実行すると, パスワードの入力待ちがうまく表示されない.

「お, こりゃ見たことあるゼ! winpty使えばいいんだろ?」

と安直にやるとリダイレクトがうまく動作しない.

うまくいった例

winpty pg_dump -f ${DumpFilePath} -h localhost -p ${LocalPort} -U ${DBUser} -d ${DBName}

困り果ててpg_dumpのマニュアルを引くと, なんとファイル出力するオプションがあるではないか.

そういえばmysqlでもそうだった気が...

msys2で使う時には以下もあった方がいいのか?

export PGCLIENTENCODING=UTF8

詳解Unixプログラミングのサンプルコードを動かす環境を作る

Ubuntu18.04で.

とりあえずbuild-essential入れりゃいいんだろと思っていました.

$ sudo apt install build-essential

APUEのサイトからサンプルライブラリを手に入れる

ここで配布されているのでダウンロードします.

# ダウンロード
$ curl -OL http://www.apuebook.com/src.3e.tar.gz
# 展開
$ tar xfvz src.3e.tar.gz

ライブラリをmakeするのに必要なライブラリを入れる

libbsd-devがいるっぽいです.

$ sudo apt install libbsd-dev

makeしてライブラリを配置

$ cd [ダウンロードして展開したディレクトリ]
$ make
$ sudo cp include/apue.h /usr/local/include
$ sudo cp lib/libapue.a /usr/local/lib

適当にサンプルをビルドする

$ gcc sample.c -lapue

これで動くはず.

WSLのUbuntuでsshしたら接続先の操作が文字化けする

WSL上のlocale設定していなかった.

日本語使わないと気づかんなぁ.

WSL上の.bashrcに以下を追加.

export LANG=ja_JP.UTF-8
export LANGUAGE=ja_JP.UTF-8
export LC_ALL=ja_JP.UTF-8

続いてlocaleを生成して設定.

$sudo locale-gen ja_JP.UTF-8
$sudo dpkg-reconfigure locales
[TUIになるのでja_JP.UTF-8をデフォルトにする]

ターミナルを再起動して完了.

laravelでdusk使う時にやること

毎回忘れるから雑にメモ.

HomesteadでなくCentOSの場合

chromeを入れてやる必要がある.

vi /etc/yum.repos.d/google-chrome.repo
[google-chrome]
name=google-chrome
baseurl=http://dl.google.com/linux/chrome/rpm/stable/$basearch
enabled=1
gpgcheck=1
gpgkey=https://dl-ssl.google.com/linux/linux_signing_key.pub
sudo yum -y install google-chrome-stable

duskのインストール

$ composer require --dev laravel/dusk
$ php artisan dusk:install

dusk用envファイルの作成

.env.testingとは異なるので注意.

$ cp .env.testing .env.dusk.testing

日本語を使えるようにする

$ vi tests/DuskTestCase.php

--lang=ja_JPを追記.

        $options = (new ChromeOptions)->addArguments([
            '--disable-gpu',
            '--headless',
            '--lang=ja_JP'
        ]);

テスト作って動かす

まぁ, 一応かいとこ.

$ php artisan dusk:make xxxTest
$ php artisan dusk