UUUMエンジニアブログ

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

オンラインハッカソンの開催方法・開催に伴う準備について。システムユニットのハッカソン開催の裏側

こんにちは!エンジニアの中村です。 今回は過去に開催したハッカソンを元にハッカソンの運営方法やシステムハッカソンの裏側などを紹介していきたいと思います! コロナ渦でイベント開催が難しい中、ハッカソン開催について興味がある人は是非参考にしてください😃

ハッカソンについて

ハッカソンの目的・開催理由

システムハッカソン開催の目的は、社内のコミュニケーション促進、スキルアップや通常業務ではできなかったチャレンジの機会を設けるためです。 以前まで開発合宿が半期に1回行われていましたが、情勢が変わりコロナ禍において開催が困難になったという背景からオンラインハッカソンを開催しました。

過去のハッカソンの記事

気になる方はこちらから記事を見てください!

【第1回】 https://system.blog.uuum.jp/entry/2021/01/22/110000

【第2回】 https://system.blog.uuum.jp/entry/2021/11/17/110148

開催までの準備

開催までにしたこと一覧

ハッカソンを開催するまでにしたこと一覧です。 第1回と第2回のハッカソンを通して自分が開催するときには必要だよなと思うことを書いたので、ご参考までに。

⚠︎第2回のハッカソンはテーマを参加者に事前に伝えており、ハッカソンでやることやチームを事前に提出してもらいました。

【準備】
[] 運営メンバー集め (司会者・カメラ係・タイムキーパー・情報共有者・ブログ執筆者・評価集計・評価者)
[] 実施計画書を書く(目的・概要・日付・開催場所・予算etc..)
[] 予算の申請
[] ハッカソンの参加者・運営のグループを作成する(運営だけのグループも作成)
[] 参加者集め(シート作成)
[] やることが似ている人たちをチームにまとめる(参加者集めのシートと同じところにまとめる)
[] ハッカソンについての説明会・質問会(複数回)
[] GoogleDrive等に発表資料置き場など資料をまとめる場所を作る
[] 評価基準を決める
[] 賞と景品を決める
[] 実施場所の予約・場所の確認
[] 実施の全社周知
[] 打ち上げの準備

【資料】
[] オープニングの資料
[] 発表前の資料
[] 結果発表の資料
[] ハッカソン参加者アンケート
[] 評価シート
[] 評価コメントシート

準備の詳細

次は開催までにしたこと一覧の詳細です。いくつかピックアップして詳細を書きます。 細かいことが多いですが、実際ここまで決めるとスムーズに行くことが多いので、 めんどくさいですがやるといいかもしれません。

運営メンバー集め

今回のハッカソンは小規模でしたが、運営メンバーは最低でも6人くらいはいるといいと思います。

役割は以下のようにしました。

・責任者 * 1 
・運営メンバー * 2
・司会進行 * 1
・ブログ執筆者 * 1
・評価者 * ?

運営メンバーは基本的にすること一覧の開催までの準備をするのですが、 本番ではカメラ係、Slackなどへの情報共有・タイムキーパーなどを担当しました。 運営メンバーがハッカソンに参加する場合は、タイムキーパーは2人いると1人が発表している時に タイムキーパーがいなくならないのでいいのかなと思います。

発表時間も発表人数から算出して「発表時間+質問タイム+評価入力時間」を出しておいて、全体的に数十分程余裕を持たせてスケジュールを作成しました。 評価集計の人も1人決めておくと、集計が楽です。(今回集計係を決めていなかったので集計が少しもたつきました)

ハッカソンの参加者・運営のグループを作成する

ハッカソン専用のグループを作成して、そこで基本的にハッカソンの話をしていました。 専用のグループを作り、そこでハッカソンについて色々と話すことで、 情報共有が楽なのと、ハッカソン開催への意識を持ってもらうためです。

運営メンバーは当日の動きの確認や、景品についてなどハッカソン参加者に知らせないがいい話をするので別でグループを作成してそこで話していました。

参加者集め・やることが似ている人たちをチームにまとめる

参加者を集める時は、スプレで参加可否・参加者名・テーマ・テーマ内容・テーマ説明を書いてもらい、提示した日までに書いてもらいました。

日を提示しても忙しくて忘れていたりする人がいるので、運営の人は1人参加者を集める担当を決めると期限までにデータが集まりやすかったです。 その日までに担当者が定期的にメンションをつけてリマインドをすることで、参加者自身が忘れないし、 何に悩んでいるのかを運営側が把握することができるからです。

オープニングの資料

オープニングの資料は主に以下の内容を準備しました。

・ハッカソンとは
・タイムテーブル
・発表について(発表日時・一人当たりの発表時間・強制終了時間・発表資料について)
・投票について(Googleフォームなどの投票場所のURLとQRコード・投票基準と各項目の点数)
・賞について(景品の発表・注意点など)
・注意点

注意点では発表前に資料を共有することや、発表中は出来る限り顔出し必須など 書いておくことで発表がスムーズにできるのかなと思います。 オープニング後はオープニングに来れなかった人や資料を見直したい人のために資料をグループに展開するといいかもしれません。

発表前の資料

発表前の前段階の資料は主に以下の内容を準備しました。

・発表について(オープニングと同じスライドでOK)
・注意点
・発表の流れ
・投票について(オープニングと同じスライドでOK)
・発表順番
・集計時間
・アンケート

発表順番に関しては参加人数が多い場合は運営側で先に決めておくとスムーズでした。 第2回は参加者が多かったので先にこちらのサイトで決めました。

順番決め.com: https://xn--ebku91pdvftn1b.com/

参加者が少ない場合は、発表順番を発表が終わるごとに決めても時間的に大丈夫そうだったので、 第1回はこちらのサイトを使わせていただきました。

3Dあみだくじ: https://exe.tanidaiz.com/3D-Amida.php

注意点に関しては、辛辣なコメントを控えるなど書くといいのかなと思います。 発表するだけですごい!と私は思うので、辛辣なコメントがあると次開催するときに参加したくなくなりますし、次発表する人も萎縮してしまうからです。

評価シート

今回はGoogleFormで評価シートを作成しました。 項目はチーム名・個人orチームのよかったところ・テーマごとの得点にしました。 第2回のハッカソンではテーマを3つに絞っており、 それぞれ評価基準が違うのでGoogleFormでチーム名を選択した際に飛ぶページをそのチームのテーマの評価シートになるように設定しました。

f:id:chibiProgrammer:20220228184133p:plain
評価シート

第2回ハッカソンの評価例↓

例:
テーマ:業務効率化
業務効率化  5点
ギーク度    5点
完成度      3点
プレゼン力   2点
総合点 ->   15点

テーマ:通常業務+a
業務効率   5点
ギーク度   5点
完成度     5点
プレゼン力  5点
総合点 ->  20点

テーマ:新規事業
新規・事業性 3点
完成度      3点
ギーク度    5点
プレゼン力   5点
総合点 ->   16点

評価コメントシート

評価コメントシートというのは、評価シートの個人orチームのよかったところを集めたシートです。 評価コメントシートは必須ではないとは思うのですが、作ったものへの評価を聞きたい人 がほとんどかなと思うので作成しています。

⚠︎基本的にはひどいコメントないかもしれませんが、あった場合は弾くなどの配慮が必要なのかなと思います。

f:id:chibiProgrammer:20220303184320p:plain
評価コメントシート

ハッカソン参加者アンケート

ハッカソンの参加者アンケートは 次のハッカソンをもっと良くするために作成しました。 質問内容は以下のようにしました。

・ハッカソンはこれまでに参加経験
・審査基準は妥当か
・審査基準の評価の理由
・開催期間は妥当か
・開催期間の評価の理由
・次やりたいイベントはあるか
・気になったこと、次のハッカソンでは直して欲しいところはあるか
・次のハッカソンへの要望 

ハッカソン参加者アンケートは運営の結果集計時間に共有すると待ち時間にアンケートを書いてくれるのでいいのかなと思います。

第2回ハッカソン開催までの裏話

開催が決定して運営でMTGして話は進んでいましたが、コロナが悪化してきて集まるのが難しいという話になったり 今回のテーマが業務改善だったので何をすればいいのかわからない人が一定数いて、このまま進むとモチベーションも上がらないしいいものも 作れないよねという話になり、9月に行う予定でしたが10月末になりました。 それと、ハッカソンまでに準備しないといけないことが用意ができてなく、 全体的な流れがぼやけていて止まっていたので準備にある程度時間がかかりました。

当日は会場に必要なものがなかったり、最後の集計でGoogleFormのデータスプレに吐き出してデータを確認すると データがバラバラで、集計に時間がかかったり、結果発表の資料が一部できておらず、即席でこんな感じの資料を準備してもらいました笑

f:id:chibiProgrammer:20220303183211p:plain
モンエナ
思った以上に想像が欠けていた部分が色々出てきたので、運営側は色々大変だなと思いました😇

まとめ

今回書いたような準備シートや期限を設定してMTGを都度行なったりすることをやってからスムーズに進んだので ハッカソンを開催する際は準備シートや期限、MTGの頻度をきちんと決めてから作業をすると延期や中止になりにくいのかなと思いました。 ハッカソンを開催するのは意外と準備が必要ですし、「ハッカソン?まぁ司会とかの資料作成くらいしとけばあとはどうにかなるでしょ?」 くらいのテンション感だと当日想定できてないことが起きてスムーズにいかないので、きちんとした計画が大切だと思いました。 第2回ハッカソンは1回目よりテーマの難しさや参加人数の増加により考えることが多かったですが、 いろんな人が協力してくださったのでうまく行きました。 第3回があるかはわかりませんが、開催できたら今以上にいいハッカソンができるようにしたいと思っています。

今回の記事でハッカソンを開催したいけど何をしたらいいのかわからない人や、 開催準備を進めているけどうまくいくか不安な人の何か参考になれば幸いです。 ここまで読んでいただきありがとうございました🙇‍♀️

第2回オンラインハッカソンで作成した拡張機能の裏話

こんにちは。エンジニアのendo_shizukaです。

UUUMに入社して1年半が経ちました。

「入社して1年半経ったとか時間の流れ早すぎん!?」って感じで日々業務に勤しんでいます。

さて、今回ブログを書くにあたりネタをいろいろ探していたのですが、

個人的にかなり大変だけど楽しかった第2回オンラインハッカソンで作成した機能でつまずいたポイントについて書いていきたいと思います。

第2回のオンラインハッカソンについてまだご存じない方は、

弊社の新進気鋭なエンジニア、久保寺君が書いてくれた記事がありますので見てみてください。 system.blog.uuum.jp

それではどうぞ。

 

そもそもGoogle Meetの出欠席確認機能って何?

前提としてGoogle Workspaceを使用している会社・チームが対象になります。

もしかしたら既に経験ある方もいるかと思いますが、会議の開催時刻になると招待されたメンバーは続々とGoogle Meet(以下、Meet)に参加します。

ファシリテーターは招待者の出欠確認を行うのですが、Meetの標準機能だと現在参加中のメンバーしか表示されていないので、Google Calendar(以下、Calendar)とMeetの参加者を照らし合わせながら確認するという手間が発生します。

もし、会議に招待しているメンバーが参加するのか否か、また現在Meetに参加しているか否かがMeet上で一目で分かれば出欠確認の手間が大幅に削減されるのでは?ということを目的に開発されました。

f:id:endo_shizuka:20220225192228p:plain
出欠席確認機能

  さて、ここから本題です。    

つまずきポイント

参加中のMeetからどの会議に参加してるかが判断できない...

会議で使用するMeet URLはCalendar上から確認できますが、

f:id:endo_shizuka:20220225160039p:plain
Calendar画面

Meet上からだとMeetのURLと入室前の会議タイトルしか情報が無く、どの会議に参加してるかが判断できません。

f:id:endo_shizuka:20220225160036p:plain
Meet画面

 

解決策

以下の手順で参加中のMeetの会議情報が取得できました。

  1. JavaScriptのlocation.pathnameを用いてMeet URLのhttps://meet.google.com/以下の文字列を取得

  2. CalendarAPIのEvents.listを用いて本日入っている自分の会議を全て取得

  3. 2で取得した会議の中にconferenceDataオブジェクト *1 があるので、その中のconferenceIdと1の文字列が一致するものを開催中の会議として取得する

参考1 developers.google.com

参考2 conferenceDataオブジェクトのデータ構造(2022/02/25現在)

conferenceData: 
{ entryPoints: [ [Object], [Object], [Object] ],
    signature: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxx',
    conferenceSolution: 
    { key: [Object],
    name: 'Google Meet',
    iconUri: 'https://xxxxxxxxxxxx' },
    conferenceId: 'yer-necw-rbs' },

 

会議室もゲスト扱いになって参加者一覧として表示されてしまう...

組織内で会議室もCalendarに作成して管理していた場合、

その会議室を使用した会議を作成するとゲストの1人として会議室も表示されてしまいます。

解決策

会議室のゲストのメールアドレスはドメインが@resource.calendar.google.comで設定される事を発見しました。

会議のゲストリストを取得する際に上記ドメインのゲストを弾くことで解決しました。

 

同じ会議IDで同じ予定が2つ取得されてしまう...

CalendarAPIのEvents.listでその日に開催される会議を取得した際に、同じ会議名で2つのオブジェクトが取得される場合があります。

これは定期的な会議の場合に発生するもので、実際に取得できたオブジェクトをそれぞれ確認してみると、 一方は定期的な会議の初回開催時の情報が入ったオブジェクト、もう一方は本日開催の情報が入ったオブジェクトであることがわかりました。

解決策

オブジェクト内を詳しく見てみると以下のような違いがありました。

  • 初回開催時の情報: recurrence: [ 'RRULE:FREQ=WEEKLY;WKST=SU;BYDAY=FR' ] *2

  • 本日開催の情報: recurringEventId: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxx',

なぜ、定期的な会議の場合2つオブジェクトが流れてくるかの理由については調べきれていませんが、 実装としては本日開催の会議情報が得られればいいのでrecurringEventIdが含まれるオブジェクトを取得することで解決しました。

 

参加者のフルネームや招待ステータスなど参加者にまつわる情報を取得したいけど...

こちらは自分の完全な見当違いだったのですが、 CalendarAPIのEvents.listで会議情報を取得したら参加者のメールアドレスやフルネーム、招待ステータス *3 などが全て一括で取得できるものだと思っていました。

Events.listはあくまで会議自体の情報しか取得できず、参加者については別途APIを叩く必要がありました。

解決策

画面表示には 参加者のフルネーム メールアドレス 招待ステータス サムネイル画像 の値が必要だったので、

メールアドレス 招待ステータスをCalendarEventのgetGuestListを使用して取得しました。

const guests = event.getGuestList(true)

developers.google.com

参加者のフルネーム サムネイル画像 についてはAdmin SDK Directory Serviceを使用する事で、会議の参加者のフルネームやサムネイル画像を取得することができました。

  do {
    page = AdminDirectory.Users.list({
      domain: DOMAIN,
      viewType: 'domain_public',
      maxResults: 500,
      pageToken: pageToken
    });

developers.google.com

注意点として、AdminDirectory.Users.listを実行する際にviewType: 'domain_publicを書かないとエラーになります。

GoogleJsonResponseException: API call to directory.users.list failed with error: Not Authorized to access this resource/api))

 

まとめ

拡張機能はストアでの公開を目標にアップデートしていますが、頑張って対応中ですので公開までもうしばらくお待ちくださいmm

なお、実装した当時はここまでで紹介した解決策でなんとか乗り切れましたが、

現在や今後の仕様変更でもっと簡易に実装できたり同じやり方では解決できない場合も出てきますので、参考にされる際は自己責任かつその時その時の一番いい方法で実装して頂ければと思います。

*1:conferenceDataオブジェクトはMeet URLが発行されている場合のみ含まれるので、Meet URLを発行していない場合はオブジェクト自体存在しません。

*2:毎週金曜開催の場合このような値が入っています。

*3:はい / いいえ / 未回答

【第2回】UUUM攻殻機動隊オンラインハッカソンを開催しました!

こんにちは!22卒エンジニアで現在内定者インターン中の久保寺です。 10月末に今年もオンラインハッカソンが行われました。

f:id:momokan928:20211115175913p:plain
弊社オフィスのスタジアムエリア

ハッカソンの概要

本来はハッカソンではなく半年に1回開発合宿をするのですが、

system.blog.uuum.jp

去年と同じくコロナ禍ということもあり、今年も開発合宿の代わりにリモートでオンラインハッカソンが開催されました。

【初】UUUM攻殻機動隊オンラインハッカソンを開催しました! - UUUM攻殻機動隊(エンジニアブログ)

今年のハッカソンのテーマは、「業務効率化」

今回のハッカソンは2回目ということもあり、ハッカソンの流れを熟知しているメンバーも多くなったため、去年よりもより実用的なテーマ「業務効率化」になりました。

※去年のテーマ「今年話題だったもの」

タイムテーブル

f:id:momokan928:20211115180447p:plain
タイムテーブル

去年は3日間あり、1日目にアイデア発表・中間発表などがありましたが、今年は2日間に短縮されてアイデア発表などは事前に行われました。発表はGooglemeetを使い進めました。有志でオフィスに集まり、オンライン&オフライン併用で開催されました。

参加チーム一覧

f:id:momokan928:20211115180652p:plain
参加チーム一覧

今回は全15チームの参加で、なんと去年の倍以上のチーム数となりました。

成果物の紹介

1.999 【無計画ハッカソン】

トップバッターからいきなり不穏なタイトルから始まりましたが.......

docbaseという社内のドキュメントがまとめられているWebアプリがあります。しかしdocbaseを見るには認証が必要なため、Slack内でそのリンクを貼ってもプレビュー機能は動作しません。それを上手い感じに動作させる拡張機能を作成していただきました。

f:id:momokan928:20211115180825p:plain

しかし完成はしていたのにデプロイがギリギリ間に合わず、成果を確認できませんでした(後日しっかり完成していた)。M1とかを見てても思いますが、やはりトップバッターは不利ですね。

このスライドは発表直前に慌てて作ったらしいです。

f:id:momokan928:20211115180915p:plain

2.ぶち【プロジェクトのER図を管理する方法】

f:id:momokan928:20211115181012p:plain

関わっているプロダクトにER図が無かったとのことだったので、ドキュメントの充実のためER図を簡単に作成できるツールを調べていただきました。

DBeaverというツールを使ったそうで、これを使ったら一瞬でER図ができたそうです。開発だけでなく、目的に合った最適なツールを探し出すことも業務効率化の最短ルートだったりしますよね〜

3.しだ【非エンジニアはじめてのGAS】

f:id:momokan928:20211115181151p:plain

支払証明書のファイルを取引先にメールで送信する作業があり、そのメールを手動でファイル添付をして送信するというやり方で時間が掛かっていたので、GASを用いてその作業を自動化していただきました。

GASを実行すると本当にメールが勝手に作成されていました。GASはこの発表で初めて知ったのですがとても便利ですね。毎日固定で行う業務があったら是非チャレンジしてみたいです。

4.スーパーカムイ改め特急カムイ号【社内コミュニケーションの可視化】

f:id:momokan928:20211115181326p:plain

「社内の人同士の繋がりがひと目で分かるものがないか」という要望があり、それを実現するようなアプリを作成していただきました。

Slack内でやり取りをしたことがある人同士をグラフで繋いでいくものなのですが、実装してみたらダークマターが出来上がっていました。

f:id:momokan928:20211115181353p:plain

「Slackでやり取りであった人」同士を全て繋ぐとダークマターになったので、「該当チャンネル内で繋がりのある人」に絞るといい感じに繋がりがある人同士の可視化ができていました。 ダークマター以外にも笑えるポイントが多々あって、成果物以外にもオーディエンスを飽きさせない工夫が施されていました。

※伏せ字には実名が含まれているため非公開とさせていただきます。

5.なかはし【GASをSlackコマンドから受け付けるためのゲートウェイをCloud Functionsでこしらえた】

f:id:momokan928:20211115181700p:plain

SlackからGASを操作できるような仕組みを作っていただきました。

しかし弊社のセキュリティが非常に厳しいのもあり、デフォルトのままではSlackからGASにはアクセスできません。そこでSlackからGASを呼び出すのではなく、Slack→GCPのCloud Functions→GASという流れで呼び出すことによりSlackからGASを動かすという仕組みでした。

6.カッコウ【GoogleMeetチャットSlack連携】

f:id:momokan928:20211115181916p:plain

Googlemeetのチャット欄のコメントをSlackにそのままリアルタイムでメッセージとして投稿されるという、弊社以外でも汎用的に活躍できるSlackアプリを作成していただきました。

ハッカソンのSlackチャンネルにも事前に仕込まれていて、参加者全員で実際に動いているのを確認できて沸いていました。 Googlemeetのチャット欄って退出したら消えてしまうので、これは本当に便利すぎます。一般公開まで漕ぎ着けていただきましたので現在私も1人のユーザーになっています。

chrome.google.com

7.KAZAMAN【チャットボット風Slackアプリ】

f:id:momokan928:20211115182104p:plain

弊社はドキュメントが非常に多く、新入社員さんにまず最初にレクチャーしなければいけない情報などが錯乱していて、どこに存在するかが分からないという悩みがありました。そこでチャットボット風お悩み質問Slackアプリを作っていただきました。

知りたい情報をプルダウンで選択できるようになっており、選択すると対象の情報先へのリンクが回答として返ってきます。 ハッカソンということもあり、スピード重視で動けばいいという発想になりがちですが、アーキテクチャーも素晴らしく、技術力が高くて感動しました!

8.現場猫【Slack leaving work】

f:id:momokan928:20211115182230p:plain

現場猫チームにはslackから勤怠が切れるSlackBotを作っていただきました。

弊社システムユニットの出退勤の流れとして、

1.出退勤アプリを起動して打刻する

2.出勤したことをシステムユニットのSlackチャンネルで報告

という流れで出退勤時に2回の作業が必要なため片方を忘れるということが頻発していました。このBotを入れておくと、Slackで出勤の報告をするだけで自動で打刻してくれるようになります。

システムユニット全員がおそらく抱えていた打刻の面倒臭さを解決してくれた素晴らしい成果物でした。

9.通りすがりの仮面ライダー㌠【無料で(←超大事)NFTアートを作る】

f:id:momokan928:20211115182819p:plain

詳しくなくて大変恐縮ですが、今流行っているNFTアートを作っていただきました。

NFTアートはブロックチェーンで管理された代替不可能なデジタルアートのことで、複製ができないため唯一性を証明することができます。世界では1つのNFTアートが70億円とかで落札された事例もあるみたいです。

本来であれば、作るのに手数料などのお金がかかるのですが、HokusaiAPIなどを利用することにより無料で作ることに成功したそうです。実装に使用されたNFTアートの絵はご自身で描かれたそうで、エンジニア以外の才能も惜しみなく発揮していました。

10.W遠藤feat.宗像【出欠席確認機能】

f:id:momokan928:20211115182843p:plain

オンライン会議特有の問題である「誰が来ていて誰が来ていないかイマイチ分からない問題」を解決するChromeの拡張機能を開発していただきました。

この拡張機能を有効にすると、Googlemeet上で誰が参加していて誰が参加していないかを一目で簡単に確認できるようになっています。カレンダー上では参加にしているけど実際の会議にはいないってパターンあるあるだと思うので、これは便利ですね。弊社以外でも活躍間違い無しの機能です。

そしてこのチームにはエンジニアだけでなくプロジェクトマネージャーもいて役割分担が完璧にされており、成果物だけでなく、発表のスライドもかなり面白いという二刀流で、参加者のハートを鷲掴みにしていました!

11.さわだ【シェルスクリプトでコマンドを一括実行する】

f:id:momokan928:20211115183223p:plain

詳細は社外秘なので詳しく載せられないのですが、 大量のCSVファイルを手動でDLして、aws s3 cpコマンドで一個一個アップロードしていたため作業に手間と時間がかかっていたものを、 シェルスクリプトでアップロード作業を一発で終わらせられるように対応していただきました。

12.かきき【youtube slack bot】

f:id:momokan928:20211115183445p:plain

Slackコマンドを叩くと、YouTubeの各種データを取ってこれるSlackBotを作成していただきました。

しっかり社内ドキュメントに操作方法なども残していただいたので、完成度抜群です。 弊社ならではの便利なBotですよね!

13.てんびん座【GoogleMeet × LIVE配信風リアクション 2.0】

f:id:momokan928:20211115183605p:plain
前回のGoogleMeet Live

前回大会優秀賞のGoogleMeet × LIVE配信風リアクションの拡張機能を更にアップデートしていただきました。

GoogleMeetの会議中に何も反応がないと寂しい・・みんなミュート・・🥺という寂しい状況を打破するために、 よくLive配信であるボタンを押したら❤️が画面に飛んでいくようなchrome拡張を作ってくださいました! 技術はFirebaseのRealtimeDatabaseを使っており、実演でもきちんと動作していて凄かったです!

【初】UUUM攻殻機動隊オンラインハッカソンを開催しました! - UUUM攻殻機動隊(エンジニアブログ)

f:id:momokan928:20211115184145p:plain

今回のアップデートではGooglemeetのピクチャ・イン・ピクチャ機能を実装し、そちらの画面でも❤️などのリアクションが見れるようになっています。試行錯誤を繰り返し14時間ぐらい実装に時間がかかったそうです。

私自身金魚のフンとして、このチームのメンバーとして参加させていただきました!

14.フィフ兄さん【Gmailに受信されたメールをSlackに通知させる】

f:id:momokan928:20211115184241p:plain

ここまでの成果物からも推測できますが、弊社で最も利用されているツールはSlackです。逆に言えばSlackと連携できていないツールはSlackをメインに見ている社員からすれば対応が遅れるリスクもあります。 なのでSlackと連携がされていないGmailをSlackと連携させて、Gmailに受信されたメールをSlackに通知させる機能を実装していただきました。

社内ドキュメントにもまとめていただきましたし、弊社以外にも需要の高い成果物でした!

15.WAGMI【スマートコントラクト実装 & NFT認証機能】

f:id:momokan928:20211115184336p:plain

通りすがりの仮面ライダー㌠でも題材になった、私からしたらヤムチャ視点のNFTの発表でした。

WAGMIチームではスマートコントラクトを実装していただきました。スマートコントラクトとはブロックチェーン上で契約を自動的に実行する仕組みのことで、NFTの売買などができるようになります?(浅い知識で申し訳ないです)

f:id:momokan928:20211115185145p:plain 通りすがりの仮面ライダー㌠の綺麗な女性の絵とは対極的な似顔絵がNFTアートとして表示されており、会場内からは笑いも出ていました。

結果発表について

f:id:momokan928:20211115185508p:plain

投票の基準としては実用性・ギーク度・完成度・プレゼン力の4項目を各5点満点の最高得点20点で総合優勝が決まります。

加えて点数に関係なく特別賞として、実用的賞ギーク賞完成度賞おもしろ賞の計4つの賞が用意されています。

結果発表

総合優勝

🏆W遠藤feat.宗像 f:id:momokan928:20211115185825p:plain

Googlemeetの出席確認拡張機能を開発したW遠藤feat.宗像チームが総合優勝しました。おめでとうございます! 票数が圧倒的だったそうです。

実用的賞

⭐しだ f:id:momokan928:20211115185948p:plain

実用的賞にはGASでメールの自動作成を実装していただいた、しだチームが実用的賞を受賞しました。 賞品のカップラーメン1ヶ月分に対し、場内は「いいなあああああ😳」と沸き上がりました。

ギーク賞

⭐KAZAMAN f:id:momokan928:20211115190027p:plain f:id:momokan928:20211115190112p:plain

様々なモダンな技術を幅広く使用されたKAZAMANチームがギーク賞を受賞しました。 ギークには寝ないことが必要不可欠とのことでモンスター1カートンが贈呈されました。

完成度賞

⭐カッコウ f:id:momokan928:20211115190315p:plain 売り物のような出来前のGoogleMeetチャットSlack連携拡張機能を開発したカッコウチームが完成度賞を受賞しました。 既に一般公開されているので気になる方は是非!

chrome.google.com

おもしろ賞

f:id:momokan928:20211115190455p:plain ダークマターをこの世に生み出すことに成功したスーパーカムイ改め特急カムイ号チームが受賞しました。 賞品のトイレットペーパー半年分に対し、場内からは「邪魔そう😩」と辛辣な声も上がりました(笑)

二次会

f:id:momokan928:20211115190533p:plain

ハッカソン終了後には二次会が開かれ、出社した社員皆でピザやお寿司を食べたりお酒を飲んだりしました。私はオンライン参加だったので二次会は不参加でしたが、後から写真を見ると羨ましい気持ちになりこの記事を書くのが嫌になってきましたね😩

皆さん仲良さそうで雰囲気がとても良かったです!

f:id:momokan928:20211115190602p:plain

まとめ

第1回オンラインハッカソンも素晴らしかったですが、第2回は作品のクオリティのインフレが進んでいる気がしています。

オンラインだと発表者が一人で喋っている感覚に陥ることがあると思うんですが、見る側はチャット欄で逐一反応したり、発表側は敢えて笑えるポイントを作ってみたりと今回も大いに盛り上げていただきました!

皆さんレベルの高い技術や未知の技術にも積極的に取り組んでいただき見ているだけでもワクワクするものばかりでした。来年以降のハードルがどんどん上がりますね〜。 なんだかんだ今年もコロナ禍が続きオンライン開催にはなりましたが、来年には対面での第3回ハッカソンが開催できたら良いですね。

rubyを通してUNIXプロセスを学ぶ

こんにちは、UUUMでエンジニアやっておりますオオハシと申します。

今までプロセスについて書籍やネットの記事の流し読みでなんとなくわかったつもりになっていたのですが
なるほどUnixプロセス - Rubyで学ぶUnixの基礎という書籍では
普段業務で取り扱っているrubyを用いてプロセスを説明されているのが特徴で
プロセスを理解する上での大きな助けとなりました。
以下、学習したことを大まかにまとめました。

全てのプロセスはID(pid)を持っている

p Process.pid

スクリプトファイルの実行やirb、各種shell、バックグラウンドプロセスなど
プログラムの実行毎に異なる値が割り振られる。

forkして子プロセスを作ってみる

fork
p Process.pid

出力される2つのpidの値が異なる
つまり2つの異なるプログラムが並列して実行されている。

親プロセスと子プロセスで処理を分けたい

p fork

親は子のプロセスIDを、子はnilを返り値にとるので
以下のように書ける

if fork
  p "親のプロセスID:#{Process.pid}"
else
  p "子のプロセスID:#{Process.pid}"
end

子プロセスの処理内容を指定したい

ブロックを渡す

fork do
  p "子プロセスのみ評価される"
end

孤児プロセスとは

親が先に終了してしまった子プロセス

fork do
  p "子プロセス開始"
  5.times do |i|
    sleep 1
    puts "#{i}秒経過"
  end
  p "子プロセス終了"
end
abort "親プロセス終了"

親プロセス終了後も子プロセスの処理が続行し、端末から<Ctrl-c>などで停止も出来ない

何故か?

→子プロセスは親プロセスを通じて間接的に端末制御されるため。
したがって親プロセスが先に終了すると子プロセスを端末による制御ができなくなる。

どうやって管理するのか?

子プロセスの終了を待つ

fork do
  p "子プロセス開始"
  5.times do |i|
    sleep 1
    puts "#{i}秒経過"
  end
  p "子プロセス終了"
end
Process.wait # 子が終了するまで親の処理が止まる。親が生きてるため端末制御も可能。
abort "親プロセス終了"

シグナルを送信する

例えば別のプロセスから孤児となったプロセスのpidを指定してシグナルを送ることでプロセス制御が可能。

fork do
  p "別プロセスから kill -INT #{Process.pid} を実行してください"
  p "子プロセス開始"
  5.times do |i|
    puts "#{i * 5}秒経過"
    sleep 5
  end
  p "子プロセス終了"
end
abort "親プロセス終了"

主要なシグナル一覧

f:id:bounce114:20210531074746p:plain

ゾンビプロセスとは

カーネルは終了した子プロセスの情報をキューに格納する。 Process.waitして子プロセスの終了ステータスを取得されないと 終了したはずの子プロセスの情報が取り除かれず、ゾンビプロセスとして取り扱われる。

ゾンビプロセス確認の仕方

pid = fork { sleep 1 }
puts "ps -ho pid,state -p #{pid}  を実行してください"
sleep 10

プロセス間で通信する

パイプ(単方向通信)

reader, writer = IO.pipe
# not opened for writing (IOError)
reader.write("読み込む側からは書き込み出来ない")

親・子のプロセス間通信をする

reader, writer = IO.pipe

fork do
  reader.close

  10.times do |i|
    # 子プロセスから書き込む
    writer.puts "#{i+1}回目の書き込み"
  end
end

writer.close

# 親プロセスで読み込む
while message = reader.gets
  $stdout.puts message
end

ソケット(双方向通信)

使用例

require 'socket'

child_socket, parent_socket = Socket.pair(:UNIX, :DGRAM, 0)
maxlen = 1000

fork do
  parent_socket.close

  2.times do |i|
    # parent_socket.sendされるまで待つ
    instruction = child_socket.recv(maxlen)
    child_socket.send("#{instruction} -> parent #{i+1}回目", 0)
  end
end
child_socket.close

2.times do
  parent_socket.send("parent -> child", 0)
end


2.times do
  # child_socket.sendされるまで待つ
  $stdout.puts parent_socket.recv(maxlen)
end

プロセスグループ・セッションについて

セッションは同一セッションIDを持つプロセスグループを束ねる
プロセスグループは同一グループIDを持つプロセスを束ねる
セッション内でセッションリーダーと呼ばれるひとつのプロセスが制御端末と接続し
セッション内全プロセスに端末から受信したシグナルを伝搬する。

終わりに

本書で解説されていたプロセスに関するごく一部を紹介してきました。
この他デーモンプロセスやシグナルの捕捉、rackその他Rescue, Unicornのプロセス管理など
プロセスに関する事項がrubyによって読みやすい形でまとめられています。
UNIXの基礎であるプロセスの知見を深めておくことは、プログラミング言語を問わず通用する技術であり
今後のエンジニアライフをより良くする上でも大変有用であると考えています。
プロセスについて理解を深めたい方は是非著書を手にとってご覧いただければと思います。

【初】UUUM攻殻機動隊オンラインハッカソンを開催しました!

こんにちは!エンジニアの中村(@chibiprogrammer)です!

去年の12月末にUUUMでオンラインハッカソンを行いました!

ハッカソンの概要

本来であれば半年に1回こんな感じで開発合宿をするのですが・・・

system.blog.uuum.jp

今回は情勢的に厳しいこともあり、 何かコロナ禍の中でも出来るイベントはないだろうかと考え、 リモートでハッカソンならできるのでは?という流れになり、 開催することが決定しました⭐️

今回のハッカソンのテーマは、「今年話題だったもの」

オンラインでの初ハッカソンなので、ハッカソンをやったことのない人も何人かいました。 なので、例えば「あつ森」「コロナ」「密」のように、

わかりやすくて、テーマの幅が広いものは何かと考え、今回は 「今年話題だったもの」というテーマにしました。

タイムスケジュール

f:id:chibiProgrammer:20210120142849p:plain
スケジュール

全体的な流れはこんな感じで、基本的にGoogleMeetを使って進めました。 最後の結果発表のみ、集まりたい人は会社に集まってそれ以外はGoogleMeetで参加する。というような形になりました。

参加チーム一覧

  • アミューズメント練馬
  • しぶえもん
  • レジャーランド六本木
  • ちびび
  • かきき
  • パワーコーダーズ
  • えんたつ

今回は全7チームに参加していただきました☺️

成果物の紹介

【周辺のショップリストをSlack Botから取得するアプリ】

f:id:chibiProgrammer:20210121120148p:plain
パワーコーダーズ

SlackBotでコマンドを打つと、周囲のショップリストを取得するアプリの作成をしていただきました! 最初はコロナの影響もあり、病院検索に限定されていましたが、要望があり他のショップも調べることができるようになっていました。 こちらはAWSとGCPのマルチクラウドで作成されていました!

また、開発中の様子がすごく楽しそうだったので個人的に好きなチームでした🤗

【あつ森チャットアプリ】

f:id:chibiProgrammer:20210120173606p:plain
アミューズメント練馬

今年かなり話題になったあつ森ですが、あつ森にはチャット機能がついており、そのチャット機能を使って他のユーザーと会話ができるのですが、 こちらアミューズメント練馬チームは、「私はPCユーザーだから、Terminalからチャットがしたい!!」というぶっ飛んだ発想であつ森チャットアプリを作ってくださいました。

グレーな感じはありますが、とにかく発想がユニークで面白かったです。

実演ではターミナルからあつ森にコメントを流したり、アクションをしたりして会場が盛り上がっていました!

【キーボード制作】

f:id:chibiProgrammer:20210120173100p:plain
えんたつ

GoogleMeetやZoomを使ってオンラインミーティングがかなり増えてきてると思うんですが、 オンラインミーティングの時、他の人が話している時は自分をミュートに何回もするといちいちミュートボタンを押したりするのはだるい・・ 爆速で切り替えたい・・

という思いを解決するために誰もやっていないキーボードを作成してくれました! 指一本でスムーズにミュートが切り替えれたり、発表では実際に半田付けしている動画なども見せてくれてとても楽しかったです!

【Uberもどき】

f:id:chibiProgrammer:20210120155248p:plain
かきき

3日間でここまで実装できるのか・・すごいなぁと最初に思いました!画面も綺麗でしたし、一連の流れもできていました🤗 サーバーに載せて実際動くところまでできてたことと、データの準備もきちんとしていてとてもよかったと思います! ハッカソンだったので時間が足りなかったところもあったと思うんですけど、ぜひ作り切って欲しいと思った作品の1つでした!

【GoogleMeet × LIVE配信風リアクション】

f:id:chibiProgrammer:20210120173741p:plain
しぶえもん

GoogleMeetの会議中に何も反応がないと寂しい・・みんなミュート・・🥺という寂しい状況を打破するために、 よくLive配信であるボタンを押したら❤️が画面に飛んでいくようなchrome拡張を作ってくださいました! 技術はFirebaseのRealtimeDatabaseを使っており、実演でもきちんと動作していて凄かったです!

このイベントの後も使っている人が何人かいて、実用的でニーズのあるいい作品ができていていいなと思いました!

【CoronaGame】

f:id:chibiProgrammer:20210120173819p:plain
ちびび

病気にかかって死にそうな主人公が3日間だけ元気な時にもどり、その3日間の間に病気にならないように健康的に過ごすという内容ゲームでした。

最近コロナが流行ってきて、在宅が多くなり、ゲームばかりしていたり、運動不足の人が増えてきているのをこのゲームをきっかけに見直してみよう!というコンセプトのゲームで、エンディングまで作ることができたのでよかったと思っています!

【ニコニコ風Youtube watch party】

f:id:chibiProgrammer:20210120174311p:plain
レジャーランド六本木

オンラインでみんなでYoutubeを見て楽しむようなサイトを作ろうとしてくれました。 今回はURLを貼り付けたらYoutubeから動画をとってきて、ニコニコ動画のように動画上にコメントを流すまでをやっていただきました! Vue CLIやVue.jsを使って実装しており、実際にコメントが流れていくのを見ることができて楽しかったです! これからもっといろんな機能をつけて欲しいなと思いました☺️

発表

投票基準

  • コンセプトの着眼点
  • コンセプトの実用性(構想だけでなく、動く)
  • 技術的に面白いか
  • UI/UXの良さ

賞の種類

  • 優秀賞
  • 井原さん賞
  • 中橋さん賞
  • 新谷さん賞

優秀賞は、参加してる人たちの投票で一番評価が高かった人に与えられました。

結果発表

⭐️優秀賞 & 新谷さん賞 しぶえもん f:id:chibiProgrammer:20210120162455p:plain

⭐️中橋さん賞 アミューズメント練馬 f:id:chibiProgrammer:20210120162306j:plain

⭐️井原さん賞 ちびび f:id:chibiProgrammer:20210120162751j:plain

参加した人のコメント

今回のハッカソンではまた違うテーマでハッカソンしてほしい! 成長した!というような声をもらえたので、とても嬉しかったです!

* 初めて使う技術を用いたから成長した。
* キーマッピングは初めて触ったので可能性を感じた。
* 0からvue.jsを用いたアプリを開発した事が無かったのでとても良い経験になった。
* 1ヶ月とか長期で開催できても良いと思いました運営お疲れ様でした!

まとめ

今回初めてのオンラインハッカソンを開催しましたが、通常のハッカソンとはまた違う雰囲気作りするのが難しかったです。 ただ、みなさんオンラインでもかなり盛り上げてくれて、自分が使ったことのない技術を使ったり面白いものをたくさん作ってくれました! 本当にいろんな作品を見ることができて、楽しかったです☺️ この状況がいつまで続くかわかりませんが、今回の経験を活かしてハッカソンやそれ以外にもいろんなイベントを開催しようと 思いました!

プロダクトマネージャーカンファレンス2020に参加しました!

こんにちは。PMチームの @mokkey19930617 です。

2020/10/27にオンラインで開催されたプロダクトマネージャーカンファレンス2020に会社の制度を利用して参加させてもらったので、そのアウトプットを書こうと思います!

内容はUUUMの事業的に関連があるTikTokのPMについてのセッション①と、個人的にPMとして参考になったセッション②の2つについて、それぞれセッションの要約と個人的な学びについてです。

※以下、PMという記載はプロジェクトマネージャーではなく、プロダクトマネージャーを指しています。

セッション①

クリエイターファーストなTikTok PMの仕事

スピーカー

TikTok Japan 久保田アレクサンダー卓磨さん

TikTokのユニークな点

コンテンツのディストリビューションが他のUGCと異なる。フォローワーゼロでもコンテンツがバズる可能性がある。

プロダクトチーム体制

  • Regional PM

    • 各地域のユーザーの体験を最適化する
    • 担当地域に最適化したアルゴリズムをエンジニアと協力して作り上げる
    • 担当地域のグロース
  • Feature PM

    • 全ユーザーの共通機能を中心に担当機能ごとに開発をリードする
    • 主にアプリのクライアント機能実装など全体的なUI/UXの最適化等

各地域で検証実験を行っていき、ベストプラクティスを地域間で横展開することで、グロースを加速する。

UGCプラットフォームのグロースサイクル

  1. コンテンツ制作: クリエイターが作成
  2. 視聴体験: フィードを通じて滞在する
  3. リテンション: 視聴体験で満足すれば、ユーザーが増えていき、コンテンツの種類が多様化していく
  4. コンテンツ制作: ユーザー(クリエイター・視聴者)が増える

キーとなるのはやはりコンテンツ制作なため、徹底してクリエイターファーストで開発を行っている。

しかしグロースサイクルがうまく回らないことがあり、TIkTokではAERフレームワークを使っている。

AERフレームワーク

Attract: クリエイターにそのプラットフォームへ移る動機を作る

フォローワーがいなくてもコンテンツが視聴者に届く仕組み

クリエイターからしても、どのプラットフォームに身をおくか悩むはず、どうやって安心材料を提供するかが問題。

TikTokではフォローしているクリエイターの投稿のみ表示するというフォローベースのフィードでなく、視聴者の興味ベースのフォードにすることで、新しいクリエイターでも平等にチャンスを与える仕組みになっている。 その仕組みによりフォローへの依存をなくし、視聴者に一番面白いコンテンツが届けることができる。

とはいえ、 フォローしているクリエイターの投稿は表示される。

なので、フォローベースと視聴者の興味ベースのフォードどちらかでもなく、いいとこ取りをしているという感じ。

Enable: クリエイターのポテンシャルの引き出し、活性化する

ネタ(アイデア)の民主化でコンテンツ制作のハードルを下げる
  • 従来のコンテンツ制作の暗黙のルール: 一人で作る文化 完全なオリジナリティが求められる

  • コンテンツ制作の新しいルール: みんなで作る文化 ネタ、型をマネしやすい、MEME文化

※MEME: ネット上でユーザーがマネとアレンジを重ねて楽しみながら広がっていくコンテンツ

TikTokでは様々なツールを活用することで、みんなで作る文化、MEME文化を実現しコンテンツ制作の敷居を下げている。

Retain: クリエイターが長期に渡って活動でき、報われる環境を作る

ファンエンゲージメントとマネタイズしやすい仕組みを設ける
  • ファンエンゲージメント 動画で返信できる機能、ライブ配信機能をすることでファンを集める

  • マネタイズ ライブ配信でギフティング、クリエイターマーケットプレイス(ブランドとクリエイターのコラボ(プラットフォーム)

まとめ

クリエイターファーストの仕組みづくり(TikTokではAER活用)をすることで、グロースを最適化する。

個人的な学び・所感とか

会社・部門内でベストプラクティスを横展開はやった方がいい

プロダクトチーム体制のところで担当地域でのベストプラクティスを会社で横展開して、他の地域に応用するという仕組みは参考になるため、PMに限らず部署内でこう言った知見の共有は積極的に行った方が良いと感じた。

MEME文化は使えそう

MEME文化を取り入れ、動画のネタや型をテンプレ化してクリエイターに提供する仕組みは、UUUMが提供しているYouTuber向けのサービス: CREAS などのクリエイター向けの各種サービスを通じで実現可能であり、プロダクトの価値を高めることにつながるのではと思う。

フォローワー0でも評価されるコンテンツディストリビューション

また、コンテンツディストリビュートの方法が他のUGCと異なり、興味・関心ベースでフィードが表示される点や、そこにクリエイター全員に平等な機会を与えるという背景がある点も興味深かった。 UUUMが最近買収したREC.などでは応用する機会がありそう。将来的にUGCプロダクトに関わることがあれば、是非参考にしたと思えるセッションだった。

セッション②

いま、PMに求められる本質的な力 ~満足度No.1プロダクトを生む組織、3つの条件~

スピーカー

エン・ジャパン株式会社 岡田康豊さん

PMの成長に重要なこと2つ

  • 土台となるテクニカルスキル
  • 本質的なスキル

本質的なスキルを伸ばすには組織が重要なファクターである。

エン・ジャパンの企画開発部門にあった3つの課題

  • あいつぐPM・エンジニアの退職
  • 低いテクニカルスキル
  • 外部パートナー1社の開発体制

そこでPMに必要なテクニカルスキルを6つの領域に分けた PM SkillChart HEX(ヘクス)を作成した。

チャート化して俯瞰することで現状把握し、キャリアを考え、やることを明確化

こちらのチャートを組織として打ち出し、PMの自己研鑽を組織的に促進することで、各PMの現状把握・必要なスキル・キャリアが明確になり、スキルアップをしていった。PJマネージメント能力も高まった。(ここ数年でHEXスコアが150%UP)

e.g. 自己研鑽して、統計スキルを身に付けデータ分析能力を磨いたり、プログラミングを学び自ら開発に参加したり、デザインから派生してフロントエンド開発を行ったり。

テクニカルスキルは重要だが、それだけでは不十分

テクニカルスキルはPMとしてとても重要だが、それだけでは不十分で、全てうまくいくわけではない。 加えて、テクニカルスキルの寿命は短く進化も早い。

必要なのは本質的な力。 また、その本質的な力を鍛える場がその組織にあるかどうかがとても重要。

本質的な力を養うことができる組織が押さえている3つのポイント

1. 抽象スキルの共通認識がある

PMには具体的なテクニカルスキルを持った上で、物事を抽象的に捉えて他者とコミュニケートする能力が必要。

Katz ModelでいうテクニカルスキルはHEXに定義されているスキル。 PMとしてはKatz Modelでいうヒューマンスキルやコンセプチュアルスキルがより重要である。

※Katz Model: 自分の職位が上がれば上がるほど、ヒューマンスキルやコンセプチュアルスキルが重要であるというキャリアの指針を示したモデル。

それらの抽象スキルを組織として定義し、共通認識とすることが重要。

e.g. エンジャパンのヒューマンスキルとコンセプチュアルスキルの抽象スキルのフレームワーク

  • 考え方

    • 自己変革性
    • 目標必達性
    • 多様受容性
    • 周辺変革性
    • 主観正義牲
    • 自発利他性
    • Enjoy-Thinking
  • 能力

    • 対人関係力
    • 発想力
    • 論理思考力
    • 組織貢献力

上記の項目にそれぞれ定義があり、例えば自己変革性だと「自己の現状に満足せず、学習や研鑽に努め、自分自身の改善、革新に挑戦している」と定義されている。

ただ、これらのスキルは独学で学ぶことは難しいため、共通見解を持った上で相互干渉を積極的に行うことが必要。

2. 答えより問いを重視する

PMというのはプロダクト価値を提供するマーケットに対して問いを投げかけ続けなければならない。 向き合うべきは上司ではなく、ユーザーやマーケットである。

達成したい事象に対して、どれだけ多く、確度の高い問いを投げかけられるかが重要。 上司は答えを聞く相手ではなく、共に問いを探求する相談相手。

3. その組織がフィードバックループを回し続ける

大事なのは質より量。 e.g. エンジャパンでは毎日個人がデイリーレポートを書いている。また毎週水曜に朝会を行っており、毎回そこで誰かが自分の成果を共有する15分のピッチを行っている。

自身が他者に晒される機会を作って、フィードバックをもらいやすい環境を整えることで、承認・称賛に繋がり、個人の成長を促すことができる。

+X

私たちPMは、なぜ、何のために働くのか?と言った、仕事の価値観が結局重要。 最終的には個々のそう言った価値観がプロダクトに反映されていく。

Inner Calling的な価値観。 ※Inner Callingとは動物的な本能である利己と、相手のことを思いやる利他の両面を持っていることらしい。

https://corp.en-japan.com/profile/message.html

まとめ(岡田さんからのメッセージ)

プロダクトを通じて社会を変革できるPMこそが日本をよくすることができる人たちだと思うので頑張りましょう。

個人的な学び・所感とか

HEX(ヘクス)わかりやすい

まず、従来のプロダクトマネジメントトライアングルとエンジャパンのPM SkillChart HEX(ヘクス)を比べると大分わかりやすく、僕のような駆け出しPMとしては理解しやすく感じた。

実際に自分を評価してみたら全く足りないチャートがあって悲しかったので、頑張ります。

UUUMでもこう言ったチャートを使って能力評価を行ってもいいかもしれない。前回評価の時からチャートが広がってたらなんか楽しそう。

本質的を力を養う組織が押さえている3つのポイントについて

マネージメント層の方々、こちらの整備よろしくお願いします🙇笑‍

個人的には答えより問いを重視するが結構胸に刺さるポイントだった。 正直に振り返ると、仕様や機能を検討する際に、「まーあの人がああ言ってて意見変えなそうだし、正直微妙だけどもうこの仕様でいいか」という感じで決めてしまった経験がある。 やはり、現状で何がベストなのかを考え、PMとして納得した上で決断しないといけないと思った。

また、これはどちらかというと組織というより、個人の意識の問題な気もするので、PMであれば誰でも絶対意識した方がいいと感じた。

抽象スキル

めちゃめちゃどうでも話ですが、 岡田さんがPMに求められる抽象スキルを「具体的なテクニカルスキルを持った上で、物事を抽象的に捉えて他者とコミュニケートする能力」 とおっしゃっていて、表現自体が過不足なくめちゃきれいな表現だなーと思い感動しました。 こんな適切な言葉を使えるPMになりたいと思いました。笑

エンジャパン、岡田さんの資料かなり有益、ありがたい

岡田さんのnoteをはじめ、エンジャパンからいろいろな資料が無料公開されているのがありがたい。

岡田さん note

エンジャパン これからの時代に必要な入社後活躍力(仕事に関する価値観や本質的な力についてまとまっています)

終わりに

以上でプロダクトマネージャーカンファレンス2020から学んだこととかのアウトプットになります。 一日、会社のお金で学ばせていただき、ありがとうございました!

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など考慮すべきことを実装していきます。