まず設定ファイルを開く.
ctrl+,
から右上のアイコン「設定(JSON)を開く」をクリックしてconfig.jsonを開く.
config.jsonを開いて以下を追加.
"files.associations": { "*.txt": "markdown" }
社内文書だと.md知らない人も多いので.txtの方が無難だったりする.
まず設定ファイルを開く.
ctrl+,
から右上のアイコン「設定(JSON)を開く」をクリックしてconfig.jsonを開く.
config.jsonを開いて以下を追加.
"files.associations": { "*.txt": "markdown" }
社内文書だと.md知らない人も多いので.txtの方が無難だったりする.
この記事はGit Advent Calendar 2019の記事です.
————————————————————
最近, 仕事でコードを書いてない...
そんな所にPro Gitをちょうど読み終わったので, 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は真似するのが簡単です.
とりあえず
git init
として, 同じものを作るプログラムを書けばよいのです.
ご存知の通りGitは.git
というディレクトリで情報を管理します.
というわけで.gitは以下のような中身です.
HEAD config description hooks/ info/ objects/ refs/
実のところ, descriptionやhooksはお遊びでやる分には不要なはず...
中身はただのディレクトリやテキストファイルなのでそっくり真似すれば簡単です.
hash-objectはPro Git - Gitオブジェクトがそのまま参考になります.
コマンドのオプション自体は
man git-hash-object
で調べることができます.
ところで, Gitのオブジェクトのフォーマットは以下のようになっています.
オブジェクト = ヘッダ + データ(中身はオブジェクトの種類によって異なる)
ヘッダ = オブジェクトの種類を表すリテラル + ' '(空白スペース1文字) + オブジェクトの長さ(byte) + NULL(\x00)
ハッシュ値 = オブジェクトをsha1関数に通したもの
データはオブジェクトによって異なります.
ヘッダのリテラルは以下の通り(そのままですが)
オブジェクト | ヘッダのリテラル |
---|---|
Blobオブジェクト | blob |
Treeオブジェクト | tree |
Commitオブジェクト | commit |
オブジェクトは
.git/objects/{SHA-1の上2桁}/{残りのSHA-1の値}
にzlib圧縮されて保存されます.
Blobの場合はデータはファイルの中身そのままなので, 割と作るのは簡単です.
これだけ情報があれば実装できるのですが, 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
フォーマットやデータの中身が確認できますね.
Gitリポジトリへhash-objectでファイルを登録したら, 今度は出したくなりますね.
取り出すために次はcat-fileコマンドを作ります.
cat-fileコマンドはオブジェクトのハッシュ値を引数に渡して, リポジトリから得たファイル内容を標準出力に出力するコマンドです.
hash-objectの逆のことをしてあげればよいので
と結構シンプルです.
実装方法は上記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
とすることでリポジトリからオブジェクトを取得できます.
それっぽくなってちょっと気分がアがります.
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で書いたのに諦めました.
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は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
treeなのでディレクトリ毎に再帰的に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
ざっくり見ると
という感じです.
私はユーザやメールアドレスなどは面倒なので固定で実装しました.
ここまででコミットができますが, 最後に(雑に作れば簡単なので)参照も実装します.
参照って何よ?となってもPro Gitがあるから大丈夫です.というか普段見ている「master」です.
上記リンクの通り, 究極は以下のコマンドで参照を切り替えることができます.
$ echo "[コミットオブジェクトのSHA1値]" > .git/refs/heads/master
コミットオブジェクトへのSHA1値をファイルに書いているだけなんですね.
オブジェクトのチェックやらなんやらを作ると面倒ですが, 上のシェルスクリプトまんまなら実装は簡単ですね. (あんまりコマンドにする意味ないけど)
作成した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の内部構造もわかるし, とても楽しいので, 好みの言語で(ちょー小さいサブセットを)年末休暇に作るのはいかがでしょうか?
Google App Engineのローカル開発環境をローカル以外からアクセスしたいことがあったので. (こんなことする人滅多にいないだろうけど...)
dev_appserver.py app.yaml --host=0.0.0.0 --enable_host_checking=no
ちょっと考えればわかりそうな事だったんだけど, メモしておく.
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
Ubuntu18.04で.
とりあえずbuild-essential入れりゃいいんだろと思っていました.
$ sudo apt install build-essential
ここで配布されているのでダウンロードします.
# ダウンロード $ curl -OL http://www.apuebook.com/src.3e.tar.gz # 展開 $ tar xfvz src.3e.tar.gz
libbsd-devがいるっぽいです.
$ sudo apt install libbsd-dev
$ cd [ダウンロードして展開したディレクトリ] $ make $ sudo cp include/apue.h /usr/local/include $ sudo cp lib/libapue.a /usr/local/lib
$ gcc sample.c -lapue
これで動くはず.
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をデフォルトにする]
ターミナルを再起動して完了.
毎回忘れるから雑にメモ.
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
$ composer require --dev laravel/dusk $ php artisan dusk:install
.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