UUUM攻殻機動隊(エンジニアブログ)

UUUMのエンジニアによる技術ブログです

addから始めるgit入門 (2)~gitをjsで実装してみよう commit編~

どうもこんにちは!

インターンの@hirokihelloです!

前回書いたこちらの記事が予想外に好評で非常に嬉しいです!

system.blog.uuum.jp

というわけで今日も前回の記事に引き続き、gitの実装をしていきます。今回はcommitについて実装をしていきます。

前回の知識を前提に実装していくので、まだ前回の記事を読まれていない方は是非是非前回の記事も合わせて読んでみてくださいね!

環境は下記の通りです。

$ git version
git version 2.21.1 (Apple Git-122.3)
$ node -v
v14.5.0

commitとは

commitに期待される挙動

まずはcommitが何をやるコマンドなのかgitのdocumentを確認してみましょう。

Create a new commit containing the current contents of the index and the given log message describing the changes. The new commit is a direct child of HEAD, usually the tip of the current branch, and the branch is updated to point to it

引用 git-scm.com

ふむ。読むのがめんどくさい。

ポイントはたくさんあるのですが、今回のポイントは下記です。

  1. indexとログメッセージからcommitオブジェクトを新しく作成する。
  2. HEADにそのcommitは属するものであり、現在のブランチが新しくそのcommitを示すように更新処理をする。

このポイントを頭の片隅にいれた状態で、commitを作成すると、どのような状態になるのかについて具体的にみていきましょう。

実際にcommitの挙動を確認する

$ mkdir git_test
$ cd git_test
$ git init
$ ls -la .git
total 24
drwxr-xr-x   9 inoue_h  staff  288  9 20 16:37 ./
drwxr-xr-x  17 inoue_h  staff  544  9 20 16:37 ../
-rw-r--r--   1 inoue_h  staff   23  9 20 16:37 HEAD
-rw-r--r--   1 inoue_h  staff  137  9 20 16:37 config
-rw-r--r--   1 inoue_h  staff   73  9 20 16:37 description
drwxr-xr-x  13 inoue_h  staff  416  9 20 16:37 hooks/
drwxr-xr-x   3 inoue_h  staff   96  9 20 16:37 info/
drwxr-xr-x   4 inoue_h  staff  128  9 20 16:37 objects/
drwxr-xr-x   4 inoue_h  staff  128  9 20 16:37 refs/

先ほどポイントに出てきたHEADファイルが存在しますね。 HEADファイルは、現在最新のコミットを示すファイルです。

ここには何が格納されているのでしょうか。

$ cat .git/HEAD
ref: refs/heads/master

ref: refs/heads/masterという文字列が格納されていました。これは.git/refs/heads/masterに今作業中の最新のコミットが格納されているという意味となります。

.git/refs/heads/masterをみてみましょう。

$ cat .git/refs/heads/master
cat: .git/refs/heads/master: No such file or directory

存在しないようです。まだコミットしていないからですね。 ひとまずrefsの中身をみてみましょう。

$ ls -la .git/refs
total 0
drwxr-xr-x  4 inoue_h  staff  128  9 20 16:37 .
drwxr-xr-x  9 inoue_h  staff  288  9 20 16:42 ..
drwxr-xr-x  2 inoue_h  staff   64  9 20 16:37 heads
drwxr-xr-x  2 inoue_h  staff   64  9 20 16:37 tags
$ ls -la .git/refs/heads
total 0
drwxr-xr-x  2 inoue_h  staff   64  9 20 16:37 .
drwxr-xr-x  4 inoue_h  staff  128  9 20 16:37 ..

今のところrefs以下には何もないですね。commitしてみましょう。

$ echo 'console.log("hoge")' > sample.js
$ node sample.js
hoge
$ git add sample.js 
$ git commit -m 'first commit'
[master (root-commit) 0e95049] first commit
 1 file changed, 1 insertion(+)
 create mode 100644 sample.js

何やらメッセージが現れましたね。ファイルはどのように更新されたでしょうか。

$ ls -la .git
total 40
drwxr-xr-x  12 inoue_h  staff  384  9 20 16:47 .
drwxr-xr-x  17 inoue_h  staff  544  9 20 16:37 ..
-rw-r--r--   1 inoue_h  staff   13  9 20 16:47 COMMIT_EDITMSG
-rw-r--r--   1 inoue_h  staff   23  9 20 16:37 HEAD
-rw-r--r--   1 inoue_h  staff  137  9 20 16:37 config
-rw-r--r--   1 inoue_h  staff   73  9 20 16:37 description
drwxr-xr-x  13 inoue_h  staff  416  9 20 16:37 hooks
-rw-r--r--   1 inoue_h  staff  137  9 20 16:47 index
drwxr-xr-x   3 inoue_h  staff   96  9 20 16:37 info
drwxr-xr-x   4 inoue_h  staff  128  9 20 16:47 logs
drwxr-xr-x   7 inoue_h  staff  224  9 20 16:47 objects
drwxr-xr-x   4 inoue_h  staff  128  9 20 16:37 refs

16:47に更新がかかったのは以下の部分です。

-rw-r--r--   1 inoue_h  staff   13  9 20 16:47 COMMIT_EDITMSG
-rw-r--r--   1 inoue_h  staff  137  9 20 16:47 index
drwxr-xr-x   3 inoue_h  staff   96  9 20 16:37 info
drwxr-xr-x   4 inoue_h  staff  128  9 20 16:47 logs
drwxr-xr-x   7 inoue_h  staff  224  9 20 16:47 objects

一見するとrefsの変更時間は変わっていません。

$ ls -la .git/refs/heads
total 8
drwxr-xr-x  3 inoue_h  staff   96  9 20 16:47 .
drwxr-xr-x  4 inoue_h  staff  128  9 20 16:37 ..
-rw-r--r--  1 inoue_h  staff   41  9 20 16:47 master

しかし、refs以下の.git/refs/headsにmasterというファイルが作られました。

$ cat .git/refs/heads/master
0e95049453fa4d33b5c1ceedb042181fa4af0c40

むむ。hashが出てきました。どうやらaddの時で作成したような、objectsを示すようですね。とりあえずcat-fileコマンドを使ってみましょう。

$ git cat-file -p 0e95049453fa4d33b5c1ceedb042181fa4af0c40
tree 161e899ffc6e06b5a8f94b77c99312c30deb9452
author hirokihello <iammyeye1@gmail.com> 1600588067 +0900
committer hirokihello <iammyeye1@gmail.com> 1600588067 +0900

first commit

cat-fileコマンドでみることができたので、objectsファイルのようです。ここで一旦先ほど更新のかかっていたディレクトリをみてみましょう。

ls -la .git/objects
total 0
drwxr-xr-x   7 inoue_h  staff  224  9 20 16:47 .
drwxr-xr-x  12 inoue_h  staff  384  9 20 16:47 ..
drwxr-xr-x   3 inoue_h  staff   96  9 20 16:47 0e
drwxr-xr-x   3 inoue_h  staff   96  9 20 16:47 16
drwxr-xr-x   3 inoue_h  staff   96  9 20 16:47 ea
drwxr-xr-x   2 inoue_h  staff   64  9 20 16:37 info
drwxr-xr-x   2 inoue_h  staff   64  9 20 16:37 pack

16:47に0e, 16, ea三つのディレクトリができています。 一つづつ確認していきます。

$ ls -la .git/objects/0e
total 8
drwxr-xr-x  3 inoue_h  staff   96  9 20 16:47 .
drwxr-xr-x  7 inoue_h  staff  224  9 20 16:47 ..
-r--r--r--  1 inoue_h  staff  128  9 20 16:47 95049453fa4d33b5c1ceedb042181fa4af0c40

0e以下にできたのは先ほど確認した、.git/refs/heads/masterで示されていたファイルですね。次のディレクトリをみてみましょう。

$ ls -la .git/objects/16
total 8
drwxr-xr-x  3 inoue_h  staff   96  9 20 16:47 .
drwxr-xr-x  7 inoue_h  staff  224  9 20 16:47 ..
-r--r--r--  1 inoue_h  staff   54  9 20 16:47 1e899ffc6e06b5a8f94b77c99312c30deb9452

先ほど確認した0e95049453fa4d33b5c1ceedb042181fa4af0c40の一行目に記載されている、tree 161e899ffc6e06b5a8f94b77c99312c30deb9452 はこのobjectファイルを示していることがわかります。 内容を確認しましょう。

$ git cat-file -p 161e899ffc6e06b5a8f94b77c99312c30deb9452
100644 blob ea8e751d31e45830b3ace4d1238a4429f3fb18f5    sample.js

100644 blob ea8e751d31e45830b3ace4d1238a4429f3fb18f5 sample.jsという内容が格納されたファイルが存在しますね。

ea8e751d31e45830b3ace4d1238a4429f3fb18f5はどうやらhashのようですが...

$ ls -la .git/objects/ea
total 8
drwxr-xr-x  3 inoue_h  staff   96  9 20 16:47 .
drwxr-xr-x  7 inoue_h  staff  224  9 20 16:47 ..
-r--r--r--  1 inoue_h  staff   36  9 20 16:47 8e751d31e45830b3ace4d1238a4429f3fb18f5
$ git cat-file -p ea8e751d31e45830b3ace4d1238a4429f3fb18f5
console.log("hoge")

161e899ffc6e06b5a8f94b77c99312c30deb9452のhashに記載されている、ea8e751d31e45830b3ace4d1238a4429f3fb18f5はobjectファイルを示していることがわかりました。

さてここまでで出てきた情報を一旦整理します。

addの段階でできるもの

  • 8e751d31e45830b3ace4d1238a4429f3fb18f5

addしたsample.jsの内容が保存されている。

ここまでは前回やった部分です。

commitの時点でできるっぽいもの。

  • 0e95049453fa4d33b5c1ceedb042181fa4af0c40
  • 161e899ffc6e06b5a8f94b77c99312c30deb9452

0e95049453fa4d33b5c1ceedb042181fa4af0c40は、.git/refs/heads/masterで参照されているobjects。中身は下記のようになっており、161e899ffc6e06b5a8f94b77c99312c30deb9452を示しています。

$ git cat-file -p 0e95049453fa4d33b5c1ceedb042181fa4af0c40
tree 161e899ffc6e06b5a8f94b77c99312c30deb9452
author hirokihello <iammyeye1@gmail.com> 1600588067 +0900
committer hirokihello <iammyeye1@gmail.com> 1600588067 +0900

first commit

161e899ffc6e06b5a8f94b77c99312c30deb9452はobjectsファイルであり、中身は下記のようになっており、addコマンドで作成されたオブジェクトであるea8e751d31e45830b3ace4d1238a4429f3fb18f5を示しています。

$ git cat-file -p 161e899ffc6e06b5a8f94b77c99312c30deb9452
100644 blob ea8e751d31e45830b3ace4d1238a4429f3fb18f5    sample.js

なので今回commitコマンドの実装にあたり、要件としては

  1. 0e95049453fa4d33b5c1ceedb042181fa4af0c40161e899ffc6e06b5a8f94b77c99312c30deb9452の二つと同じ構造をもつobjectsを作成する
  2. .git/refs/heads/masterを書き換える

の2点となります。

今回の実装の注意として、commitコマンドと同じファイルを生成する、つまりファイルの内容から生成されるhashを生成することをゴールとし実際にファイルの書き込みは行いません。

hashが同様に生成できればファイルの中身が同じであることが証明され、あとは書き込むか書き込まないかの違いだけなためそのようにします。

理由として、gitでは同じhash objectsがあれば書き込まないハンドリング、ファイルのパーミッションなど今回のcommit実装というテーマの本質とは若干外れる部分が必須になってくるためです。

書き込みは次回の記事に回します。書き込む方法自体はこれも記事内で言及しますが、headerとcontent部分をzlibでdeflateするだけであり、addでやった通りです。 念の為コメントアウトして書いておきますので、興味のある方はやってみてください。

commitを実装してみる

前回実装したコードの知識は前提として実装していきますので、objects/index周りは前回の記事を参照してくださいね。

system.blog.uuum.jp

まずは先ほどみた、

0e95049453fa4d33b5c1ceedb042181fa4af0c40161e899ffc6e06b5a8f94b77c99312c30deb9452の二つと同じ構造をもつobjectsを作成する

これを実装します。

それぞれ0e95049453fa4d33b5c1ceedb042181fa4af0c40はコミットオブジェクト、161e899ffc6e06b5a8f94b77c99312c30deb9452はtreeオブジェクトと呼ばれています。

内容をもう一度みておきましょう。

bash-3.2$ git cat-file -p 0e95049453fa4d33b5c1ceedb042181fa4af0c40
tree 161e899ffc6e06b5a8f94b77c99312c30deb9452
author hirokihello <iammyeye1@gmail.com> 1600588067 +0900
committer hirokihello <iammyeye1@gmail.com> 1600588067 +0900

first commit

bash-3.2$ git cat-file -p 161e899ffc6e06b5a8f94b77c99312c30deb9452
100644 blob ea8e751d31e45830b3ace4d1238a4429f3fb18f5    sample.js

treeオブジェクトはunixのディレクトリのような役割をしており、gitにおいてディレクトリ構造を表現するのに使用されます。

treeオブジェクトはディレクトリのように、内部にtreeオブジェクトもしくはblobオブジェクトを子として持ちます。

コミットオブジェクトは、commit時のindexファイルの一覧を示すtreeオブジェクト、author情報などを格納したファイルとなります。

それぞれtreeオブジェクト、commitオブジェクトに関しては詳細はこの記事を読み込むと良いです。

git-scm.com

今回の例でどのように使われているかというと

  1. addでsample.jsのblob objectが作成される
  2. commit時にindexファイルのentriesに記載されているobjectsの一覧が記載されたtreeオブジェクトが作成される
  3. commitオブジェクトに、2で作成したtree objectのsha1 hashやauthor情報、message("first commit"など)を記載する

といった流れで生成・使用されています。

なのでまずやるべきことは、treeファイルを作成することです。

treeファイルを生成する

最初にtreeファイルを生成する完成系のコードを乗っけておきます。

const crypto = require('crypto');
const fs = require('fs').promises;
const zlib = require('zlib');

async function genTree (fileContents=[]) {
  const content = calcContents(fileContents)
  const header= Buffer.from(`tree ${content.length}\0`)
  const store = Buffer.concat([header, content], header.length + content.length);
  const shasum = crypto.createHash('sha1');
  shasum.update(store);
  const sha1 = shasum.digest('hex')
  zlib.deflate(store, async function (err, result) {  
    dirPath = __dirname + '/.git/objects/' + sha1.substring(0,2)
    filePath = dirPath + '/' + sha1.substring(2, 40)
    // await fs.mkdir(dirPath, { recursive: true }, (err) => {
    //  if (err) throw err;
    // });
    //  fs.writeFile(filePath, result, function (err) {
    //  if (err) throw err;
    //  console.log('Saved!');
    // })
  });
  return sha1;
}

function calcContents (fileContents=[]) {
  return fileContents.reduce((acc, file) => {
    const content = calcContent(file)
    return Buffer.concat([acc, content], acc.length + content.length)
  }, Buffer.alloc(0))
}

function calcContent (fileContent) {
  const fileMode = fileContent.mode //100644
  const fileName = fileContent.name // sample.js
  const fileHash = fileContent.sha1 // 52679e5d3d185546a06f54ac40c1c652e33d7842
  const hash = Buffer.from(fileHash, "hex")
  const content = Buffer.from(`${fileMode} ${fileName}\0`) // modeとnameの間に半角スペースを開ける。

  const buffer = Buffer.concat([content, hash], hash.length + content.length)
  return buffer
}

module.exports = {
  genTree
}

treeファイルのフォーマット

treeファイルはどのようなフォーマットなのでしょうか。下記のようであるとの情報があります。

tree <content length><NUL><file mode> <filename><NUL><item sha>...

引用 www.dulwich.io

必要なのは、 1. filename 2. mode 3. sha

の三つですね。

これらはaddのタイミングでindexに全て書き込んでいるので、それが使えそうです。

実際にこの形式で書き込まれているのか検証してみましょう。

先ほどのsample.jsをcommitした時に生成された161e899ffc6e06b5a8f94b77c99312c30deb9452をみてみます。

$ git cat-file -p 161e899ffc6e06b5a8f94b77c99312c30deb9452
100644 blob ea8e751d31e45830b3ace4d1238a4429f3fb18f5    sample.js

$ hexdump -C .git/objects/16/1e899ffc6e06b5a8f94b77c99312c30deb9452
00000000  78 01 2b 29 4a 4d 55 30  36 67 30 34 30 30 33 31  |x.+)JMU06g040031|
00000010  51 28 4e cc 2d c8 49 d5  cb 2a 66 78 d5 57 2a 6b  |Q(N.-.I..*fx.W*k|
00000020  f8 24 c2 60 f3 9a 27 17  95 bb 5c 34 3f ff 96 f8  |.$.`..'...\4?...|
00000030  0a 00 56 72 11 e7                                 |..Vr..|
00000036

object fileはzlibで圧縮されているので解凍できるように関数ファイルを作ります。

const fs = require('fs');
const zlib = require('zlib');

async function inflate () {
  const hash = process.argv[2]
  const dirPath = __dirname + '/.git/objects/' + hash.substring(0,2)
  const filePath = dirPath + '/' + hash.substring(2, 41)

  fs.readFile(filePath, function(err, res) {
    console.log(res.toString('latin1'));

    if(err) throw err
    zlib.inflate(res, function (err, result) {
      console.log(result)
      if(err) throw err
      console.log(result.toString());
      // 最初のnull byteを見つけてくれる。
      console.log(result.indexOf('\0'))
      console.log(result.slice(result.indexOf('\0') + 1).toString())

    });
  })
}

inflate()

これでファイルをinflateした状態で確認することができます。

やっていることは単純で、20byteのhashを受け取りファイルのpathを計算し、そのファイルを読み込みzlibでinflateします。

161e899ffc6e06b5a8f94b77c99312c30deb9452を読み込ませます。

$ node inflate.js 161e899ffc6e06b5a8f94b77c99312c30deb9452
x+)JMU06g040031Q(N�-�I��*fx�W*k�$�`�'��\4?���
Vr�
<Buffer 74 72 65 65 20 33 37 00 31 30 30 36 34 34 20 73 61 6d 70 6c 65 2e 6a 73 00 ea 8e 75 1d 31 e4 58 30 b3 ac e4 d1 23 8a 44 29 f3 fb 18 f5>
tree 37100644 sample.js�u1�X0����#�D)���
7
100644 sample.js�u1�X0����#�D)���

最初の出力結果はzlibをかまさない結果です。

x+)JMU06g040031Q(N�-�I��*fx�W*k�$�`�'��\4?���
Vr�

zlibでinfaleteした結果のbufferです。

<Buffer 74 72 65 65 20 33 37 00 31 30 30 36 34 34 20 73 61 6d 70 6c 65 2e 6a 73 00 ea 8e 75 1d 31 e4 58 30 b3 ac e4 d1 23 8a 44 29 f3 fb 18 f5>

headerを取らないでinflateしたbufferをstringにした出力結果です。

tree 37100644 sample.js�u1�X0����#�D)���

今のとことはこれをみていれば良いでしょう。

先ほど確認したところ、treeのファイル構造は下記のようになっていました

tree <content length><NUL><file mode> <filename><NUL><item sha>...

inflateした結果と比べてみましょう。treeという文字列の後に37という数値、それからファイルのmodeを表す100644が来ています。その後半角スペースがあります。 おかしいですね。sha1 hashがあるはずが文字化けしています。本来ならこの結果はcat-fileした時に表示されるhashのea8e751d31e45830b3ace4d1238a4429f3fb18f5が来て欲しいのですが、ありません。

tree 37100644 sample.js�u1�X0����#�D)���

inflateした結果のbufferをみてみましょう。何か見えてきませんか?そう最後の20byteがea8e751d31e45830b3ace4d1238a4429f3fb18f5と一致していることに気づくでしょう。

<Buffer 74 72 65 65 20 33 37 00 31 30 30 36 34 34 20 73 61 6d 70 6c 65 2e 6a 73 00 ea 8e 75 1d 31 e4 58 30 b3 ac e4 d1 23 8a 44 29 f3 fb 18 f5>

hashの前の部分を文字列で作りbufferに変換して、最後にhashをそのままbufferにしたものと連結すればうまくいきそうです。

treeファイルを実装してみる。

さてとりあえず実装してみましょう。このファイルhashが同じ161e899ffc6e06b5a8f94b77c99312c30deb9452と一致するものが生成できたらクリアです。今の段階では実際に保存する必要がないので、コメントアウトしておきます。

まずこれです。これはbody部の1ファイル分のbufferを生成します。fileContentの想定される引数は、{name: "sample.js", mode: 100644, sha1: "ea8e751d31e45830b3ace4d1238a4429f3fb18f5"}です。

function calcContent (fileContent) {
  const fileMode = fileContent.mode //100644
  const fileName = fileContent.name // sample.js
  const fileHash = fileContent.sha1 // ea8e751d31e45830b3ace4d1238a4429f3fb18f5
  const hash = Buffer.from(fileHash, "hex")
  const content = Buffer.from(`${fileMode} ${fileName}\0`) // modeとnameの間に半角スペースを開ける。

  const buffer = Buffer.concat([content, hash], hash.length + content.length)
  return buffer
}

具体的には下記のようなtreeオブジェクトの構造のうち

tree <content length><NUL><file mode> <filename><NUL><item sha>...

このひとまとまりを生成します。

<file mode> <filename><NUL><item sha>

注意するのは下記の部分です。これは、"ea8e751d31e45830b3ace4d1238a4429f3fb18f5"<Buffer ea 8e 75 1d 31 e4 58 30 b3 ac e4 d1 23 8a 44 29 f3 fb 18 f5>に変換する為にhexオプションをつけています。それ以外は先ほどstring()メソッドを使うことで文字列にできたので、普通にbufferに変換します。 `

 const hash = Buffer.from(fileHash, "hex")

次はこの関数を足します。{name: "sample.js", mode: 100644, sha1: "ea8e751d31e45830b3ace4d1238a4429f3fb18f5"}を配列で引数にとり、先ほどのcalcContentで<file mode> <filename><NUL><item sha>を生成して、最終的に全てのbufferを連結します。

function calcContents (fileContents=[]) {
  return fileContents.reduce((acc, file) => {
    const content = calcContent(file)
    return Buffer.concat([acc, content], acc.length + content.length)
  }, Buffer.alloc(0))
}

最後に上記二つの関数を使う関数を定義します。ここでは、calcContentsでbody部分のbufferを作った後にheader部分と連結しています。それをあとはaddで作成したようにsha1 hashを計算しています。本当ならば、headerとcontentを合わせたものをzlibで圧縮して保存するのですが、まずはこれで正しくファイルが作れているのかわからないので実行してみます。デフォルトで引数を与えているのは検証用です。

async function genTree (fileContents=[{name: "sample.js", mode: 100644, sha1: "ea8e751d31e45830b3ace4d1238a4429f3fb18f5"}]) {
  const content = calcContents(fileContents)
  const header= Buffer.from(`tree ${content.length}\0`)
  const store = Buffer.concat([header, content], header.length + content.length);
  const shasum = crypto.createHash('sha1');
  shasum.update(store);
  const sha1 = shasum.digest('hex')
  // zlib.deflate(store, async function (err, result) { // bufferを引数で取れる!! https://nodejs.org/api/zlib.html#zlib_class_zlib_deflate 便利!
  //   dirPath = __dirname + '/.git/objects/' + sha1.substring(0,2)
  //   filePath = dirPath + '/' + sha1.substring(2, 40)
  //   await fs.mkdir(dirPath, { recursive: true }, (err) => {
  //     if (err) throw err;
  //   });
  //   fs.writeFile(filePath, result, function (err) {
  //     if (err) throw err;
  //     console.log('Saved!');
  //   })
  // });
  console.log(sha1)
  return sha1;
}

161e899ffc6e06b5a8f94b77c99312c30deb9452と同じ出力結果が返ってくる為、正しくtreeオブジェクトが作れていることがわかります。(gitのobjectはファイルの内容とheaderを足してhashを計算する為、hashが同じなら同じ内容で作れている。)

$ node tree.js
161e899ffc6e06b5a8f94b77c99312c30deb9452

commitオブジェクトの生成を実装する。

それではcommitオブジェクトを作ります。最初に完成コードを乗せます。

tree.js

const crypto = require('crypto');
const fs = require('fs').promises;
const zlib = require('zlib');

async function genTree (fileContents=[]) {
  const content = calcContents(fileContents)
  const header= Buffer.from(`tree ${content.length}\0`)
  const store = Buffer.concat([header, content], header.length + content.length);
  const shasum = crypto.createHash('sha1');
  shasum.update(store);
  const sha1 = shasum.digest('hex')
  // zlib.deflate(store, async function (err, result) { // bufferを引数で取れる!! https://nodejs.org/api/zlib.html#zlib_class_zlib_deflate 便利!
  //   dirPath = __dirname + '/.git/objects/' + sha1.substring(0,2)
  //   filePath = dirPath + '/' + sha1.substring(2, 40)
  //   await fs.mkdir(dirPath, { recursive: true }, (err) => {
  //     if (err) throw err;
  //   });
  //   fs.writeFile(filePath, result, function (err) {
  //     if (err) throw err;
  //     console.log('Saved!');
  //   })
  // });
  return sha1;
}

function calcContents (fileContents=[]) {
  return fileContents.reduce((acc, file) => {
    const content = calcContent(file)
    return Buffer.concat([acc, content], acc.length + content.length)
  }, Buffer.alloc(0))
}

function calcContent (fileContent) {
  const fileMode = fileContent.mode //100644
  const fileName = fileContent.name // sample.js
  const fileHash = fileContent.sha1 // 52679e5d3d185546a06f54ac40c1c652e33d7842
  const hash = Buffer.from(fileHash, "hex")
  const content = Buffer.from(`${fileMode} ${fileName}\0`) // modeとnameの間に半角スペースを開ける。

  const buffer = Buffer.concat([content, hash], hash.length + content.length)
  return buffer
}

module.exports = {
  genTree
}

commit.js

const { genTree } = require('./tree.js')
const fs = require('fs').promises;
const crypto = require('crypto');
const zlib = require('zlib');

async function getFileInfoFromIndex () {
  const message = process.argv[2]
  const indexFile = await fs.readFile(".git/index")
  const header = indexFile.slice(0, 12)
  let body = indexFile.slice(12)
  const fileNum = parseInt(header.slice(8, 12).toString("hex"), 16)
  const fileInfo = []
  console.log(fileNum)
  for (let i = 0; i < fileNum; i++) {
    const mode = parseInt(body.slice(24, 28).toString('hex'), 16).toString(8)

    const sha1 = body.slice(40, 60).toString('hex')

    const flag = body.slice(60, 62)
    const fileLength =parseInt(flag.toString("hex"), 16) & 0b0011111111111111

    const name = body.slice(62, 62+fileLength).toString()
    const zeroPadding = 8 - ((62+fileLength) % 8)
    fileInfo.push({mode, sha1, name})
    body = body.slice(62+fileLength+zeroPadding)
  }
  const treeHash = await genTree(fileInfo)
  genCommitObject(treeHash, message)
}

async function genCommitObject (treeSha1, commitMessage) {
  const commitTime = (Date.now() / 1000).toFixed(0)
  const content = `tree ${treeSha1}\n` +
  `author hirokihello <iammyeye1@gmail.com> ${commitTime} +0900\n` +
  `committer hirokihello <iammyeye1@gmail.com> ${commitTime} +0900\n` +
    "\n" +
    `${commitMessage}\n`

  const header= `commit ${content.length}\0`
  const store = header + content
  const shasum = crypto.createHash('sha1');
  shasum.update(store);
  const commitSha1 = shasum.digest('hex')

  // zlib.deflate(store, async function (err, result) { // bufferを引数で取れる!! https://nodejs.org/api/zlib.html#zlib_class_zlib_deflate 便利!
  //   dirPath = __dirname + '/.git/objects/' +  commitSha1.substring(0,2)
  //   filePath = dirPath + '/' +  commitSha1.substring(2, 40)
  //   await fs.mkdir(dirPath, { recursive: true }, (err) => {
  //     if (err) throw err;
  //   });
  //   fs.writeFile(filePath, result, function (err) {
  //     if (err) throw err;
  //     console.log('Saved!');
  //   })
  // });

  console.log(commitSha1)
}

getFileInfoFromIndex()

commitオブジェクトのフォーマット

この記事によればcommitオブジェクトは下記のフォーマットとなっています。 www.dulwich.io

commit <content length><NUL>tree <tree sha>
parent <parent sha>
[parent <parent sha> if several parents from merges]
author <author name> <author e-mail> <timestamp> <timezone>
committer <author name> <author e-mail> <timestamp> <timezone>

<commit message>

さて正しいのでしょうか。先ほど作成したinflate.jsを使って実際のファイルの形式をみていきます。今回作りたいhashの値は0e95049453fa4d33b5c1ceedb042181fa4af0c40ですので、これをzlibでinflateしたバッファー、文字列などをみていきます。

$ node inflate.js 0e95049453fa4d33b5c1ceedb042181fa4af0c40
x��I                                     ��\"Z��V|p��,*|FiJ��U
"��O�33Yu��&�YD4ޣupFBT����d�k�5�f�V�'�9Y�;                    �{�k
<Buffer 63 6f 6d 6d 69 74 20 31 37 39 00 74 72 65 65 20 31 36 31 65 38 39 39 66 66 63 36 65 30 36 62 35 61 38 66 39 34 62 37 37 63 39 39 33 31 32 63 33 30 64 ... 140 more bytes>
commit 179tree 161e899ffc6e06b5a8f94b77c99312c30deb9452
author hirokihello <iammyeye1@gmail.com> 1600588067 +0900
committer hirokihello <iammyeye1@gmail.com> 1600588067 +0900

first commit

10
tree 161e899ffc6e06b5a8f94b77c99312c30deb9452
author hirokihello <iammyeye1@gmail.com> 1600588067 +0900
committer hirokihello <iammyeye1@gmail.com> 1600588067 +0900

first commit

bash-3.2$

結果を一つづつみていきましょう。

これがinflateする前のファイルです。読み込めません。

x��I                                     ��\"Z��V|p��,*|FiJ��U
"��O�33Yu��&�YD4ޣupFBT����d�k�5�f�V�'�9Y�;                    �{�k

これがzlibでinflateした後のバッファです。

<Buffer 63 6f 6d 6d 69 74 20 31 37 39 00 74 72 65 65 20 31 36 31 65 38 39 39 66 66 63 36 65 30 36 62 35 61 38 66 39 34 62 37 37 63 39 39 33 31 32 63 33 30 64 ... 140 more bytes>

これがinflateした後のバッファを無加工でstringに変換したものです。気になる点として、最後に空行が入っています。

commit 179tree 161e899ffc6e06b5a8f94b77c99312c30deb9452
author hirokihello <iammyeye1@gmail.com> 1600588067 +0900
committer hirokihello <iammyeye1@gmail.com> 1600588067 +0900

first commit

最初のnull byteの位置です。

10

null byteより前(header)を取り除いてstringにしたものです。

tree 161e899ffc6e06b5a8f94b77c99312c30deb9452
author hirokihello <iammyeye1@gmail.com> 1600588067 +0900
committer hirokihello <iammyeye1@gmail.com> 1600588067 +0900

first commit

先ほど見たフォーマットと比べてみます。

commit <content length><NUL>tree <tree sha>
parent <parent sha>
[parent <parent sha> if several parents from merges]
author <author name> <author e-mail> <timestamp> <timezone>
committer <author name> <author e-mail> <timestamp> <timezone>

<commit message>

ファイルの最後に空行がある以外は同じですね。どちらが正しいのでしょうか。

commitオブジェクトを実装してみる。

さてとりあえず上記の結果を踏まえて、commitオブジェクトを生成する部分を実装してみます。

const fs = require('fs').promises;
const crypto = require('crypto');
const zlib = require('zlib');

async function genCommitObject (treeSha1, commitMessage) {
  const commitTime = (Date.now() / 1000).toFixed(0)
  const content = `tree ${treeSha1}\n` +
  `author hirokihello <iammyeye1@gmail.com> ${commitTime} +0900\n` +
  `committer hirokihello <iammyeye1@gmail.com> ${commitTime} +0900\n` +
    "\n" +
    `${commitMessage}\n`

  const header= `commit ${content.length}\0`
  const store = header + content
  const shasum = crypto.createHash('sha1');
  shasum.update(store);
  const commitSha1 = shasum.digest('hex')

  // zlib.deflate(store, async function (err, result) { // bufferを引数で取れる!! https://nodejs.org/api/zlib.html#zlib_class_zlib_deflate 便利!
  //   dirPath = __dirname + '/.git/objects/' +  commitSha1.substring(0,2)
  //   filePath = dirPath + '/' +  commitSha1.substring(2, 40)
  //   await fs.mkdir(dirPath, { recursive: true }, (err) => {
  //     if (err) throw err;
  //   });
  //   fs.writeFile(filePath, result, function (err) {
  //     if (err) throw err;
  //     console.log('Saved!');
  //   })
  // });

  const refsPath = ".git/refs/heads/master"
   fs.writeFile(refsPath, commitSha1, function (err) {
     if (err) throw err;
     console.log('Saved!');
   })

  console.log(commitSha1)
}

ここでやっていることは至極単純です。先ほどinflateした結果を文字列として突っ込みcontentとして入れ、残りはいつものobjectsを生成する手順でhashにして返しているだけです。メールアドレスとユーザー名は今回は固定にしています。

  const commitTime = (Date.now() / 1000).toFixed(0)

この部分がなぜこのような実装になるのでしょうか。現在時刻をunixタイムで取得すると下記になりました。実際のコミットオブジェクトに記載されていた日付の部分を見ると1600588067となっています。indexの実装の時もそうですが、unixtimeを記入するときはm秒以下は切り捨てるようです。

Date.now()
=> 1600622610846

一旦引数を固定値にして、先ほどのcommitオブジェクトに記載されていた日付、hash, commit messageにして同じhashが生成されるか確認してみましょう。

commit.js

const fs = require('fs').promises;
const crypto = require('crypto');
const zlib = require('zlib');

async function genCommitObject (treeSha1="161e899ffc6e06b5a8f94b77c99312c30deb9452", commitMessage="first commit") {
  const commitTime = (Date.now() / 1000).toFixed(0)
  const content = `tree ${treeSha1}\n` +
  `author hirokihello <iammyeye1@gmail.com> 1600588067 +0900\n` +
  `committer hirokihello <iammyeye1@gmail.com> 1600588067 +0900\n` +
    "\n" +
    `${commitMessage}\n`

  const header= `commit ${content.length}\0`
  const store = header + content
  const shasum = crypto.createHash('sha1');
  shasum.update(store);
  const commitSha1 = shasum.digest('hex')

  // zlib.deflate(store, async function (err, result) { // bufferを引数で取れる!! https://nodejs.org/api/zlib.html#zlib_class_zlib_deflate 便利!
  //   dirPath = __dirname + '/.git/objects/' +  commitSha1.substring(0,2)
  //   filePath = dirPath + '/' +  commitSha1.substring(2, 40)
  //   await fs.mkdir(dirPath, { recursive: true }, (err) => {
  //     if (err) throw err;
  //   });
  //   fs.writeFile(filePath, result, function (err) {
  //     if (err) throw err;
  //     console.log('Saved!');
  //   })
  // });

  const refsPath = ".git/refs/heads/master"
   fs.writeFile(refsPath, commitSha1, function (err) {
     if (err) throw err;
     console.log('Saved!');
   })

  console.log(commitSha1)
}

genCommitObject()
$ node commit.js
0e95049453fa4d33b5c1ceedb042181fa4af0c40

先ほどの.git/refs/heads/masterに記載されていた結果と同じhashが得られました。

よって正しそうです。

先ほど参照したドキュメントにある通り、content内部の最後に改行を入れないで実装してみます。

const fs = require('fs').promises;
const crypto = require('crypto');
const zlib = require('zlib');

async function genCommitObject (treeSha1="161e899ffc6e06b5a8f94b77c99312c30deb9452", commitMessage="first commit") {
  const commitTime = (Date.now() / 1000).toFixed(0)
  const content = `tree ${treeSha1}\n` +
  `author hirokihello <iammyeye1@gmail.com> 1600588067 +0900\n` +
  `committer hirokihello <iammyeye1@gmail.com> 1600588067 +0900\n` +
    "\n" +
    `${commitMessage}`

  const header= `commit ${content.length}\0`
  const store = header + content
  const shasum = crypto.createHash('sha1');
  shasum.update(store);
  const commitSha1 = shasum.digest('hex')

  // zlib.deflate(store, async function (err, result) { // bufferを引数で取れる!! https://nodejs.org/api/zlib.html#zlib_class_zlib_deflate 便利!
  //   dirPath = __dirname + '/.git/objects/' +  commitSha1.substring(0,2)
  //   filePath = dirPath + '/' +  commitSha1.substring(2, 40)
  //   await fs.mkdir(dirPath, { recursive: true }, (err) => {
  //     if (err) throw err;
  //   });
  //   fs.writeFile(filePath, result, function (err) {
  //     if (err) throw err;
  //     console.log('Saved!');
  //   })
  // });

  const refsPath = ".git/refs/heads/master"
   fs.writeFile(refsPath, commitSha1, function (err) {
     if (err) throw err;
     console.log('Saved!');
   })

  console.log(commitSha1)
}

genCommitObject()

実行するとhashが期待したものと変わってしまいました。行末の改行は必須のようです。

$ node commit.js
75b4fad1f9c26fc2c0cbdb2f4f486c1262eba5ac

よってコミットオブジェクトの生成部分の実装はこのようになります。

const fs = require('fs').promises;
const crypto = require('crypto');
const zlib = require('zlib');

async function genCommitObject (treeSha1, commitMessage) {
  const commitTime = (Date.now() / 1000).toFixed(0)
  const content = `tree ${treeSha1}\n` +
  `author hirokihello <iammyeye1@gmail.com> ${commitTime} +0900\n` +
  `committer hirokihello <iammyeye1@gmail.com> ${commitTime} +0900\n` +
    "\n" +
    `${commitMessage}\n`

  const header= `commit ${content.length}\0`
  const store = header + content
  const shasum = crypto.createHash('sha1');
  shasum.update(store);
  const commitSha1 = shasum.digest('hex')

  // zlib.deflate(store, async function (err, result) { // bufferを引数で取れる!! https://nodejs.org/api/zlib.html#zlib_class_zlib_deflate 便利!
  //   dirPath = __dirname + '/.git/objects/' +  commitSha1.substring(0,2)
  //   filePath = dirPath + '/' +  commitSha1.substring(2, 40)
  //   await fs.mkdir(dirPath, { recursive: true }, (err) => {
  //     if (err) throw err;
  //   });
  //   fs.writeFile(filePath, result, function (err) {
  //     if (err) throw err;
  //     console.log('Saved!');
  //   })
  // });

  const refsPath = ".git/refs/heads/master"
   fs.writeFile(refsPath, commitSha1, function (err) {
     if (err) throw err;
     console.log('Saved!');
   })
  console.log(commitSha1)
}

genTreeとgenCommitObjectを接続する。

さてあとはtreeSha1とcommitMessageをgenCommitObjectに渡してあげるだけです。よって下記を追加します。

一見複雑ですが、やっていることは単純です。indexファイルをreadして、中身をヘッダーとボディに分けます。

headerからcommitする必要のあるファイル数を取り出したら、あとはbodyのentriesをそのファイル数の数だけparseして情報を取り出します。それを先ほど実装したtreeを作り出す関数genTreeに渡し、その返り値のhash、そしてコミットmessageをcommitObjectを生成するgenCommitObjectに渡してあげているだけです。

indexファイルの仕様については下記の前回の記事を参照してください。

system.blog.uuum.jp

async function getFileInfoFromIndex () {
  const message = process.argv[2]
  const indexFile = await fs.readFile(".git/index")
  const header = indexFile.slice(0, 12)
  let body = indexFile.slice(12)
  const fileNum = parseInt(header.slice(8, 12).toString("hex"), 16)
  const fileInfo = []
  for (let i = 0; i < fileNum; i++) {
    const mode = parseInt(body.slice(24, 28).toString('hex'), 16).toString(8)

    const sha1 = body.slice(40, 60).toString('hex')

    const flag = body.slice(60, 62)
    const fileLength =parseInt(flag.toString("hex"), 16) & 0b0011111111111111

    const name = body.slice(62, 62+fileLength).toString()
    const zeroPadding = 8 - ((62+fileLength) % 8)
    fileInfo.push({mode, sha1, name})
    body = body.slice(62+fileLength+zeroPadding)
  }
  const treeHash = await genTree(fileInfo)

  genCommitObject(treeHash, message)
}

具体的にみていきます。ここは簡単ですね。indexの仕様通りに情報を取り出しています。ここら辺は前回の記事をみてください。parseInt(header.slice(8, 12).toString("hex"), 16)ここが曲者ですが、これも仕様です。ヘッダーの8バイト目から12バイト目にWriteUInt32メソッドを使って書き込んでいるので、その数値をstringにする際にそのままの文字列として取り出し、それを16進数として扱い10進数にparseIntしています。

async function getFileInfoFromIndex () {
  const message = process.argv[2]
  const indexFile = await fs.readFile(".git/index")
  const header = indexFile.slice(0, 12)
  let body = indexFile.slice(12)
  const fileNum = parseInt(header.slice(8, 12).toString("hex"), 16)
}

次はここです。ここは簡単です。エントリーの仕様に乗っ取り、ファイルの回数分loopを回してmode, sha1, nameをそれぞれ取り出して、 配列fileInfoに格納しています。情報がどのbyteにあるか、可変長であるentriesの長さとファイル名の取得などは前回の記事でやった通りなので、覚えていない方は前回の記事を参照していただけると幸いです。一つ面白い?点として、parseInt(flag.toString("hex"), 16) & 0b0011111111111111で10進数と2進数でbit演算をしているのですが、自動で結果を10進数に直してくれます。

  const fileInfo = []
  for (let i = 0; i < fileNum; i++) {
    const mode = parseInt(body.slice(24, 28).toString('hex'), 16).toString(8)

    const sha1 = body.slice(40, 60).toString('hex')

    const flag = body.slice(60, 62)
    const fileLength =parseInt(flag.toString("hex"), 16) & 0b0011111111111111

    const name = body.slice(62, 62+fileLength).toString()
    const zeroPadding = 8 - ((62+fileLength) % 8)
    fileInfo.push({mode, sha1, name})
    body = body.slice(62+fileLength+zeroPadding)
  }

最後に先ほど実装したtree生成のコマンドを呼び出し、そのオブジェクトのhashをgenCommitObjectに渡すだけです。

  const treeHash = await genTree(fileInfo)
  genCommitObject(treeHash, message)

まとめ

これでひとまずcommitオブジェクトの生成のコアの全ての実装が終わりました。

最終的なコードは下記になります。

authorとmail addressとtimestampは、それぞれの実行タイミング・環境で異なりますので適宜変更してください。

commit.js

const { genTree } = require('./tree.js')
const fs = require('fs').promises;
const crypto = require('crypto');

async function getFileInfoFromIndex () {
  const message = process.argv[2]
  const indexFile = await fs.readFile(".git/index")
  const header = indexFile.slice(0, 12)
  let body = indexFile.slice(12)
  const fileNum = parseInt(header.slice(8, 12).toString("hex"))
  const fileInfo = []
  console.log(fileNum)
  for (let i = 0; i < fileNum; i++) {
    const mode = parseInt(body.slice(24, 28).toString('hex'), 16).toString(8)

    const sha1 = body.slice(40, 60).toString('hex')

    const flag = body.slice(60, 62)
    const fileLength =parseInt(flag.toString("hex"), 16) & 0b0011111111111111

    const name = body.slice(62, 62+fileLength).toString()
    const zeroPadding = 8 - ((62+fileLength) % 8)
    fileInfo.push({mode, sha1, name})
    body = body.slice(62+fileLength+zeroPadding)
  }
  const treeHash = await genTree(fileInfo)
  genCommitObject(treeHash, message)
}

async function genCommitObject (treeSha1, commitMessage) {
  const commitTime = (Date.now() / 1000).toFixed(0)
  const content = `tree ${treeSha1}\n` +
    `author hirokihello <iammyeye1@gmail.com> 1600538469 +0900\n` +
    `committer hirokihello <iammyeye1@gmail.com> 1600538469 +0900\n` +
    "\n" +
    `${commitMessage}\n`

  const header= `commit ${content.length}\0`
  const store = header + content
  const shasum = crypto.createHash('sha1');
  shasum.update(store);
  const commitSha1 = shasum.digest('hex')

  // zlib.deflate(store, async function (err, result) { // bufferを引数で取れる!! https://nodejs.org/api/zlib.html#zlib_class_zlib_deflate 便利!
  //   dirPath = __dirname + '/.git/objects/' +  commitSha1.substring(0,2)
  //   filePath = dirPath + '/' +  commitSha1.substring(2, 40)
  //   await fs.mkdir(dirPath, { recursive: true }, (err) => {
  //     if (err) throw err;
  //   });
  //   fs.writeFile(filePath, result, function (err) {
  //     if (err) throw err;
  //     console.log('Saved!');
  //   })
  // });

  const refsPath = ".git/refs/heads/master"
   fs.writeFile(refsPath, commitSha1, function (err) {
     if (err) throw err;
     console.log('Saved!');
   })

  console.log(commitSha1)
}

getFileInfoFromIndex()

tree.js

const crypto = require('crypto');
const fs = require('fs').promises;
const zlib = require('zlib');

async function genTree (fileContents=[]) {
  const content = calcContents(fileContents)
  const header= Buffer.from(`tree ${content.length}\0`)
  const store = Buffer.concat([header, content], header.length + content.length);
  const shasum = crypto.createHash('sha1');
  shasum.update(store);
  const sha1 = shasum.digest('hex')
  zlib.deflate(store, async function (err, result) { // bufferを引数で取れる!! https://nodejs.org/api/zlib.html#zlib_class_zlib_deflate 便利!
    dirPath = __dirname + '/.git/objects/' + sha1.substring(0,2)
    filePath = dirPath + '/' + sha1.substring(2, 40)
    await fs.mkdir(dirPath, { recursive: true }, (err) => {
      if (err) throw err;
    });
    fs.writeFile(filePath, result, function (err) {
      if (err) throw err;
      console.log('Saved!');
    })
  });
  return sha1;
}

function calcContents (fileContents=[]) {
  return fileContents.reduce((acc, file) => {
    const content = calcContent(file)
    return Buffer.concat([acc, content], acc.length + content.length)
  }, Buffer.alloc(0))
}

function calcContent (fileContent) {
  const fileMode = fileContent.mode //100644
  const fileName = fileContent.name // sample.js
  const fileHash = fileContent.sha1 // 52679e5d3d185546a06f54ac40c1c652e33d7842
  const hash = Buffer.from(fileHash, "hex")
  const content = Buffer.from(`${fileMode} ${fileName}\0`) // modeとnameの間に半角スペースを開ける。

  const buffer = Buffer.concat([content, hash], hash.length + content.length)
  return buffer
}

module.exports = {
  genTree
}

今最終的にcommit.jsではそのcontentからhashを生成するようにしているので、hashの帰り値がcommitした.git/refs/heads/masterの示す0e95049453fa4d33b5c1ceedb042181fa4af0c40と一致すれば全ての内容が同じで作れていることになります。 実行してみます。

$ node commit.js 'first commit'
7c2a37f7dfc40c8d15455c9e2e1c5d6dad977ae2

あいません。genCommitObjectでcommitオブジェクトを作る際のtimestampが現在時刻を入れるようになっているので、gitコマンドを使って先ほど実際にcommitしたものとあいません。一旦timestampを1600588067で固定します。(それぞれのcommitオブジェクトに記載されているものにしてください)

node commit.js 'first commit'
0e95049453fa4d33b5c1ceedb042181fa4af0c40

treeオブジェクトの内容から作られるhashを含む、commit objectの内容から作られるcommit objectのhashがgit commandと同じものになったのでこれにて実装が正しいことが証明されました。

長い間お読みいただきありがとうございました!

これにて終了です!commitの実装(のコア)を行うことができました。

次回はlogsや、実際に保存するにあたりvalidationなど考慮すべきことを実装していきます。

addから始めるgit入門~gitをjsで実装してみよう~

こんにちは〜!! インターンの@hirokihelloです!!!

季節は早いものでuuumで働き出して、そしてエンジニアとして1年半が経とうとしています。早いですね〜〜〜。

開発していると当たり前に使っているgitですが、みなさんgitについてきちんと理解していますでしょうか?

先日自分のtwitterのTLにgitを実装してみたとのツイートが流れてきて、そういえば使い方はわかるけどどんな仕組みかちゃんと知らないなあと気づきました。

そんなわけで今日はgitのaddコマンドって?仕様は?実装は?などまとめてみます!

  1. addコマンドの挙動を探る
  2. javascriptでaddコマンド(正確にはコアの部分)を実装

の流れになっています。

gitの使い方の記事ではないのでご注意を!

今回は下記の環境で実装・検証を行っていきます。

$ git version
git version 2.21.1 (Apple Git-122.3)
$ node -v
v14.5.0

最終的なコードはここにあります!

github.com

addコマンドって何してるの?

docを読んでみる

まずaddコマンドは実際なにを行うコマンドなのでしょうか。

c言語のgit本体の実装を読むのが早いのですが、 めんどくさいので 今回は公式のサイトを取り上げてみましょう。

This command updates the index using the current content found in the working tree, to prepare the content staged for the next commit. It typically adds the current content of existing paths as a whole, but with some options it can also be used to add content with only part of the changes made to the working tree files applied, or remove paths that do not exist in the working tree anymore.

The "index" holds a snapshot of the content of the working tree, and it is this snapshot that is taken as the contents of the next commit. Thus after making any changes to the working tree, and before running the commit command, you must use the add command to add any new or modified files to the index.

This command can be performed multiple times before a commit. It only adds the content of the specified file(s) at the time the add command is run; if you want subsequent changes included in the next commit, then you must run git add again to add the new content to the index.

引用 git-scm.com

ふーむなるほど。(わからん)

注目して欲しいのは、以下の2点です。

  • This command updates the index using the current content found in the working tree
  • The "index" holds a snapshot of the content of the working tree, and it is this snapshot that is taken as the contents of the next commit

addコマンドがindexファイルを更新するコマンドであること、indexファイルが次のコミット用のファイルのスナップショットであることが述べられています。

挙動を確認する

実際にどんな風にファイルが書き換わっているのかみてみましょう。

$ mkdir test_git
$ cd test_git
$ git init
$ ls -la .git/
total 24
drwxr-xr-x   9 hirokihello  staff  288  9 15 20:45 ./
drwxr-xr-x  10 hirokihello  staff  320  9 15 20:45 ../
-rw-r--r--   1 hirokihello  staff   23  9 15 20:45 HEAD
-rw-r--r--   1 hirokihello  staff  137  9 15 20:45 config
-rw-r--r--   1 hirokihello  staff   73  9 15 20:45 description
drwxr-xr-x  13 hirokihello  staff  416  9 15 20:45 hooks/
drwxr-xr-x   3 hirokihello  staff   96  9 15 20:45 info/
drwxr-xr-x   4 hirokihello  staff  128  9 15 20:45 objects/
drwxr-xr-x   4 hirokihello  staff  128  9 15 20:45 refs/

indexというファイルはここでは見当たりませんね。

適当なファイルを作成してみます。

$ cat <<EOF > sample.js
console.log("hoge");
console.log("fuga");
EOF
$ node sample.js
hoge
fuga

この時点では何も追加されませんし更新は行われません。

$ ls -la .git/
total 24
drwxr-xr-x   9 hirokihello  staff  288  9 15 20:45 ./
drwxr-xr-x  10 hirokihello  staff  320  9 15 20:45 ../
-rw-r--r--   1 hirokihello  staff   23  9 15 20:45 HEAD
-rw-r--r--   1 hirokihello  staff  137  9 15 20:45 config
-rw-r--r--   1 hirokihello  staff   73  9 15 20:45 description
drwxr-xr-x  13 hirokihello  staff  416  9 15 20:45 hooks/
drwxr-xr-x   3 hirokihello  staff   96  9 15 20:45 info/
drwxr-xr-x   4 hirokihello  staff  128  9 15 20:45 objects/
drwxr-xr-x   4 hirokihello  staff  128  9 15 20:45 refs/

addコマンドを打ってみます。

$ git add sample.js

先ほどの.gitディレクトリをみてみましょう。

$ ls -la ./.git
total 32
drwxr-xr-x  10 hirokihello  staff  320  9 15 20:57 .
drwxr-xr-x  11 hirokihello  staff  352  9 15 20:55 ..
-rw-r--r--   1 hirokihello  staff   23  9 15 20:45 HEAD
-rw-r--r--   1 hirokihello  staff  137  9 15 20:45 config
-rw-r--r--   1 hirokihello  staff   73  9 15 20:45 description
drwxr-xr-x  13 hirokihello  staff  416  9 15 20:45 hooks
-rw-r--r--   1 hirokihello  staff  104  9 15 20:57 index
drwxr-xr-x   3 hirokihello  staff   96  9 15 20:45 info
drwxr-xr-x   5 hirokihello  staff  160  9 15 20:57 objects
drwxr-xr-x   4 hirokihello  staff  128  9 15 20:45 refs

indexファイルができました!またobjectsディレクトリも更新されていますね。 中身をみてみましょう。

$ cat ./.git/index
DIRC_`��#�_`��#���;���*{���

読み込めませんね。 このファイルを読み込む、 ls-filesというgitコマンドが公式で用意されているので使ってみます。

$ git ls-files --stage
100644 7b96e6fb0a0744f5d01bb735f1622f275b440d85 0       sample.js

謎の文字列100644, 7b96e6fb0a0744f5d01bb735f1622f275b440d85,そして数値の0、そして先ほど作成してaddしたsample.jsが見えます。

更新のあった.git/objectsディレクトリもみてみしょう。

$ ls -la .git/objects/
total 0
drwxr-xr-x   5 hirokihello  staff  160  9 15 20:57 .
drwxr-xr-x  10 hirokihello  staff  320  9 15 20:57 ..
drwxr-xr-x   3 hirokihello  staff   96  9 15 20:57 7b
drwxr-xr-x   2 hirokihello  staff   64  9 15 20:45 info
drwxr-xr-x   2 hirokihello  staff   64  9 15 20:45 pack

20:57(git addを行った時間)に7bが追加されていますね。7bの中身をみてみましょう。

bash-3.2$ ls -la .git/objects/7b/
total 8
drwxr-xr-x  3 hirokihello  staff   96  9 15 20:57 .
drwxr-xr-x  5 hirokihello  staff  160  9 15 20:57 ..
-r--r--r--  1 hirokihello  staff   45  9 15 20:57 96e6fb0a0744f5d01bb735f1622f275b440d85

むむ。ディレクトリ名の7bと中身の96e6fb0a0744f5d01bb735f1622f275b440d85を足すと、先ほどのgit ls-filesで出た結果の文字列と一致します。

$ git ls-files --stage
100644 7b96e6fb0a0744f5d01bb735f1622f275b440d85 0       sample.js

それでは.git/objects/7b/96e6fb0a0744f5d01bb735f1622f275b440d85をみてみましょう。

$ cat .git/objects/7b/96e6fb0a0744f5d01bb735f1622f275b440d85
xK��OR01bH��+��I���O�P��OOUҴ�BL+MO      ��

文字化けしていますね。

これを見るコマンドがgitには用意されています。

$ git cat-file -p 7b96e6fb0a0744f5d01bb735f1622f275b440d85
console.log("hoge");
console.log("fuga");

先ほど追加した、sample.jsがこの7b96e6fb0a0744f5d01bb735f1622f275b440d85に格納されているということがわかりました。

ここでsample.jsを変更してaddするとどうなるでしょうか。

$ echo 'console.log("hogefuga");' >> sample.js
$ git add sample.js
$ git ls-files --stage
100644 a9e94074dc086aec661591147de3e821fa87fb36 0       sample.js

hashが変わっていますね。

$ ls -la .git/objects/
total 0
drwxr-xr-x   6 hirokihello  staff  192  9 16 00:20 ./
drwxr-xr-x  10 hirokihello  staff  320  9 16 00:20 ../
drwxr-xr-x   3 hirokihello  staff   96  9 15 20:57 7b/
drwxr-xr-x   3 hirokihello  staff   96  9 16 00:20 ed/
drwxr-xr-x   2 hirokihello  staff   64  9 15 20:45 info/
drwxr-xr-x   2 hirokihello  staff   64  9 15 20:45 pack/

$ ls -la .git/objects/7b
-r--r--r--  1 hirokihello  staff  45  9 15 20:57 .git/objects/7b/96e6fb0a0744f5d01bb735f1622f275b440d85

$ git cat-file -p 7b96e6fb0a0744f5d01bb735f1622f275b440d85
console.log("hoge");
console.log("fuga");

$ ls -la .git/objects/a9/e94074dc086aec661591147de3e821fa87fb36
-r--r--r--  1 hirokihello  staff  51  9 16 00:20 .git/objects/a9/e94074dc086aec661591147de3e821fa87fb36

$ git cat-file -p a9e94074dc086aec661591147de3e821fa87fb36
console.log("hoge");
console.log("fuga");
console.log("hogefuga");

先ほど作られたhashはそのままで、新しくaddした時点のファイル情報を持ったobjectが作られました。

このようにgitでは、addコマンドを打つごとにobjectsにそのファイルのコピーが作られ、indexがそのobjects以下に作られたものを示すように更新されることがわかりました。

今回はこのobjectsの生成とindexの作成・更新部分のコアを実装することにします。(treeなどディレクトリ構造の保存については範囲外とします)

addコマンドを実装してみる

それでは具体的にどのように実装すればいいのでしょうか。

具体的なindexとobjectsに作られるファイルの仕様は下記に具体的に記述されています。

git-scm.com

github.com

今回は通常のファイル(blobと呼ばれています)で考えます。blobではないものは、シンボリックリンクファイルなどが該当しますが、これをgitに保存するのは通常のweb開発に置いてあまり多くないと思われますので今回はblob一本でいきます。

objects作成部分の実装

まずはじめに、objectsを作成できるようにadd.jsを作成します。 完成形のコードをのっけます。

add.js

const crypto = require('crypto');
const fs = require('fs').promises;
const zlib = require('zlib');

async function add (file) {
  const fileObj = await fs.readFile(file)
  const content = fileObj.toString()
  const header=`blob ${content.length}\0`
  const store = header + content;
  const shasum = crypto.createHash('sha1');
  shasum.update(store);
  const sha1 = shasum.digest('hex')

  zlib.deflate(store, async function (err, result) {
    dirPath = __dirname + '/.git/objects/' + sha1.substring(0,2)
    filePath = dirPath + '/' + sha1.substring(2, 40)
    await fs.mkdir(dirPath, { recursive: true }, (err) => {
      if (err) throw err;
    });
    fs.writeFile(filePath, result, function (err) {
      if (err) throw err;
      console.log('Saved!');
    })
  });
}

async function porcelainAdd () {
  if (process.argv.length <= 2) return console.log("error no file was added")
  await files.forEach(file => add(file))
}

porcelainAdd()

add.jsのporcelainAdd()では、引数のファイルのオブジェクトをadd関数を使って作成することができます。

使い方としては、

$ node add.js test.js

のように使います。

ここからは具体的なコードの解説をします。

まずobjectの仕様についてですが、

  1. ヘッダとファイルの中身からsha-1チェックサムの生成
  2. zlibを用いてこの新しいコンテンツを圧縮
  3. 1で求めたSHA-1ハッシュ値の最初の2文字をディレクトリ名で、残りの38文字はそのディレクトリ内のファイル名として2で求めた圧縮したコンテンツを保存

のようになっています。

ヘッダとファイルの中身からsha-1チェックサムの生成

まずヘッダは、 1. オブジェクトのタイプを表す文字列(blob) 2. スペースに続いてコンテンツのサイズ 3. 最後にヌルバイト

の三つからなっています。

sample.jsで考えてみます。sample.jsはこのようになっています。

console.log("hoge");
console.log("fuga");
console.log("hogefuga");

このファイルのコンテンツのサイズは

const fs = require('fs').promises;
async function add () {
  const file = await fs.readFile("sample.js")
  const content = file.toString()
  return content.length
}

で求めることができます。 よってheader部分の作成コードは

const fs = require('fs').promises;

async function add () {
  const file = await fs.readFile("sample.js")
  const content = file.toString()
  const header=`blob ${content.length}\0`
}

となります。

ここまできたら、ヘッダーとファイルの中身を結合させます。ファイルの中身は先ほど読み込んだconst content = file.toString()にあるので、足すだけで大丈夫です。

  const store = header + content;

sha-1ハッシュを求めるには下記のようにします。

  const crypto = require('crypto');

  const shasum = crypto.createHash('sha1');
  shasum.update(VARIABLE_YOU_NEED_TO_HASH);
  const sha1 = shasum.digest('hex')

よってここまでの最終的なコードは下記のようになります。

const crypto = require('crypto');
const fs = require('fs').promises;

async function add () {
  const file = await fs.readFile("sample.js")
  const content = file.toString()
  const header=`blob ${content.length}\0`
  const store = header + content;

  const shasum = crypto.createHash('sha1');
  shasum.update(store);
  const sha1 = shasum.digest('hex')
}

圧縮

圧縮ですが、zlibを用いてヘッダーとファイルの中身を足したものを圧縮します。 nodejsではzlibライブラリが標準で提供されています。

このように使います。

const zlib = require('zlib');

zlib.deflate(VARIABLE_YOU_NEED_DEFLATE, function (err, result) {})

よってこのようになります。

const crypto = require('crypto');
const fs = require('fs').promises;
const zlib = require('zlib');

async function add () {
  const file = await fs.readFile("sample.js")
  const content = file.toString()
  const header=`blob ${content.length}\0`
  const store = header + content;
  const shasum = crypto.createHash('sha1');
  shasum.update(store);
  const sha1 = shasum.digest('hex')

  zlib.deflate(store, async function (err, result) {
    // 具体的な処理
});
}

.git/objectsへの書き込み

ここでは先ほどdeflateした結果を、最初に求めたsha-1hash値の最初の2文字をディレクトリ名、残りの38文字をファイル名にして保存するだけです。

zlibの処理から書きます。

  zlib.deflate(store, async function (err, result) {
    dirPath = __dirname + '/.git/objects/' + sha1.substring(0,2)
    filePath = dirPath + '/' + sha1.substring(2, 40)
    await fs.mkdir(dirPath, { recursive: true }, (err) => {
      if (err) throw err;
    });
    fs.writeFile(filePath, result, function (err) {
      if (err) throw err;
      console.log('Saved!');
    })
  });

簡単ですね。

最終的にはsample.jsの固定の部分を受け取るようにします。

const crypto = require('crypto');
const fs = require('fs').promises;
const zlib = require('zlib');

async function add (file) {
  const fileObj = await fs.readFile(file)
  const content = fileObj.toString()
  const header=`blob ${content.length}\0`
  const store = header + content;
  const shasum = crypto.createHash('sha1');
  shasum.update(store);
  const sha1 = shasum.digest('hex')

  zlib.deflate(store, async function (err, result) {
    dirPath = __dirname + '/.git/objects/' + sha1.substring(0,2)
    filePath = dirPath + '/' + sha1.substring(2, 40)
    await fs.mkdir(dirPath, { recursive: true }, (err) => {
      if (err) throw err;
    });
    fs.writeFile(filePath, result, function (err) {
      if (err) throw err;
      console.log('Saved!');
    })
  });
}

async function porcelainAdd () {
  if (process.argv.length <= 2) return console.log("error no file was added")
  await files.forEach(file => add(file))
}

porcelainAdd()

実際に同じものができているか検証してみましょう。add.jsを呼び出す時に、内部に関数の実行を忘れずに。

先ほどaddした時に作られたobjectsのhash値はa9e94074dc086aec661591147de3e821fa87fb36でした。同じhashになれば同じheader, contentsとして保存されており、cat-fileで中身が見れればきちんとdeflateができているということになります。

$ rm -rf .git
$ git init
$ node add.js sample.js
$ ls -la .git/objects/a9
-rw-r--r--  1 hirokihello  staff  50  9 16 02:02 .git/objects/a9/e94074dc086aec661591147de3e821fa87fb36
$ git cat-file -p a9e94074dc086aec661591147de3e821fa87fb36
console.log("hoge");
console.log("fuga");
console.log("hogefuga");

うまくいきましたね。これでobjectsを保存することができました。

index更新部分の実装

さて最後にして今回の山場です。

まずこいつらがどのようになっているのかについてみてみましょう。

.git/indexの中身

先ほど.git/indexを確認した時はバイナリファイルだったので、hexdumpしてみることにします。これは皆さんの環境で実行結果が異なります。

$ hexdump -C .git/index | head -n 50
00000000  44 49 52 43 00 00 00 02  00 00 00 01 5f 61 c1 fd  |DIRC........_a..|
00000010  08 f1   c6 d9  5f  61  c1 fd   08 f1 c6 d9 01 00 00 04  |...._a..........|
00000020  05 d5 ea 3b 00 00 81  a4  00 00 01 f5 00 00 00 14  |...;............|
00000030  00 00 00 43 a9 e9 40 74  dc 08 6a ec 66 15 91 14  |...C..@t..j.f...|
00000040  7d e3 e8 21   fa 87  fb  36 00 09 73 61 6d 70 6c 65  |}..!...6..sample|
00000050  2e 6a 73 00 79 e5 e8 a6   c3 81 2e 7f 61 20 cc 5a  |.js.y.......a .Z|
00000060  0f 15 b4 ae 37 ec 52 ec                           |....7.R.|
00000068

DIRCという文字列、sample.jsという文字列があることがわかります。

このindexについての仕様はgit公式のgithubにあります。

github.com

indexは大きく三つに分かれます。

  1. ヘッダー(12byte)
  2. エントリー(可変)
  3. sha-1チェックサム(20byte)

今回の例でいうと、 ヘッダー

44 49 52 43 00 00 00 02  00 00 00 01

エントリー

                                     5f 61 c1 fd
08 f1 c6 d9 5f 61 c1 fd  08 f1 c6 d9 01 00 00 04
05 d5 ea 3b 00 00 81 a4  00 00 01 f5 00 00 00 14
00 00 00 43 a9 e9 40 74  dc 08 6a ec 66 15 91 14
7d e3 e8 21 fa 87 fb 36  00 09 73 61 6d 70 6c 65
2e 6a 73 00

sha-1チェックサム

             a6  c3 81 2e 7f 61 20 cc 5a
0f 15 b4 ae 37 ec 52 ec

となります。

この最終的な実装は先ほど実装したobjectsの実装と合わせて下記となります。

add.js

const crypto = require('crypto');
const fs = require('fs').promises;
const zlib = require('zlib');


async function add (file) {
  const fileObj = await fs.readFile(file)
  const content = fileObj.toString()
  const header=`blob ${content.length}\0`
  const store = header + content;
  const shasum = crypto.createHash('sha1');
  shasum.update(store);
  const sha1 = shasum.digest('hex')

  zlib.deflate(store, async function (err, result) {
    dirPath = __dirname + '/.git/objects/' + sha1.substring(0,2)
    filePath = dirPath + '/' + sha1.substring(2, 40)
    await fs.mkdir(dirPath, { recursive: true }, (err) => {
      if (err) throw err;
    });
    fs.writeFile(filePath, result, function (err) {
      if (err) throw err;
      console.log('Saved!');
    })
  });
}

async function updateIndex (files) {
  const header = Buffer.alloc(12);
  const fileNum = files.length 

  header.write('DIRC', 0);
  header.writeUInt32BE(2, 4);
  header.writeUInt32BE(fileNum, 8);
  const entries = await Promise.all(entriesArray(files))

  const content = [header].concat(entries).reduce((accumulator, currentValue) =>{
    const length = currentValue.length + accumulator.length
    return Buffer.concat([accumulator, currentValue], length)
  })

  const hash = crypto.createHash('sha1')
  hash.update(content);
  const sha1 = Buffer.from(hash.digest('hex'), 'hex')

  const finalObj = Buffer.concat([content, sha1], content.length + sha1.length)

  fs.writeFile(".git/index", finalObj, function (err) {
    if (err) throw err;
    console.log('Saved!');
  })
}

function entriesArray(filePathArray) {
  return filePathArray.map(async filePath =>  {
    const statInfo = await fs.stat(filePath, {bigint: true})

    const ctime = parseInt((statInfo.ctime.getTime() / 1000 ).toFixed(0))
    const ctimeNs = parseInt(statInfo.ctimeNs  % 1000000000n) // 下9桁欲しい
    const mtime = parseInt((statInfo.mtime.getTime() / 1000 ).toFixed(0))
    const mtimeNs = parseInt(statInfo.mtimeNs % 1000000000n)
    const dev = parseInt(statInfo.dev)
    const ino = parseInt(statInfo.ino)
    const mode = parseInt(statInfo.mode)
    const uid = parseInt(statInfo.uid)
    const gid = parseInt(statInfo.gid)
    const size = parseInt(statInfo.size)

    const stat = Buffer.alloc(40);
    [
      ctime,
      ctimeNs,
      mtime,
      mtimeNs,
      dev,
      ino,
      mode,
      uid,
      gid,
      size,
    ].forEach((attr, idx) => stat.writeUInt32BE(attr, idx * 4))

    const sha1String = await genBlobSha1(filePath)
    const sha1 = Buffer.from(sha1String, 'hex')

    const assumeValid = 0b0 // 1 or 0 default is 0
    const extendedFlag = 0b0 // 1 or 0 default is 0
    const optionalFlag = (((0b0 | assumeValid) << 1) | extendedFlag) << 14

    const flagRes = optionalFlag | filePath.length
    const flag = Buffer.alloc(2)
    flag.writeUInt16BE(flagRes)

    const fileName = Buffer.from(filePath)
    const length = stat.length + sha1.length + flag.length + fileName.length
    const paddingCount = 8 - (length % 8)
    const padding = Buffer.alloc(paddingCount, '\0');
    const entry = Buffer.concat([stat, sha1, flag, fileName, padding], length + paddingCount)
    return entry
  })
}

async function genBlobSha1 (filePath) {
  const file = await fs.readFile(filePath)
  const content = file.toString()
  const header=`blob ${content.length}\0`
  const store = header + content;
  const shasum = crypto.createHash('sha1');
  shasum.update(store);
  const sha1 = shasum.digest('hex')

  return sha1
}

async function porcelainAdd () {
  if (process.argv.length <= 2) return console.log("error no file was added")
  const files = process.argv.slice(2).map(file => file.replace(/^\.\//, ""))
  await files.forEach(file => add(file))
  await updateIndex(files)
}

porcelainAdd()

ヘッダー

ヘッダー部分の仕様は下記のようになっています。

  1. 4-byte DIRCの文字列
  2. 4-byte バージョン(今回は2)
  3. 32-bit entriesの数

これだけです。

node標準のbufferクラスはhexバイナリを扱うことができるので、今回はbufferクラスで実装していきます。

実装はこのようになります。

async function updateIndex (files) {
  const header = Buffer.alloc(12);
  const fileNum = files.length

  header.write('DIRC', 0);
  header.writeUInt32BE(2, 4);
  header.writeUInt32BE(fileNum, 8);
}

ヘッダー用の12byteをBuffer.allocで確保します。fileNumは今回は一つとしますが、可変にしても構いません。

  const header = Buffer.alloc(12);
  const fileNum = files.length

bufferクラスのインスタンスのwriteメソッドとwriteUInt32BEで、それぞれ1バイト目から4byte, 4byte目からバージョンの2を4byte分(32bit=4byte)、8バイト目からファイル数を書き込みます。

  header.write('DIRC', 0);
  header.writeUInt32BE(2, 4);
  header.writeUInt32BE(fileNum, 8);

先ほど見たヘッダー部分のバイナリと一致するか確認しましょう。

先ほど見たヘッダー

44 49 52 43 00 00 00 02  00 00 00 01

ここまでのコード

async function updateIndex (files) {
  const header = Buffer.alloc(12);
  const fileNum = files.length

  header.write('DIRC', 0);
  header.writeUInt32BE(2, 4);
  header.writeUInt32BE(fileNum, 8);
}

updateIndex(process.argv[2])
$ node add.js sample.js
<Buffer 44 49 52 43 00 00 00 02 00 00 00 01>

一致することがわかります。

エントリー部分。

エントリー部分は下記のようになっています。

  - 32-bit ctime(stat(2) data)
  - 32-bit ctime nanosecond fractions(stat(2) data)
  - 32-bit mtime seconds(stat(2) data)
  - 32-bit mtime nanosecond fractions(stat(2) data)
  - 32-bit dev(stat(2) data)
  - 32-bit ino(stat(2) data)
  - 32-bit mode, split into (high to low bits)
    - 4-bit object type valid values in binary are 1000 (regular file), 1010 (symbolic link) and 1110 (gitlink)
    - 3-bit unused
    - 9-bit unix permission.
  - 32-bit uid(stat(2) data)
  - 32-bit gid(stat(2) data)
  - 32-bit file size(stat(2) data)
  - 160-bit SHA-1 for the represented object
  - A 16-bit 'flags' field split into (high to low bits)
    - 1-bit assume-valid flag
    - 1-bit extended flag (must be zero in version 2)
    - 2-bit stage (during merge)
    - 12-bit name length
  -   Entry path name
  -  1-8 nul bytes

基本的には、c言語で用意されているstatという構造体の通りに実装すれば大丈夫です。

nodeでは標準で、fsライブラリが提供されており、fs.statでファイル情報を参照することができます。

気になるのは、色々書いているmodeですが通常ファイルを今回は考えるのでfs.stat.modeをそのままstatの結果を入力すれば大丈夫です。

実装はこのようになります。

ファイルpathが複数渡されても良いように、arrayを受け取るようにしています。

function entriesArray(filePathArray) {
  return filePathArray.map(async filePath =>  {
    const statInfo = await fs.stat(filePath, {bigint: true})

    const ctime = parseInt((statInfo.ctime.getTime() / 1000 ).toFixed(0))
    const ctimeNs = parseInt(statInfo.ctimeNs  % 1000000000n) // 下9桁欲しい
    const mtime = parseInt((statInfo.mtime.getTime() / 1000 ).toFixed(0))
    const mtimeNs = parseInt(statInfo.mtimeNs % 1000000000n)
    const dev = parseInt(statInfo.dev)
    const ino = parseInt(statInfo.ino)
    const mode = parseInt(statInfo.mode)
    const uid = parseInt(statInfo.uid)
    const gid = parseInt(statInfo.gid)
    const size = parseInt(statInfo.size)

    const stat = Buffer.alloc(40);
    [
      ctime,
      ctimeNs,
      mtime,
      mtimeNs,
      dev,
      ino,
      mode,
      uid,
      gid,
      size,
    ].forEach((attr, idx) => stat.writeUInt32BE(attr, idx * 4))

    const sha1String = await genBlobSha1(filePath)
    const sha1 = Buffer.from(sha1String, 'hex')

    const assumeValid = 0b0 // 1 or 0 default is 0
    const extendedFlag = 0b0 // 1 or 0 default is 0
    const optionalFlag = (((0b0 | assumeValid) << 1) | extendedFlag) << 14

    const flagRes = optionalFlag | filePath.length
    const flag = Buffer.alloc(2)
    flag.writeUInt16BE(flagRes)

    const fileName = Buffer.from(filePath)
    const length = stat.length + sha1.length + flag.length + fileName.length
    const paddingCount = 8 - (length % 8)
    const padding = Buffer.alloc(paddingCount, '\0');
    const entry = Buffer.concat([stat, sha1, flag, fileName, padding], length + paddingCount)
    return entry
  })
}


async function genBlobSha1 (filePath) {
  const file = await fs.readFile(filePath)
  const content = file.toString()
  const header=`blob ${content.length}\0`
  const store = header + content;
  shasum.update(store);
  const sha1 = shasum.digest('hex')

  return sha1
}

ここではbigintオプションを渡しctimeなどのnano sec fractionsを取得できるようにします。

    const statInfo = await fs.stat(filePath, {bigint: true})

ここでstatのデータを取得し、加工しています。注意として、ctimeなどはm秒以下を切り捨て、それぞれ4byteに収める必要がある部分です。(デフォルトだとm秒以下もfs.stat.ctimeで取得されますが4byteに収まらないので切り捨て、m秒以下はnano sec fractionsで表現します)

    const ctime = parseInt((statInfo.ctime.getTime() / 1000 ).toFixed(0))
    const ctimeNs = parseInt(statInfo.ctimeNs  % 1000000000n) // 下9桁欲しい
    const mtime = parseInt((statInfo.mtime.getTime() / 1000 ).toFixed(0))
    const mtimeNs = parseInt(statInfo.mtimeNs % 1000000000n)
    const dev = parseInt(statInfo.dev)
    const ino = parseInt(statInfo.ino)
    const mode = parseInt(statInfo.mode)
    const uid = parseInt(statInfo.uid)
    const gid = parseInt(statInfo.gid)
    const size = parseInt(statInfo.size)

statで取得したデータを4byteづつ書き込んでいきます。

    const stat = Buffer.alloc(40);
    [
      ctime,
      ctimeNs,
      mtime,
      mtimeNs,
      dev,
      ino,
      mode,
      uid,
      gid,
      size,
    ].forEach((attr, idx) => stat.writeUInt32BE(attr, idx * 4))

その後先ほどobjectsを生成するときの方法と同じで算出したsha1 hashをそのままhexにします。 例えば、先ほど作成したsample.jsのobjectsの.git/objects/a9/e94074dc086aec661591147de3e821fa87fb36ですが、

エントリーのhexdumpに

                                     5f 60 db b7
08 f1 c6 d9  5f  61  c1 fd   08 f1 c6 d9 01 00 00 04
05 d5 ea 3b 00 00 81 a4  00 00 01 f5 00 00 00 14
00 00 00 43 a9 e9 40 74  dc 08 6a ec 66 15 91 14
7d e3 e8 21  fa  87  fb  36  00 09 73 61 6d 70 6c 65
2e 6a 73 00

hash名がそのままhexに変換されているのがわかると思います。

a6  c3 81 2e 7f 61 20 cc 5a
0f 15 b4 ae 37 ec 52 ec

コードはこのようになります。先ほどとほとんど同じなので解説はほとんどいらないでしょう。

const sha1String = await genBlobSha1(filePath)
const sha1 = Buffer.from(sha1String, 'hex')

async function genBlobSha1 (filePath) {
  const file = await fs.readFile(filePath)
  const content = file.toString()
  const header=`blob ${content.length}\0`
  const store = header + content;
  shasum.update(store);
  const sha1 = shasum.digest('hex')

  return sha1
}

Buffer.fromにhexオプションを渡すことで、文字列をそのままhexにしています。(ex. "aa" => )

Buffer.from(sha1String, 'hex')

あと少しです。

フラグを実装します。フラグはbit単位で指定がありbit演算が必要です。

    const assumeValid = 0b0 // 1 or 0 default is 0
    const extendedFlag = 0b0 // 1 or 0 default is 0
    const optionalFlag = (((0b0 | assumeValid) << 1) | extendedFlag) << 14

    const flagRes = optionalFlag | filePath.length
    const flag = Buffer.alloc(2)
    flag.writeUInt16BE(flagRes)

これで最後です。

file名をbufferにして、今まで作ったバッファーと合わせた全体のエントリーの長さを計算します。 8の倍数になるように、null byteでパディングを行います。(8の倍数の場合、8byteのnull byteを足す。)

    const fileName = Buffer.from(filePath)
    const length = stat.length + sha1.length + flag.length + fileName.length
    const paddingCount = 8 - (length % 8)
    const padding = Buffer.alloc(paddingCount, '\0');
    const entry = Buffer.concat([stat, sha1, flag, fileName, padding], length + paddingCount)
    return entry

最後に全てのbufferをくっつけてreturnすれば完了です。

sha-1チェックサム

ここでは、最終的にヘッダーと先ほど作ったエントリーを組み合わせてsha-1hashを作ります。そのhashをそのままhexにして、それを保存すれば終了です。

ここでは全体としてconcatメソッドを使ってbufferを結合しています。

async function updateIndex (files) {
  const header = Buffer.alloc(12);
  const fileNum = files.length

  header.write('DIRC', 0);
  header.writeUInt32BE(2, 4);
  header.writeUInt32BE(fileNum, 8);
  const entries = await Promise.all(entriesArray(files))

  const content = [header].concat(entries).reduce((accumulator, currentValue) =>{
    const length = currentValue.length + accumulator.length
    return Buffer.concat([accumulator, currentValue], length)
  })

  const hash = crypto.createHash('sha1')
  hash.update(content);
  const sha1 = Buffer.from(hash.digest('hex'), 'hex')

  const finalObj = Buffer.concat([content, sha1], content.length + sha1.length)

  fs.writeFile(".git/index", finalObj, function (err) {
    if (err) throw err;
    console.log('Saved!');
  })
}

完成

それでは実際に動かしてみましょう。

add.js

const crypto = require('crypto');
const fs = require('fs').promises;
const zlib = require('zlib');


async function add (file) {
  const fileObj = await fs.readFile(file)
  const content = fileObj.toString()
  const header=`blob ${content.length}\0`
  const store = header + content;
  const shasum = crypto.createHash('sha1');
  shasum.update(store);
  const sha1 = shasum.digest('hex')

  zlib.deflate(store, async function (err, result) {
    dirPath = __dirname + '/.git/objects/' + sha1.substring(0,2)
    filePath = dirPath + '/' + sha1.substring(2, 40)
    await fs.mkdir(dirPath, { recursive: true }, (err) => {
      if (err) throw err;
    });
    fs.writeFile(filePath, result, function (err) {
      if (err) throw err;
      console.log('Saved!');
    })
  });
}

async function updateIndex (files) {
  const header = Buffer.alloc(12);
  const fileNum = files.length /

  header.write('DIRC', 0);
  header.writeUInt32BE(2, 4);
  header.writeUInt32BE(fileNum, 8);
  const entries = await Promise.all(entriesArray(files))

  const content = [header].concat(entries).reduce((accumulator, currentValue) =>{
    const length = currentValue.length + accumulator.length
    return Buffer.concat([accumulator, currentValue], length)
  })

  const hash = crypto.createHash('sha1')
  hash.update(content);
  const sha1 = Buffer.from(hash.digest('hex'), 'hex')

  const finalObj = Buffer.concat([content, sha1], content.length + sha1.length)

  fs.writeFile(".git/index", finalObj, function (err) {
    if (err) throw err;
    console.log('Saved!');
  })
}

function entriesArray(filePathArray) {
  return filePathArray.map(async filePath =>  {
    const statInfo = await fs.stat(filePath, {bigint: true})

    const ctime = parseInt((statInfo.ctime.getTime() / 1000 ).toFixed(0))
    const ctimeNs = parseInt(statInfo.ctimeNs  % 1000000000n) // 下9桁欲しい
    const mtime = parseInt((statInfo.mtime.getTime() / 1000 ).toFixed(0))
    const mtimeNs = parseInt(statInfo.mtimeNs % 1000000000n)
    const dev = parseInt(statInfo.dev)
    const ino = parseInt(statInfo.ino)
    const mode = parseInt(statInfo.mode)
    const uid = parseInt(statInfo.uid)
    const gid = parseInt(statInfo.gid)
    const size = parseInt(statInfo.size)

    const stat = Buffer.alloc(40);
    [
      ctime,
      ctimeNs,
      mtime,
      mtimeNs,
      dev,
      ino,
      mode,
      uid,
      gid,
      size,
    ].forEach((attr, idx) => stat.writeUInt32BE(attr, idx * 4))

    const sha1String = await genBlobSha1(filePath)
    const sha1 = Buffer.from(sha1String, 'hex')

    const assumeValid = 0b0 // 1 or 0 default is 0
    const extendedFlag = 0b0 // 1 or 0 default is 0
    const optionalFlag = (((0b0 | assumeValid) << 1) | extendedFlag) << 14

    const flagRes = optionalFlag | filePath.length
    const flag = Buffer.alloc(2)
    // 16bitなのでこのメソッドを使う。writeIntメソッドもあるがrangeが-32768 < val< 32767で、assumeValid=1になった時flagは最低でも32769となり
    // エラーが出るのでwriteUInt16BEを使う。
    // ファイル名の制限は一旦なしで。
    flag.writeUInt16BE(flagRes)

    const fileName = Buffer.from(filePath)
    const length = stat.length + sha1.length + flag.length + fileName.length
    const paddingCount = 8 - (length % 8)
    const padding = Buffer.alloc(paddingCount, '\0');
    const entry = Buffer.concat([stat, sha1, flag, fileName, padding], length + paddingCount)
    return entry
  })
}


async function genBlobSha1 (filePath) {
  const file = await fs.readFile(filePath)
  const content = file.toString()
  const header=`blob ${content.length}\0`
  const store = header + content;
  const shasum = crypto.createHash('sha1');
  shasum.update(store);
  const sha1 = shasum.digest('hex')

  return sha1
}

async function porcelainAdd () {
  if (process.argv.length <= 2) return console.log("error no file was added")
  const files = process.argv.slice(2).map(file => file.replace(/^\.\//, ""))
  await files.forEach(file => add(file))
  await updateIndex(files)
}

porcelainAdd()

コマンドを実行してみます。

$ rm -rf .git
$ git init
$ node add.js sample.js 
$ git ls-files --stage
100644 a9e94074dc086aec661591147de3e821fa87fb36 0       sample.js
$ git cat-file -p a9e94074dc086aec661591147de3e821fa87fb36
console.log("hoge");
console.log("fuga");
console.log("hogefuga");
$ hexdump -C .git/index | head -n 50
00000000  44 49 52 43 00 00 00 02  00 00 00 01 5f 61 c1 fd  |DIRC........_a..|
00000010  08 f1 c6 d9 5f 61 c1 fd        08 f1 c6 d9 01 00 00 04  |...._a..........|
00000020  05 d5 ea 3b 00 00 81 a4    00 00 01 f5 00 00 00 14  |...;............|
00000030  00 00 00 43 a9 e9 40 74   dc 08 6a ec 66 15 91 14  |...C..@t..j.f...|
00000040  7d e3 e8 21 fa 87 fb 36      00 09 73 61 6d 70 6c 65  |}..!...6..sample|
00000050  2e 6a 73 00 79 e5 e8 a6    c3 81 2e 7f 61 20 cc 5a  |.js.y.......a .Z|
00000060  0f 15 b4 ae 37 ec 52 ec                           |....7.R.|
00000068

gitコマンドでaddするのと全く同じようにindex, objectsファイルを作れました!

これで終わりです!

今後はバリデーション部分やコミットの部分も作っていきたいですね!

ここまで読んでくださりありがとうございました!

reference

github.com

パケットキャプチャに入門しました

システムユニットの5n4wasa6です。

UUUMではゲームの運用も行なっており、最近アプリのNW対応をしていたエンジニアがおり、普段業務で、触ることのないパケットキャプチャの勉強をし他ので綴る。

※キャッシュ(CDN)対応をしていたので、パケチャは関係ないのだが...

UUUMには、福利厚生の一環でエンタメ費として10,000円まで支給される制度があり、攻殻機動隊員は、主に技術書を購入している(と思う)。

業務では、なかなか触れない領域の知識を身に着けることも非常に大事であるため、この制度を利用してインプットをしている。

詳しくはこちら

はじめに

私は、前々職でキャリアの法人営業をしており、

800程拠点のある運送業様のNW(主にWAN)を担当し、NWエンジニアと毎週謝りに手厚いサービスを提供していた。

一番記憶に残っているのは、3週連続土曜日に同拠点同区間で障害がおきたことで、原因は小動物(ネズミ)によるケーブル断線(地下埋設)。

解決策は、ケーブルに唐辛子(カプサイシン)を練りこんだカバーをおおうことで、障害はおさまった(笑)

マスタリングTCP/IPは数回読んだが、パケットキャプチャを使ったことがほぼなかったの今回はこちらを購入して実際にパケットキャプチャしてみた。

パケットキャプチャの教科書

パケチャ本まとめ (第3章まで)

第1章 パケットキャプチャの流れ

パケットキャプチャとは…

  • パケット => NWを介してやりとりされたデータ(小包)
  • キャプチャ => 取得する、捕捉する ex)画面キャプチャ

どんな時にパケットキャプチャするか?

[インフラシステム構築の6フェーズ]

①要件定義 -> ②基本設計 -> ③詳細設計 -> ④構築 -> ⑤試験 -> ⑥運用管理

  • パケットキャプチャが活躍するフェーズ
    • 試験フェーズ => 単体試験、正常試験、障害(冗長化)試験など各試験を実施するフェーズ

      • 試験にパスしなかった場合に、失敗原因を追求
      • 試験結果に関わらず、お客様にエビデンス提出することも有
    • 運用管理フェーズ => サービスインしたサービスを継続的かつ安定的に維持する

      • 設定変更
      • トラブル対応 ※パケットキャプチャが役立つのがこちら

パケットキャプチャツール

  • Wireshark

  • tcpdump → CLIでしか使用できない環境で利用 → パケットキャプチャした後に、SCPやFTPで移動して、Wireshark環境のあるPCで解析することもある)

  • Micsoft Message Analyzer → 本番商用環境等で、OSSを安易にインストールできない環境で利用 → tcpdumpと同様、キャプチャしたデータをWiresharkで解析することも可能

パケットキャプチャ手法の検討

  • パケットキャプチャ端末自身がやりとりしているパケットをキャプチャする
    • パケットキャプチャツールは、パケットキャプチャ端末自身が持っているNICを通過するパケットをソフトウェア的にインターセプト(傍受、盗み見)してキャプチャする。
    • パケットキャプチャの処理不可は気を付ける点であり、フィルタ条件を指定することで対象のパケットを絞り込むなどして、処理負荷の軽減を図る。
  • パケットキャプチャ端末とは別の端末がやりとりしているパケットをキャプチャする
    • この場合、自分のNICにパケットを流し込むためにの仕掛けが必要 リピータハブミラーポート(圧倒的にこちらが多い)

    リピータハブ… 受け取ったパケットのコピーを、そのままその他全てのポートに転送するネットワーク機器。(バカハブ)

    ミラーポート… 指定したポートでやりとりしているパケットを別のポートにリアルタイムにコピー。(SPAN)

最近では、ミラーポート機能を備えたスイッチが安価に入手できるため、ミラーポートが圧倒的に多いようです。 コストにかなりシビアで機能を求めていない企業様はまだ、バカハブを使っていると思われる。(数年前は結構使っている企業あった)

第2章 Wiresharkの使い方

Wiresharkのインストール https://www.wireshark.org/#download

wiresharkの見方や便利機能はこちらの記事を参考にしました。

物理アドレス解決(MAC)ネットワークアドレス解決(IP)の設定をしておくと、直感的でわかりやすい。

こんな感じですね。

【設定前】 設定前

【設定後】 設定後

第3章 Layer2プロトコル

  • レイヤー2プロトコル… 現代のNWで使用されているのは、Ethernet(有線LAN)とIEEE802.11(無線LAN)のどちらか

  • Ethernetプロトコル Ethernetネットワークを流れるパケットのことをEthernetフレームという

  • Ethernet Ⅱのフレームワークフォーマット

プリアンブル,宛先/送信元MACアドレス,タイプ,Ethernetペイロード,FCSの5つのフィールドで構成。 宛先/送信元MACアドレス,タイプを合わせてEthernetヘッダーと言う。

プリアンブル 「これからEhernetを送りますよー」と言う合図を意味する64ビット(8バイト)の特別なビットパターン。先頭から「1010101010」が7つ送られ、最後に「10101011」が1つ送られる。 受信側はこの特別なビットパターンを見て、「これからEhternetフレームが届くんだな」と判断。 ※Wiresharkでキャプチャする前に取り外される。

宛先/送信元MACアドレス Ehernetネットワークにおける住所のようなもの。 Ehernetネットワークに接続されている端末を表す48ビット(6バイト)の識別ID。

タイプ ネットワークそうでどんなプロトコルを使用しているかを表す16ビット(2バイト)の識別IDです。IPv4だったら「0x0800」、ARPだったら「0x0806」など使用するプロトコルによって値が決まっています。

Ethernetペイロード 上位層のデータそのもの。 IPだったら「IPパケット」、APRだったら「ARPフレーム」 データのサイズは、デフォルトで46バイトから1500バイトまでで、この範囲に収めなければいけない。 46バイトに足りない場合、「パディング」と呼ばれるダミーのデータを付加して強引に46バイトに。 1500バイト以上の場合、上位層でデータを分割して1500バイトに収める。 Ethernetフレームに入るデータの最大サイズをMTU(Maximum Transmission Unitと言う)

FCS (Frame Check Sequence) Ethernetペイロードが壊れていないかどうかを確認する32ビット(4バイト)フィールド。 Ethernetペイロードに対して一定の計算(チェックサム計算、CRC)を行い、その結果をFCSとしてフレームの最後に付与。 受信側の端末がフレームを受け取ると、同じ計算を行い、その値がFCSと同じだったら正しいEthernetフレームと判断。 FCSは送信側から見て、Wiresharkでキャプチャした後に付与され、受信側から見てキャプチャキャプチャする前に取り外される。 フレーム長としても換算されない。

MACアドレス Ehernetネットワークに接続している各端末(ノード)の識別IDです。 NICを製造するときにROMに書き込まれる。 48ビットで構成され、16進数で表記。

先頭から8ビット目にあるI/Gビット (Individual/Groupビット)と 7ビット目にあるU/Lビット (Unique/Localビット)が特別な意味を持つビット。

I/Gビット (Individual/Groupビット) 1:1通信のためのユニキャストアドレスか、1:n通信のためのマルチキャストアドレスかを表す。 0の場合、各端末に個別に割り当てられているMACアドレス 1の場合、複数の端末のグループに割り当てられているMACアドレス ※同じEthernet上にいる全ての端末を表すブロードキャストアドレスは、全てのビットが1の「ff:ff:ff:ff:ff:ff」の特別なMACアドレスが割り当てられる。

U/Lビット (Unique/Localビット) そのMACアドレスがグローバルアドレスローカルアドレスかを表している。 Wiresharkでは、LG bitと表記されます。 0の場合、IEEEから割り当てられた世界で唯一のMACアドレス 1の場合、管理者が独自に割り当てたMACアドレス

3種類のMACアドレス

通信の種類 送信元:宛先 送信元MACアドレス 宛先MACアドレス
ユニキャスト 1:1 送信元端末のMACアドレス 宛先端末のMACアドレス
ブロードキャスト 1:n(同Ethernet上の全端末) 送信元端末のMACアドレス ff-ff-ff-ff-ff-ff
マルチキャスト 1:n(特定Groupの端末) 送信元端末のMACアドレス I/Gビットが「1」のMACアドレス

PPPoE (Point to Point over Ethernet) ポイントとポイントを1:1に繋ぐためのに繋ぐためのレイヤー2プロトコル

ARP (Address Resolution Protocol) 実際のデータ通信に先立って,IPアドレスからMACアドレスを求めるもの

終わりに

パケットキャプチャのまとめと言うよりも、TCP/IP基礎のまとめになってしまったため、 レイヤー2までとして、次回以降(たぶんない)見せ方を工夫せねば... (次回は、業務のお話を書こう)

弊社では、ネットワークに長けているアプリケーションエンジニアも募集しております。

www.wantedly.com

EmacsLispでBrainfuckのMajor Modeとインタプリタを作る

こんにちは、最近 Ubuntu 20.04 から Manjaro 19.0 に乗り換えた @takeokunn です。

非常にサクサク動くようになって快適になりました。Linux詳しくなりたいです。

はじめに

brainfuckは実行できる命令が8種類しかないシンプルな言語です。brainfuckの記事は世の中に転がっているのでそちらを参照ください。

知り合いのエンジニアとガストで喋ってたときに「emacs詳しくなりたいならbrainfuckとか良いんじゃね?作ろうよー」と言われたので作ってみました。

できたもの

repo: https://github.com/takeokunn/brainfuck.el

f:id:bararararatty:20200324160118p:plain

  • syntax highlightできるようにした
  • tokenのdocをmodelineに出すようにした
  • interpreterを作ってbuffer内のstringを取得して結果を出すようにした

Major Mode

(defvar bf-syntax-table
  (let ((table (make-syntax-table)))
    (modify-syntax-entry ?\" "." table)
    table))

;;;###autoload
(define-derived-mode brainfuck-mode prog-mode "Brainfuck"
  :syntax-table bf-syntax-table
  (bf--add-keywords)
  (bf--help-doc-fun))

;;;###autoload
(add-to-list 'auto-mode-alist '("\\.bf" . brainfuck-mode))

(defvar brainfuck-mode-map nil "Keymap for brainfuck-mode.")

(defun bf--add-keywords ()
  (font-lock-add-keywords
   nil
   (list (cons (rx (any "[" "]")) font-lock-keyword-face)
         (cons (rx (any ">" "<" "+" "-" "." ",")) font-lock-function-name-face)
         (cons (rx (not (any "[" "]" ">" "<" "+" "-" "." ","))) font-lock-comment-face))))

今回は prog-mode から派生させてみました。

token 8種類をハイライトするだけなので、 keyword を正規表現でmatchさせて [ ] はkeyword, 他6種類のtokenは function, それ以外の文字列は comment として表示するように書いてみました。

DocをModeLineに表示

(defun bf--help-sym-called-at-point ()
  (unless (eobp)
    (buffer-substring-no-properties (point) (1+ (point)))))

(defun bf--help-lookup-doc (sym)
  "Return document string for SYM."
  (pcase sym
    (">" "Increment the pointer.")
    ("<" "Decrement the pointer.")
    ("+" "Increment the value indicated by the pointer.")
    ("-" "Decrement the value indicated by the pointer.")
    ("." "Print the value indicated by the pointer.")
    ("," "Read one byte from input and store it in the indicated value.")
    ("[" "Jump to the matching `]' if the indicated value is zero.")
    ("]" "Jump to the matching `[' if the indicated value is not zero.")))

(defun bf--help-summerize-doc (sym doc)
  (concat sym " : " (car (split-string doc "[\n\r]+"))))

(defun bf-help-minibuffer-help-string ()
  (interactive)
  (let* ((sym (bf--help-sym-called-at-point))
         (doc (when sym (bf--help-lookup-doc sym))))
    (when doc (bf--help-summerize-doc sym doc))))

(defun bf--help-doc-fun ()
  (make-local-variable 'eldoc-documentation-function)
  (setq eldoc-documentation-function
        'bf-help-minibuffer-help-string))

自分の現在のカーソルの文字を取得し、それにあたるdocを取得、 eldoc に流し込むように実装しました。

インタープリタと実行処理

(defun bf-interpreter (input)
  (interactive)
  (let* ((input-list (-map #'char-to-string (coerce input 'list)))
         (ptr 0)
         (mem (make-vector 30000 0))
         (braces (make-vector (length input-list) 0))
         (braces-stack '()))
    (dotimes (outer (length input-list))
      (if (string-equal (nth outer input-list) "[")
          (let ((cnt 0))
            (progn
              (do ((inner 0 (1+ inner)))
                  ((< (length (nthcdr outer input-list)) inner))
                (cond ((string-equal (nth (+ outer inner) input-list) "[") (push t braces-stack))
                      ((string-equal (nth (+ outer inner) input-list) "]") (pop braces-stack)))
                (if (zerop (length braces-stack))
                    (setq inner (length (nthcdr outer input-list)))
                    (incf cnt)))
              (aset braces outer (+ outer cnt))
              (aset braces (+ outer cnt) outer)))))
    (do ((index 0 (1+ index)))
        ((< (length input-list) index))
      (pcase (nth index input-list)
        (">" (incf ptr))
        ("<" (decf ptr))
        ("+" (aset mem ptr (incf (aref mem ptr))))
        ("-" (aset mem ptr (decf (aref mem ptr))))
        ("." (princ (char-to-string (aref mem ptr))))
        ("," (aset mem ptr (read-char)))
        ("[" (if (zerop (aref mem ptr)) (setq index (incf (aref braces index)))))
        ("]" (unless (zerop (aref mem ptr)) (setq index (aref braces index))))))))

(defun bf-exec ()
  (interactive)
  (let ((str (buffer-string)))
    (bf-interpreter str)))

Loopの処理(括弧の対応)が非常に大変だったが、 JavascriptでBrainfuckのインタプリタを実装してみた。 を参考にしたらいけた。

M-x bf-exec と叩くとバッファ内の文字列を取得し、インタープリタで処理をし、結果を吐き出すようにしました。

参考サイト

終わりに

次はEmacsLispでC compilerを作りたいです。

弊社ではemacsのpluginを作れるプログラマを募集しています。

www.wantedly.com

UUUM攻殻機動隊に入隊して

システムユニットの5n4wasa6です。

UUUM攻殻機動隊に入隊して1ヶ月立ちましたので、定例(予定)の1ヶ月経過blogを書きます。

UUUMは現在、中期IT計画というPJが進行しており、私はその開発にjoinしております。 開発で使用している言語は主に、Ruby on Rails,Vue.jsでインフラタスクにも関わっています。

エンジニアとしての経歴は社内で1番短いですが、やりたいと言えばやらせてもらえる環境で日々楽しみながら働いております。

今回は、UUUM攻殻機動隊について3本建てで書いていきます。

  1. UUUM攻殻機動隊に入隊して感じたこと
  2. サービスの紹介 (一部)
  3. 社内勉強会の様子

はじめに

先ほど記載した通り、私は2019年2月にUUUMにjoinしました。

前職では、マイクロサービスの開発とインフラ周りの構築や移行などやっておりました。

その前は、通信会社で法人営業をやっておりましたので、エンジニアとしてはかなり後発であり気合いを入れて日々精進しております。

1. UUUM攻殻機動隊に入隊して感じたこと

現在、隊員は約30名おり、日々サービス開発に勤しんでおります。

UUUM攻殻機動隊に入隊して感じたことは、めっちゃ働きやすい環境であること。

技術やサービスベースで気兼ねなく議論しており、スピード感が非常に早い点が大きな要因だと思います。

また、後述しますが、勉強会が非常に充実しており、技術への追求が素晴らしいです。

2週間に1,2度程のペースで開催されており、私の1番楽しみにしている時間です。

ジャンル問わず開催されておりますが、最近はvim熱が非常に高いです。

詳しくはこちらをご参照ください。 system.blog.uuum.jp system.blog.uuum.jp

2. サービスの紹介 (一部)

UUUMはYoutuberのマネジメント会社であることは、ご存知の方もいるかと思うのですが、 そこで働くエンジニアがどんなことをしているかを想像することは難しいと思います。

私も、UUUMに興味を持った際に、実際にエンジニアがどんな業務をしているのか容易には想像できませんでした。 ※攻殻機動隊blogの存在を知らなかった...というただの情弱なのですが。。

そこで、サービスの一部紹介したいと思います。 Youtuberさんのマネジメント会社であるため、Youtuberさん向けやそれを管理する社内システムが多いです。

[クリエイター(youtuberさん等)向け各種サービス]

・クリエイターのサポートツール ・クリエイターの情報管理ツール ・各種データ分析ツール

[中期IT計画]

・社内管理システム こちらは私がJOINしているPJで、上記のサービスやデータを一元管理できる社内管理システムになります。

[MUUU]

UUUM公式オンラインストア

Youtuberさんのグッズなどを販売するオンインストアサービス

[レモネード]

Instagram特化型インフルエンサーマーケティングツール

インフルエンサーの選定、依頼、関係性構築、パフォーマンス分析、コンテンツ管理など 全ての作業を1つの自動ツールで簡単に実施できるサービスになります。

レモネードの技術詳細につきましては、こちらをご参照頂ければと思います。

社内,youtuberさん向けサービスが主で、説明し切れていない部分もございますが、 是非UUUM攻殻機動隊に入隊いただき、実態を解明頂ければと思います。

3. 社内勉強会の様子

前述の通り、UUUM攻殻機動隊は非常に勉強会が充実している。

勉強会では、業務効率化のための設定やネットワークの基礎、暗号化/復号化など様々なジャンルで開催されている。

今回は、勉強会の派生で展開した、ターミナル操作効率化のための各種設定について紹介します。 恥ずかしながら、dotfilesも作成していなかった私ですが、諸先輩方に教えていただきながら支給されたPCを進化させたので記します。

dotfilesについては、何度となく紹介されております 汎用性・拡張性の高い dotfiles 環境を作る を参考にした諸先輩のdotfilesを参考に自前で作成(ほぼ~パクリ~)しました。 今回はその中のいくつかの設定をご紹介します。

tmux

tmuxとは、端末多重化ソフトウェアです。 これまで、ターミナルでタブを複数表示したり、複数ターミナルを立ち上げており非常に不便でしたが、tmuxを導入することでこのような煩わしさは解消。 要するに、1つのターミナル上で複数のターミナルを立ち上げて同時に作業を可能にしてくれる便利なやつです。

tmuxの導入は非常に簡単

brew install tmux

あとは、~/.tmux.confこちらを参考に設定すれば便利なターミナル操作が可能になります。 mouse操作をonにすることをお勧めします。

# マウス操作を有効にする
set-option -g mouse on
bind -n WheelUpPane if-shell -F -t = "#{mouse_any_flag}" "send-keys -M" "if -Ft= '#{pane_in_mode}' 'send-keys -M' 'copy-mode -e'"

tmuxの操作方法もチートシート集がございますので安心。

かく言う私は、優しい優しい先輩方にあれこれ聞いて操作を習得しました。笑

peco

pecoとは、一覧結果のインクリメンタルサーチ(入力に応じて候補を絞り込む検索方法)を実現してくれるツールです。

pecoの導入も非常に簡単です。

brew install peco

あとは、バインドキーを利用したコマンドの履歴検索にpecoを使う設定をしましょう(bash,zshユーザー)。 カスタマイズして自分に合ったpecoちゃんにすることが可能です。 「履歴降順」「重複削除」がポピュラーでしょうか。

ghq

ghqとは、リポジトリを一元管理してくれるツールです。

ghqの導入も簡単

brew install ghq

ghq rootでghqのルートディレクトリを返してくれ、 ghq listでghqルートディレクトリ以下のGitリポジトリ一覧を表示できます。 上記のpecoと組み合わせると非常に便利になります。

hub

hubとは、CLIで作業中リポジトリのURLをコマンド一発で開けるようになるツール。

hubの導入も簡単

brew install hub

peco, ghq, (hub)を組み合わせることで、リポジトリ一覧をインクリメンタルサーチして、 選択したリポジトリに移動することができるため、非常に便利で業務効率化につながります。

ghq list | peco
cd $(ghq root)/$(ghq list | peco)

参考サイトのようにエイリアスを貼ってより便利に使用できます。

alias g='cd (ghq root)/(ghq list | peco)'
alias gh='hub browse (ghq list | peco | cut -d "/" -f 2,3)'

fish

fishとは、インタラクティブシェルの1つで補完機能が便利なやつです。 ユーザフレンドリーであり、エラーメッセージが親切でコマンド入力時にコマンドに色を付けてくれる親切なやつです。

fishの導入

brew install fish

bash or zshからfishに変更が必要なためこちらを参考に。

fishに乗り換えて良い点は、補完機能が優秀で操作スピードが格段に上がっていることです。

終わりに

弊社では、自作dotfilesの自慢のできるプログラマを大募集しています。 www.wantedly.com

vimサークル活動報告 #2

システムユニットのxxuxa_kです。ついにUUUMでの生活も3年目に入りました。今日もvim活動報告です。普段使っているneovimの設定について書きたいと思います。たぶん5億番煎じくらいの内容になると思います。前回分もぜひご覧ください。

system.blog.uuum.jp

プラグインと設定

プラグイン管理にはvim-plugを使っています。インストールやアップデートが並列で走るので早いのとコマンドがシンプルなのがとてもいいです。使っているプラグインと設定を一部紹介していきます。

GitHub - junegunn/vim-plug: Minimalist Vim Plugin Manager

Denite

github.com

Deniteはありとあらゆる検索をやってくれるプラグインでDark poweredシリーズの一つです。前回の活動報告にもありますがこれがなくてはもはやvim生活は成り立ちません。まさにDark powerです。denite本体に合わせてgit関連のdenite sourceを追加してくれるプラグインも合わせて入れています。

Plug 'Shougo/denite.nvim', { 'do': ':UpdateRemotePlugins' }
Plug 'chemzqm/denite-git'

Deniteを起動するkeymapをいくつか設定していますが実際に使うのはやはりfile/recとlineが圧倒的に多いです。Deniteはsourceを複数与えることができるのでfile/recとlineを一度に指定するkeymapもつけていますが、あんまり使わないですね。ここに挙げているsource以外を見たいときはDeniteでsourceを検索してそのまま見つけた別のsourceで検索しています。(実はこれだけでいいんじゃね?と思ったりします。)ちなみにLeaderにはスペースキーを割り当てているのですが大きくて打ちやすいのが好みです。

nnoremap <Leader>f :Denite file/rec<CR>
nnoremap <Leader>l :Denite line<CR>
nnoremap <Leader>e :Denite file/rec line<CR>
nnoremap <Leader>h :Denite defx/history<CR>
nnoremap <Leader>g :Denite grep<CR>
nnoremap <Leader>b :Denite buffer<CR>
nnoremap <Leader>m :Denite menu<CR>
nnoremap <Leader>o :Denite outline<CR>
nnoremap <Leader>u :Denite source<CR>
nnoremap <Leader>c :Denite gitstatus<CR>

次に自動コマンドの設定です。DeniteはdeniteというFileTypeをもっているので、Deniteのバッファにいるときはこのkeymapが有効になります。iキーを押すとfilterが開始されますが、filterを終了したいときにCtrl-oが使えるというようになっています。

autocmd FileType denite call s:denite_my_settings()
function! s:denite_my_settings() abort
  nnoremap <silent><buffer><expr> <CR> denite#do_map('do_action')
  nnoremap <silent><buffer><expr> d denite#do_map('do_action', 'delete')
  nnoremap <silent><buffer><expr> p denite#do_map('do_action', 'preview')
  nnoremap <silent><buffer><expr> q denite#do_map('quit')
  nnoremap <silent><buffer><expr> i denite#do_map('open_filter_buffer')
  nnoremap <silent><buffer><expr> <Space> denite#do_map('toggle_select').'j'
endfunction

autocmd FileType denite-filter call s:denite_filter_my_settings()
function! s:denite_filter_my_settings() abort
  imap <silent><buffer><C-o> <Plug>(denite_filter_quit)
endfunction

1つおすすめの設定を書いておきます。↑のようにkeymapを設定すればDeniteのバッファからはiキーで検索を開始できますが、start_filter: v:trueを指定することでDeniiteを開くと同時にfilterが開始されます。filter時のプロンプトも好きなものに変えられるようになっています。

call denite#custom#option('default', {
      \ 'split': 'horizontal',
      \ 'highlight_filter_background': 'DeniteFilter',
      \ 'prompt': '> ',
      \ 'start_filter': v:true
      \})

最後にDeniteのmenuに関する設定です。Deniteのmenuについてはがんばってhelp読んではいるんですがまだ理解できておらず、使い道を探っているところです。とりあえず今できるとわかっていることは

  • 事前に用意したコマンドを実行する
  • 特定のファイルを開く

の2つです。bundle exec ...系のコマンド追加と、dotfileが開けるようにしました。追加はしたものの普段はtmux起動してpaneとかwindowを移動しながらやっているので今後もあまり使わない気がします。そもそもこういうのはDeniteに求める機能ではないですね。

let s:menus = {}
let s:menus.rails = {
      \ 'description': 'Commands in Rails project',
      \ 'command_candidates': [
        \ ['install gems', '!bundle install'],
        \ ['execute tests', '!bundle exec rails test'],
        \ ['rubocop', '!bundle exec rubocop'],
        \ ['rubocop auto-correct', '!bundle exec rubocop --auto-correct'],
        \ ['slim-lint', '!bundle exec slim-lint app/**/*.slim'],
      \ ]
      \ }
let s:menus.dotfiles = {
      \ 'description': 'Edit dotfiles',
      \ 'file_candidates': [
        \ ['neovim', '~/.config/nvim/init.vim'],
        \ ['vim', '~/.vimrc'],
        \ ['tmux', '~/.tmux.conf'],
        \ ['zshrc', '~/.zsh/.zshrc'],
      \ ]
      \ }
call denite#custom#var('menu', 'menus', s:menus)

Defx

github.com

またしてもDark poweredシリーズです。Defxはファイルブラウザです。Deniteと同様にsourceを対象に起動するという作りになっていて、Deniteと同じように設定ファイルがかけます。(ただしDefxはDeniteに依存しておらず単体で動きます)

Plug 'Shougo/defx.nvim', { 'do': ':UpdateRemotePlugins' }

keymapはこれだけです。defx導入前はNERDTreeを使っていた(今も入ってはいる)んですが全く使わなくなりました。

nnoremap <silent>,d :Defx<CR>

私は普段ファイルを指定してneovimを起動するということをしないので、プロジェクトのルートに移動したらすぐnvimとかnvとかを引数なしで叩いてneovimを起動します。(nnvnviを全部nvimのaliasにしています。)その際にdefxが自動で起動するようにしているという設定です。

autocmd VimEnter * if argc() == 0 && !exists("s:std_in") | Defx | endif

DefxもDeniteと同様にdefxのバッファにdefxという独自のFileTypeがあるので、自動コマンドで専用のkeymapを割り当てることができます。ほぼDefxのhelpの内容そのままです。スペースキーで複数のファイルを選択して一気に開けるのはたまに嬉しいときがあります。Defxも便利なんですがほとんどの場合Deniteで済むのでめちゃくちゃ活用できているという感じではないです。

autocmd FileType defx call s:defx_my_settings()
function! s:defx_my_settings() abort
  nnoremap <silent><buffer><expr><CR> defx#do_action('open')
  nnoremap <silent><buffer><expr> c defx#do_action('copy')
  nnoremap <silent><buffer><expr> m defx#do_action('move')
  nnoremap <silent><buffer><expr> p defx#do_action('paste')
  nnoremap <silent><buffer><expr> v defx#do_action('open', 'vsplit')
  nnoremap <silent><buffer><expr> i defx#do_action('open', 'split')
  nnoremap <silent><buffer><expr> o defx#do_action('open_or_close_tree')
  nnoremap <silent><buffer><expr> O defx#do_action('open_tree_recursive')
  nnoremap <silent><buffer><expr> x defx#do_action('close_tree')
  nnoremap <silent><buffer><expr> P defx#do_action('open', 'pedit')
  nnoremap <silent><buffer><expr> K defx#do_action('new_directory')
  nnoremap <silent><buffer><expr> N defx#do_action('new_file')
  nnoremap <silent><buffer><expr> M defx#do_action('new_multiple_files')
  nnoremap <silent><buffer><expr> C defx#do_action('toggle_columns', 'mark:indent:icon:filename:type:size:time')
  nnoremap <silent><buffer><expr> S defx#do_action('toggle_sort', 'time')
  nnoremap <silent><buffer><expr> d defx#do_action('remove')
  nnoremap <silent><buffer><expr> r defx#do_action('rename')
  nnoremap <silent><buffer><expr> ! defx#do_action('execute_command')
  nnoremap <silent><buffer><expr> ee defx#do_action('execute_system')
  nnoremap <silent><buffer><expr> yy defx#do_action('yank_path')
  nnoremap <silent><buffer><expr> . defx#do_action('toggle_ignored_files')
  nnoremap <silent><buffer><expr> ; defx#do_action('repeat')
  nnoremap <silent><buffer><expr> b defx#do_action('cd', ['..'])
  nnoremap <silent><buffer><expr> ~ defx#do_action('cd')
  nnoremap <silent><buffer><expr> q defx#do_action('quit')
  nnoremap <silent><buffer><expr> <Space> defx#do_action('toggle_select') . 'j'
  nnoremap <silent><buffer><expr> * defx#do_action('toggle_select_all')
  nnoremap <silent><buffer><expr> j line('.') == line('$') ? 'gg' : 'j'
  nnoremap <silent><buffer><expr> k line('.') == 1 ? 'G' : 'k'
  nnoremap <silent><buffer><expr> <C-l> defx#do_action('redraw')
  nnoremap <silent><buffer><expr> <C-g> defx#do_action('print')
  nnoremap <silent><buffer><expr> cd defx#do_action('change_vim_cwd')
endfunction

vimdoc-ja

github.com

vimだったりneovimだったりvimscriptだったりを理解するスピードが最近上がってきたなと思うんですが、完全にこれのおかげです。おそらくvimでもneovimでも特定の内容についてhelpを引くと英語版の内容がヒットすると思いますが、これを日本語版で出せるようになるというものです。インストールしてset helplang=jaするだけで日本語のhelpがある内容であれば日本語で出してくれます。一度参加したゴリラ.vimで教えていただきました。翻訳をされた皆様には本当に感謝しかありません。 普段vimのhelp引いてないっていう方はぜひ使ってください。Deniteのline検索と一緒に使えばスラスラ読めます。

gorillavim.connpass.com

vim-lsp

github.com

さて最後にLsp周りです。私は特に理由はないのですがvim-lspを使っています。(というか他のものを使ったことがありません。)非同期補完にはまたしてもDark poweredシリーズdeoplete.nvimを使っています。 mattnさんのvim-lsp-settingsも入れているのですが:LspInstallServerしたあとの挙動がどうにも?となることが多かったので最近はあまり使っておらずvim-lspのwikiを見て1つずつLanguage Serverをセットするようにしています。

Plug 'prabirshrestha/async.vim'
Plug 'prabirshrestha/vim-lsp'
Plug 'mattn/vim-lsp-settings'
Plug 'Shougo/deoplete.nvim', { 'do': ':UpdateRemotePlugins' }
Plug 'lighttiger2505/deoplete-vim-lsp'
Plug 'mattn/vim-lsp-icons'
Plug 'ryanolsonx/vim-lsp-typescript'

Language Server設定の例です。すべてに共通して実行できるLanguage Serverがあれば自動コマンドを実行してregister_serverするという形式になっています。これはvim-lspのwikiに書いてある内容をそのまま実行すればすぐに使えるようになります。とはいえwikiだけでは設定がうまくいかないこともあってGroovy用のLanguage Serverがその例です。設定方法をQiitaに書いたのでこちらも良ければ見てください。

qiita.com

if executable('solargraph')
  augroup LspRuby
    autocmd!
    autocmd User lsp_setup call lsp#register_server({
          \ 'name': 'solargraph',
          \ 'cmd': {server_info->[&shell, &shellcmdflag, 'solargraph stdio']},
          \ 'initialization_options': {"diagnostics": "true"},
          \ 'whitelist': ['ruby'],
          \ })
  augroup END
endif

if executable('vim-language-server')
  augroup LspVim
    autocmd!
    autocmd User lsp_setup call lsp#register_server({
          \ 'name': 'vim-language-server',
          \ 'cmd': {server_info->['vim-language-server', '--stdio']},
          \ 'whitelist': ['vim'],
          \ 'initialization_options': {
          \   'iskeyword': '@,48-57,_,192-255,-#',
          \   'vimruntime': expand($VIMRUNTIME),
          \   'suggest': { 'fromVimruntime': v:true },
          \ }})
  augroup END
endif

if executable('java') && filereadable(expand('$HOME/.lsp/groovy-language-server/build/libs/groovy-language-server.jar'))
  autocmd User lsp_setup call lsp#register_server({
        \'name': 'groovy-language-server',
        \'cmd': {server_info->[
        \  'java',
        \  '-jar',
        \  expand('$HOME/.lsp/groovy-language-server/build/libs/groovy-language-server.jar')
        \]},
        \'whitelist': ['groovy']
        \})
endif

まとめ

こうしてみると私のneovim設定はDark power無しにしては成り立たないものになっているなと思います。Shougoさんの作るプラグインは思想が一貫していて1つ何かがわかると他にも応用できるのがとても美しい作りになっているなと思います。またLspについてですが、もうちょっとなんとかできるよね?という感想を日々持っています。Lspを使ってneovimによりIDE感を出す活動をvimサークルではやっていきたいです。

最後に

我々システムユニットではvimmerもneovimmerも募集しています。興味のある方はぜひ採用サイトを見てみてください。

recruit.uuum.co.jp

Swagger OpenAPIでAPI Referenceを書く

こんにちは、@takeokunn です。

yamlyml の違いってなんだろーと調べたところ Please use “.yaml” when possible. って公式のfaqに書いてあったのでなるべく .yaml を使っていこうと思った今日このごろです。

今回はOpenAPIの実装例について書いていこうと思います。

Dockerで構築

docker-compose.yml はこんな感じ。

version: "3.3"
services:
  swagger-editor:
    image: swaggerapi/swagger-editor
    container_name: "swagger-editor"
    ports:
      - "19881:8080"
  swagger-ui:
    image: swaggerapi/swagger-ui
    container_name: "swagger-ui"
    ports:
      - "19882:8080"
    volumes:
      - ./openapi.yaml:/usr/share/nginx/html/openapi.yaml
    environment:
      API_URL: openapi.yaml
  swagger-api:
    image: stoplight/prism:3
    container_name: "swagger-api"
    ports:
      - "19883:4010"
    command: mock -h 0.0.0.0 /openapi.yaml
    volumes:
      - ./openapi.yaml:/openapi.yaml

swagger-uiswagger-editor は転がってるコードをそのままペタっと貼り付けました。

以前は danielgtaylor/apisprout を使っていたのですが、どうもOpenAPIの最新の仕様に追いついていなかった(要出典)ので今回は stoplight/prism を使いました。

下のサンプルの get: /colors を試しに叩いてみるとこんな感じ。簡単にmockが作れて良いですね。

~/.g/g/t/.emacs.d (*´ω`*) < curl "http://localhost:19883/colors" -H "Authorization: Bearer xxx"
{"message":200,"colors":[{"id":1,"name":"赤","created_at":"2002/05/12 20:30:15","updated_at":"2002/05/12 20:30:15"}]}

実際に openapi.yaml を書いてみる

Specifitcation とにらめっこして書けば大体できます。

試しに2つのendpointを作ってみました。

  • get: /colors: 色を取得する
  • post: /colors: 色を作成する

Bearer token を必要とする場合でも簡単に記述てきてとても良いです。

openapi: 3.0.5
info:
  title: OpenAPIテスト
  version: 1.0.0
  description: OpenAPIテスト
servers:
  - url: http://localhost:3000/api/v1
    description: Local server
  - url: https://staging.test.tokyo/api/v1
    description: Staging server
security:
  - bearerAuth: []
paths:
  /colors:
    get:
      summary: Get colors
      description: 色一覧を取得
      responses:
        200:
          description: 色一覧を返す
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: 200
                  colors:
                    type: array
                    items:
                      $ref: '#/components/schemas/ColorModel'
    post:
      summary: Create color
      description: 色を作成
      requestBody:
        content:
          application/json:
            schema:
              required:
                - name
              properties:
                name:
                  type: string
                  example:responses:
        201:
          description: 作成した色を返す
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Success
                  color:
                    type: object
                    $ref: '#/components/schemas/ColorModel'
        400:
          description: 作成エラー
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Failure
components:
  schemas:
    ColorModel:
      type: object
      properties:
        id:
          type: integer
          example: 1
          description: primary id
        name:
          type: string
          example:description: 名前
        created_at:
          type: string
          example: 2002/05/12 20:30:15
          description: 作成日
        updated_at:
          type: string
          example: 2002/05/12 20:30:15
          description: 作成日
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: API Key

emacsで快適に openapi.yaml を書く

emacsで快適にyamlを書くためにいくつかpluginを入れました。

openapi-yaml-mode

url: magoyette/openapi-yaml-mode

MELPAで公開されていないが、顧客が欲しかった感じのEmacsLispが書かれているので導入した。completionや簡易的なhighlightなどが入っている。

f:id:bararararatty:20200225222634p:plain
openapi

;; cask
(depends-on "openapi-yaml-mode" :git "https://github.com/magoyette/openapi-yaml-mode.git")

;; config
(use-package openapi-yaml-mode
  :mode (("\\openapi.yaml$" . openapi-yaml-mode))
  :config
  (setq openapi-yaml-use-yaml-mode-syntax-highlight t))

DarthFennec/highlight-indent-guides

url: DarthFennec/highlight-indent-guides

highlight-indent-guides

yaml のインデントを可視化してくれるplugin。以下のようにconfigを書いた。

(use-package highlight-indent-guides
  :diminish
  :hook
  ((prog-mode yaml-mode) . highlight-indent-guides-mode)
  :custom
  (highlight-indent-guides-auto-enabled t)
  (highlight-indent-guides-responsive t)
  (highlight-indent-guides-method 'character))

終わりに

openapi.yaml 職人の方は是非弊社で一緒にyamlをかきましょう!!!

www.wantedly.com