自分の使う OSS に GitHub で少し貢献した話

Ryohei Tsuda
20 min readAug 12, 2020

--

https://github.com/crowi/crowi/pull/709

われわれは、短い人生を授かったのではない。われわれが、人生を短くしているのだ。われわれは、人生に不足などしていない。われわれが、人生を浪費しているのだ。

セネカ. 人生の短さについて — 中澤務 訳

今年の4月にプログラマに転職して他の仕事と兼務しつつ見習いとして修行をしているのだが、ここ最近あまり成長実感がない。もう31歳なのにこんな状況でいいのかなと思いながら毎日を過ごしている。

そんなある日、会社から指示された課題の一環として、私も使っている OSS (オープンソースソフトウェア)に機能を追加することになった。

これに取り組む中で「自分が使うソフトウェア」を構成するコードを読んで修正していくことで、それぞれのコードが何をしているかの仮説とその検証が行いやすくなり高い学習効果が見込めるのではないか、という実感があったので書き留めておきたい。

この記事の想定読者

  • 自分
  • プログラマになったばかりの人
  • プログラマになったばかりの人を見たい人

ソースコードの読み方

本題に入る前に、ソースコードを読む心構えとして下記のスライドが参考になったので貼っておきたい。

上記のスライドで Satoru Takeuchi さんがおっしゃっている通り、漫然とコードを読んでも得るものは少ないと思う。特に自分のような初心者は尚更だ。

ちなみに Takeuchi さんの YouTube チャンネルは OS の仕組みを分かりやすく解説されていて面白いのでおすすめ。最近はこれと徳丸浩のウェブセキュリティ講座をよく見ている。

私にとってコードを読む目的は「機能の実装を理解し(私にとっては直近では React.js などの)ライブラリの実践的な使い方を学ぶ」ことなので、それを使っている箇所を自分で動かしながら「その機能がどう実装されているか、どのような背景でそうなっているのか」をコードと対応させながら調べていくのがよさそうだ。

スライドで紹介されている git コマンドを使ってソースコードを読む方法は「大規模」ということで数十人、数百人で開発しており長いコミットログを積み重ねてきたようなソフトウェア向けだと思う。10人未満くらいの規模で開発してる OSS なら自分で動かしながら読むのが効率的なような気がした。

今回の題材となる OSS

課題として与えられたのは Markdown Wiki の OSS である Crowi だった。これを読んでいる皆さんは何でも自分の課題に応じて好きに選べばよいと思う。私の場合は TypeScript + React Hooks で Web アプリケーションを作る練習をするためだった。

実際にやってみると Wiki (or ブログの CMS)は Web アプリケーションの様々な要素が盛り込まれていて、 Web プログラミングの教材としてなかなか優れている。

私に与えられた課題としては、この Crowi の管理画面に各ユーザーの情報を見られる View があるので、そこから開けるドロップダウンメニューからモーダルを出して各ユーザーの名前とメールアドレスを編集する機能を追加することとなった。

対象の選び方

自分が取り組む対象の OSS を選ぶにあたっては、下記の点を重視するとよさそうだ。

  • 自分や周囲が頻繁に使っており、機能や挙動をよく把握している
  • 自分が今学びたい技術が使われている
  • Docker 等で)簡単に環境を構築できる

最後の点は、自分で少しずつ変更しながら動かして挙動に対する理解を確かめる(ここをこう変更すると動かなくなるとか)ときに依存関係が面倒なことになっていると環境を構築・再構築するだけで疲弊してしまうので、それらを docker-compose 等で簡単に構築できるのが重要だということを言いたかった。 Docker でなくても同じことができれば何でもいい。

Docker に詳しくなくても Image や Container 等の概念を大まかに理解しており Google で検索しながらコマンドを実行できれば十分だと思う。かくいう私も最近になって使い始めたし、 Dockerfile やdocker-compose.yml は一行も書いていないし、基本的なコマンドの書式すら覚えておらず大抵はコピペで何とかしている。

Docker の概念を学習するにあたっては、公式ドキュメントさくらのナレッジがお勧め。

Crowi を Docker で建てる

Docker をインストールしていれば、README 記載の docker-compose コマンドを使うことで簡単に localhost で Crowi を建てることができる。

docker-compose up の実行後に localhost:3000 で Crowi が動くようになるが、このとき Docker に割り当てられたメモリが不足して起動に失敗するケースがある。私の場合は Docker for Mac のデフォルト設定のまま 2GB を割り当てていたのだが、これだけでは何度やっても起動に失敗し、割当を 4GB に上げたら一度で起動に成功した。その後 8GB に上げた。

Docker がうまく動かないときは docker-compose logs -f | grep なんとか みたいなコマンドで Docker のログを調べて error とか crashed とか表示されている箇所を抽出し、詳しい人に聞くとよい。

うちの Mac では Docker にこれだけしか食べさせてあげられない

Docker はメモリだけでなく CPU や Disk Image 等のリソースを食いそうな雰囲気を感じており、次に買う PC は強いやつにしようかと考えている。

どこか壊してみる

うまく動いているソフトウェアを一部だけ壊してログや stack trace を読むことで、どこから何が呼び出されてどのような処理をしているかが何となく分かってくる。

stack trace は下記のような trace (痕跡)で、関数の call (呼び出し)を stack した(積み上げた)もの。 call stack とも呼ばれる。

呼出は下の行から順番に行われていて、最終的に一番上の行の Error が出ていることが分かる。ファイル名の後の数字は行数と列数で、例えば下記の表記は admin.ts というファイルの 45 行目、左から 10 列目を指す。

/crowi/lib/routes/api/admin.ts:45:10

上記は実際に管理画面の機能開発をしていて出た stack trace で、ここから課題を解決する定跡としてはこの中から自分が変更した箇所(このときは admin.ts だった)を探し出し、それが何から呼び出され(stack trace の下を見る)、何を呼び出しているか(stack trace の上を見る)を読む。

なお、自分の変更した箇所を読むのは既に動いているコードより自分のコードが間違っている可能性の方が高いという自己認識によるものなので、自分のコードに自信がある玄人は他人のコードやライブラリのバグ等を疑ってそちらのコードを読むといいと思う。

破壊活動の心得

ローカルの Docker 上で動かす分には派手にコードや環境を壊しても誰も困らないし、何か起きてもコンテナを作り直したり git stash すればすぐ治せる。 git stash は最後にコミットした点に戻ることができるコマンド。実は git stash push ファイル名 でファイル単位で git stash することもできる。また、 git checkout コミットハッシュ で特定のコミットに戻ることもできる。

壊したところを治したり自分なりに変えてみたりすると、変数や関数の役割がだんだん分かってくる。もちろん誰か周りに詳しい人がいれば仕様を聞いた方が早いのだが、自分で壊しながら理解することでより的確な質問ができその人への負担も軽減できるというメリットがある。

この破壊活動は環境さえ整っていれば誰でも一人で何度でもできるので、新しくソフトウェアの開発に携わるときにはまずやってみるのが良いかなと思った。私はしばらくこのスタイルでやって行こうと考えている。

もっとも、 Docker 等でかんたんに環境構築できる状況でなければこのようなスタイルを続けることは難しい。そのためまずその状況を作るところから始めるのがよさそうだが、私は幸運なことにまだそうなっていない。

ドキュメントを読みながら全体像を把握する

対象の OSS の README や依存先ライブラリの公式ドキュメントを読み、どういう技術が使われているのか概要を把握する。

Crowi の管理画面は TypeScript や React Hooks で作られていて、 CSS には reactstrap という Bootstrap CSS を React Component 化したものが使われている。データベースには MongoDB が使われている。

React はチュートリアルのサンプルアプリと Web サイトを少し作ったくらいで TypeScript と React Hooks については全く分からなかったので、公式ドキュメントを読みながら動かして試してみた。

TypeScript

TypeScript は文字通り「型(Type)」がついた JavaScript で、関数の引数や戻り値の型を定義することで、実行前に(コンパイル時に)その定義に沿わないコードを検出できる。

TypeScript はコンパイルされた後は普通の JavaScript なので、JavaScript が動くところならどこでも動く。

React Hooks

React Hooks は React で「状態(State)」を管理するための機能。React に限らず Web アプリケーションでは様々な状態を管理し、その状態に応じて処理を行うことが多い。例えばフォームの入力に応じて変数で一時的に値を保持し(これが「状態」)、実行ボタンを押したらデータをサーバーに送るなど。

React Hooks の useState を使うとこのような機能を簡単に作ることができる。 useState の戻り値が「状態」と「状態を更新する関数」となるので、これらを分割代入しておき、コンポーネント上でのイベント(例えばボタンをクリックしたり)のコールバックとして更新関数を呼び出して状態を更新することでコンポーネントに最新の状態が反映される。 React Hooks には useState 以外にも様々な関数が用意されており、目的に応じて使い分けたり自分でカスタムのフックを作ったりする。

めちゃくちゃ雑な React 史

昔は皆 Web アプリケーションを作るときユーザーの行動に応じて jQuery とかで DOM を書き換えていたが、設計が面倒なのと DOM を更新した箇所だけ書き換えるのを楽したいということで仮想 DOM という概念が広まり、その実装の一つとして Facebook 社によって React が誕生した。

React Hooks 以前の伝統的な React アプリケーションで状態を扱うときは class ベースのコンポーネントを作成し、その class 内で state を宣言しその値を必要に応じてリフトアップしてコンポーネント間で使いまわしながら UI を作っていた。なお、 React のチュートリアルでは今でもこのような方法でコンポーネントを作ることになっている。

しかし、実務上はデータの流れやどこで何が変更されているかが分かりづらいとかの問題があって、データの流れを一方向に統一し、引数を取って JSX を返す関数を組み合わせてコンポーネントを作ることが主流になった。これには Flux アーキテクチャという名前がついた。

このアーキテクチャを採用するにあたって今までクラスとかで管理していた状態をどう管理するかみたいないろいろな問題があり、 Flux の実装の一つとして Redux というライブラリが生まれた。しかし Redux は Redux で割と大変だったので React Hooks が生まれた。

また、 JSX を返す関数が大量に生まれたり JavaScript の役割が増えてモジュールとして細分化されるようになり引数や返り値の型を定義しておかないと複数人で開発するのが面倒だったので、そういう人が TypeScript を使うようになった。

多分こんな感じだと思う。間違ってたらすみません。詳しい方いたら教えて下さい。

ドキュメントの読み方

自分が初心者ということもありライブラリの使い方はドキュメント読むだけではいまいち分からなかったので、動かしながら都度ペンで紙に書いては捨て、ある程度わかってきたら Crowi にまとめてその URL をつけて詳しい人に不明点を質問していた。

自分用のメモに使うツールは何でも好きなもので良いのだが、

  • Markdown で書ける
  • 画像を貼れる
  • コードブロックを書くとシンタックスハイライトしてくれる
  • URL で共有できる

あたりの機能があると便利。

Fork して機能を追加する

ユーザーのメールアドレスと名前を管理画面から編集できるようにするためには何をすべきか。

ひとまず GitHub で Fork して動かしながらコードを読む。

Crowi の管理画面の全体像

Crowi 全体では React を使ったクライアントサイドでのレンダリングとサーバサイドでのレンダリングが同居しているが、管理画面はほぼ React で作られている。

コンポーネントは管理画面のメニューごとにファイルが分割されており、 client/components/Admin 以下にまとまっている。

今回はユーザー管理機能を編集するので /User 以下を見る。 UserPage.tsx が様々なコンポーネントを import して管理画面を構築していることがわかるので、そこから深堀りしていく。

データの流れを分かりやすくするため、ユーザー管理画面に関する React Hooks の state は UserPage.tsx 上で宣言しておき、そこから各コンポーネントに props として渡していく形でページを作る。

やるべきこと

  • モーダルやフォームなど client を作る
  • フォームを validate する express のミドルウェアを作る
  • Request を処理して Response を返す Controller を作る
  • Controller が呼び出す Model を追加する
  • POST Request を受け取って幾つかのミドルウェアを連続で呼び出して Response を返すためのルーティングを定義する

Model は Crowi が使うデータの構造とそれらを操作する関数を集めたもの。既にメールアドレスを update する関数があったので、それを参考に名前とメールアドレスを update する関数を追加して Controller から呼び出すようにした。

コードを書く時間より自分がやるべきことが何なのかを理解する時間の方が圧倒的に長かった。実際にやるべきことを明確にイメージできれば、コードを書く時間はさほどかからない。

Pull Request を出す

GitHub 上で差分を見ながらコードをレビューしていくときは Pull Request (PR)を作ると便利だ。下記のように追加した行を緑、削除した行を赤で見ることができる。

← before | after →

PR を作るときは変更の内容をかんたんに書くと他の人に見てもらったり自分で見直すときに分かりやすい。

Markdown で書ける

問題なければ Merge pull request 。 PR にコメントすることもできる。

conflict が起きるか CI でエラーがあると Merge できない

私は恥ずかしいことに誤っていきなり本家の repository に自分用の PR を作ってしまった…そして本家の人から 👍 の reaction がついて気づいた。まずは base ref を自分で Fork して作った repository にして PR を作り、問題がなければそこから本家に PR を出すのがよいと思う。

ここで Create new pull request すると本家に PR が出されてしまう

ちなみに、大きなソフトウェアだと Pull Request 作法みたいなのがあるそうだ。例えば React を活用したフレームワークの一つである Next.js では contribute するためのガイドをこのような形で公開している。

Pull Request 余談

余談だが、 branch で行われた変更を master に取り込む際には Git の管理システムによって Pull Request と Merge Request の2種類のやり方がある。 GitHub では Pull Request しかないけど、同じリポジトリ内での Pull Request が実質的に GitLab の Merge Request と同じ?

- master + ?

なお、 GitHub においてはデフォルトで作られるブランチとしての master という名前は近い内に変わるらしい。

https://twitter.com/natfriedman/status/1271253144442253312

Nat Friedman さんは GitHub 社の CEO。ちなみに Git は v2.28.0 でデフォルトのブランチ名を変えられるようになったそうだ。

git rebase でコミットをまとめる

自分の作業 branch から本家の master に Pull Request を出すと、自分の恥ずかしい作業履歴が永久に残ってしまう可能性がある。

こういうときは git rebase -i コミットハッシュ で複数のコミットをまとめる。一行が一コミットに対応していて、これは使う(pick)とか前のコミットと統一する(squash)とかを指定できる。

ちなみにこのとき行を削除するとコミットログではなくコミットそのものが消失するらしい(怖いのでやってない)。

rebase が完了したら git log --oneline とかでコミットログの一覧を出してみる。すると…

なお、 rebase は本来は恥ずかしいコミットを消すための機能ではなく、自分が作業している間に本流に加えられた変更などを自分の作業ブランチに取り込んだりするときに使う。公式の情報はこちら

テスト書いてないとかお前…

今回の機能追加にあたって複数のファイルを修正した。当然ながら自分の手ではいろいろなパターンの入力を試してみたのだが、テストを書いてないので Codecov から coverage が下がったという連絡がきた。

様々なテストを充実させるのも貢献になるので、今後はこれをやってみようと考えている。仕事でも間違いなく必要になるだろうし。

Crowi では jest が使われており、例えばユーザー周りの Model のテストは次のように書かれている。

感想

実はめっちゃ時間がかかった。

他の仕事(経理とか労務とか)をやりながらとはいえ、コードを読み始めてから機能を一通り完成させるまで2 ~ 3週間かかった。最初に課題を言い渡されたときは3日くらい、長くて1週間程でできると思っていた。素人の見積もりは全くあてにならない。

進捗が悪く何も解決しない日が続いたので、やる気が途中で著しく下がったりした。転機になったのは自分で書いたコードで client から server までデータを渡しながら処理を一通りログを出しつつ実行できたことだった。これで一挙に様々な小さな問題が解決して React Hooks や TypeScript の仕組みの理解が進んだことでやる気が高まった。

その後も課題が解決しないときはやる気が下がることが多かったが、これはまぁそういうものかなと思っている。そういうときは小さな課題を解決したり基本の理解を確認し直したりして自己肯定感を高めている。自分で好き勝手にできる小さなサブプロジェクトとかがあると気分転換になっていいかもしれないので、個人開発にも手をだしていきたい。

OSS について

やる前は OSS についてハードルが高いなと思っていた。

その理由としては(自分がプログラミング下手くそというのもあるが)、自分が使うソフトウェアにとって OSS 化されている部分はユーザーが触る箇所から遠いことが多く、それらの多くが具体的にどのような機能を提供しているのかあまり理解できないということがあった。

しかし、その OSS を何らかの形で直接使っておりその機能として何が提供されているのかを理解できれば、プログラミング自体は初心者であっても何かしら貢献できるところがあると思う。

最後に

これは OSS に限らないしプログラミングに限る話でもないが「自分が使うツールを自分でメンテナンスする」という行為はとても楽しい。

前職で経理や Product Manager として仕事をしていた時期も、自分の仕事を自動化する Excel やスプレッドシートを作るのは楽しかった。私がプログラマに転職しようと思った理由として、自分が今までやってきた経理の仕事を自動化していくために自分が欲しいものを自分の手で作りたいということがあった。アイデアはあるので、少しずつ作って OSS として公開したい。

ここまで読んだ皆さんは OSS に興味があると思うのでぜひ自分が使っている OSS に貢献してみてください。

これは Merged 記念に飲みに来たビール in P2B Haus

--

--