Galileo Figaro

常に初陣!

.gitattributes による改行コード設定の説明

概要

Git を用いた複数人でのファイル管理では、必ず改行コードの問題が付きまといます。 メンバーが WindowsLinuxMac といった異なる環境を使っていた場合はもちろん、 チーム全員が同じ OS を使っているのになぜか改行コード問題に悩まされることもあります。

そうならないために .gitattributes を用いて改行コードの設定をしましょう。

主なファイルと改行コードの扱い

まず、各ファイル形式ごとに改行コードが決まっているものもあれば決まってないものもあります。 どういうファイルでどの改行コードを使うべきか、おさらいしておきます。

ファイル形式 改行コード
.bat CRLF とすべき。
.sh LF とすべき。
.md 環境によって変えるべき。
.bmp 改行コードという概念はない。

.bat と .sh は特定の改行コードを使わなければいけない代表例です。 Windows 系のスクリプトファイルである .bat ファイルは CRLF を、UNIX 系のスクリプトファイルである .sh ファイルは LF を使わないと、誤作動を起こす可能性があります。 なお、別の例でいうと CSV ファイルも CRLF を使うのが標準規格のようですね (RFC4180)。あまり問題になる場面は少ないかもしれませんが。

.md は、改行コードに関して特に決まりがありません。 そのような場合は、ファイルを利用する環境によって変えるべきでしょう。 エディタによっては、システム既定の改行コードで保存するものが多いです。 Windows 系の環境で利用する場合は CRLF、それ以外の環境では LF として扱うのが、いろいろ都合がよいかと思います。

様々な環境で上記のファイルをチェックアウトした際、それぞれの手元の作業ディレクトリではどうなっていてほしいのかを、下図に示します。 なお、ファイルを利用するのは開発者だけではありません。 ビルドサーバーのような CD/CI の用途でもリポジトリをチェックアウトするかと思いますので、下図に含めています。

開発者B (Windows)
開発者B (Windows)
開発者A
(Linux)
開発者A...
ビルドサーバー
(Linux)
ビルドサーバー (Linux)
Remote
Repository
Remote...
.sh
.sh
Checkout
Checkout
Checkout
Checkout
Checkout
Checkout
.bat (CRLF )
.bat (CRLF)
.sh (LF )
.sh (LF)
.md (LF )
.md (LF)
.md
.md
.bat
.bat
.bmp
.bmp
.bmp
.bmp
.bat (CRLF )
.bat (CRLF)
.sh (LF )
.sh (LF)
.md (CRLF )
.md (CRLF)
.bmp
.bmp
.bat (CRLF )
.bat (CRLF)
.sh (LF )
.sh (LF)
.md (LF )
.md (LF)
.bmp
.bmp
Text is not SVG - cannot display

環境によって改行コードを変わらないようにしてみよう

上で例示したファイルのうち、まずは .bat や .sh のような、いつも特定の改行コードで扱いたいファイルについて見ていきましょう。 それを実現するには、.gitattributes に下記のように書けばよいです。

*.bat text eol=crlf
*.sh  text eol=lf

.bat と .sh について text という属性が付いていますね。 この text 属性が付いていると、テキストファイルとみなされます。 Git はテキストファイルに対して、チェックアウト時とチェックイン時に、改行コードについて下記のような処理を行います。

  • チェックアウト時

    eol=crlf の場合は CRLF に変換する。eol=lf の場合は変換処理しない。

  • チェックイン時

    常に LF に変換する。

つまり、eol=改行コード を指定することで、各自の開発環境ではその環境に左右されず常に特定の改行コードで扱うことができます。 一方、チェックイン時は常に LF に変換されるので、リモートリポジトリに CRLF が混入することを防ぐことができ、全てのファイルを LF で統一できます。(下図)

Remote
Repository
Remote...
.sh (LF)
.sh (LF)
チェックアウト
チェックアウト
.bat (LF)
.bat (LF)
.bat (CRLF )
.bat (CRLF)
.sh (LF )
.sh (LF)
.bat は CRLF に変換
.sh は変換しない
.bat は CRLF に変換...
チェックイン
チェックイン
.bat は LF に変換
.sh は LF に変換
.bat は LF に変換...
Text is not SVG - cannot display

(余談ですが、正確には改行コードの変換はインデックスと作業ツリーの間のコピー時に行われるようです。 ただ、リモートリポジトリと手元の作業ツリーとで対比させて説明したほうが分かりやすいと判断し、 本記事ではインデックスを介在させていません。)

eol=lf は LF に変換するのではない

少し話がそれますが、前節で eol=crlf の場合はチェックイン時に CRLF に変換することを述べました。 一方、eol=lf の場合は「LF に変換する」ではなく「変換処理しない」と書かれていたことにお気づきでしょうか。

eol=lf の場合は変換処理はされません。 なぜなら、リモートリポジトリでは全てのファイルが LF で統一されているハズなので、変換処理しなくても LF になるからです。 もし、LF とすべき .sh ファイルが、リモートリポジトリに CRLF として入っていた場合でも、LF への変換処理はされません。 すなわち、CRLF としてチェックアウトされてしまうので注意しましょう。 たぶん、リモートリポジトリはすべて LF であるという想定で、Git が設計されているのだと思います。

Remote
Repository
Remote...
.sh (CRLF)
.sh (CRLF)
チェックアウト
チェックアウト
.sh (CRLF )
.sh (CRLF)
変換しない
変換しない
チェックイン
チェックイン
LF に変換する
リポジトリに登録されている
ファイルと異なる
(差分として検出)
LF に変換する...
(.gitattributesが 設置され 以前に
登録されるなどして)
CRLFとしてリモートリポジトリに登録されているファイル
(.gitattributesが設置される以前に...
eol=lf だと変換しないので、
CRLF としてチェックアウトされる
eol=lf だと変換しないので、...
Text is not SVG - cannot display

また、チェックイン時には常に LF へ変換しようとしますので、 リポジトリに登録されているファイル (CRLF) と異なってしまいます。 (クローンしただけで差分が出てしまう。)

環境によって改行コードを変えてみよう

ここまでで、環境によらず常に特定の改行コードでチェックアウトするには、 texteol=○○ を指定すればよいことが分かりました。

次に、チェックアウトする環境ごとに改行コードを変えるべきファイルについて見ていきましょう。 環境ごとに最適な改行コードに変えるには、.gitattributes に下記のように書けばよいです。

*.md  text

先ほどとは異なり、eol が指定されていません。 このように、text は付与されているが eol が指定されていない場合、 Git の設定によって eol=crlfeol=lf かが自動的に決まります。 その決め方は、具体的には後述しますが、大雑把に言うと

  • Windows であれば eol=crlf を指定したときと同じ
  • それ以外であれば eol=lf を指定したときと同じ

と思ってもらってよいでしょう。

Remote
Repository
Remote...
チェックアウト
チェックアウト
.md (LF)
.md (LF)
.md (CRLF or LF )
.md (CRLF or LF)
Windowsの場合
→ .md は CRLF に変換 (eol=crlf と同じ)

それ以外の場合
→ .md は変換しない (eol=lf と同じ)
Windowsの場合...
チェックイン
チェックイン
.md は LF に変換
.md は LF に変換
Text is not SVG - cannot display

このように、eol=改行コード を指定しないことによって、Windows 環境では CRLF、それ以外では LF として、ファイルをチェックアウトできます。

「eol=○○」を指定しないときの判定

前節で、text が付与されているが eol=改行コード が指定されていない場合は、大雑把に言うと Windows 環境であれば eol=crlf と同じで、それ以外であれば eol=lf と同じになると述べました。 ここでより詳細について語ろうと思います。 (細かい話なので、飛ばしてもいいかもしれません。)

eol=改行コード が指定されていないテキストファイルについて、Git はどのように eol=crlfeol=lf かを判定しているかというと、 設定 (git config) の core.autocrlfcore.eol の設定を見て判定しているようです。 具体的には

  1. core.autocrlftrueinput であれば、その core.autocrlf の設定に従う

    • core.autocrlf=true はチェックアウト時は CRLF に変換、チェックイン時は LF に変換する。すなわち .gitattributes で eol=crlf を指定したときと同じ。
    • core.autocrlf=input はチェックアウト時は変換せず、チェックイン時は LF に変換する。すなわち .gitattributes で eol=lf を指定したときと同じ。
  2. core.autocrlftrueinput でなければ、core.eol の設定に従う

    • core.eol=crlf であれば .gitattributes で eol=crlf を指定したときと同じ。
    • core.eol=lf であれば .gitattributes で eol=lf を指定したときと同じ。
    • core.eol=native であれば、Git が実行されている OS によって決まる。(core.eol を明示的に指定しない場合もこれ。)

      • Windows 環境であれば .gitattributes で eol=crlf を指定したときと同じ。
      • それ以外であれば .gitattributes で eol=lf を指定したときと同じ。

core.autocrlf は、基本的に Windows であれば true で、それ以外 (LinuxMac) では input または false と設定します。 core.eol も、多くの場合デフォルト設定のまま使うことが多いでしょう。

そのため、本記事では大雑把に Windows では eol=crlf を指定したときと同じで、 それ以外では eol=lf を指定したときと同じ、と説明しました。

text=auto について

ここで、text 属性に関する別の指定方法の紹介をしておきます。 text 属性には auto という文字列を指定することができます。 例えば下記のように記述します。

*.html text=auto

このように指定すると、「.html ファイルについて、text 属性を付与するかどうかは Git の判断に任せる」という意味になります。 「拡張子が .html ファイルなんだからテキストファイルと判定されるに決まってるじゃないか」と思うかもしれませんが、 Git はそんな単純な判定は行っていません。 ファイルの中身を調べて、テキストファイルか否かを判定しています。(判定方法は後述)

テキストファイルだと判定された場合は、これまで紹介した text 属性が付与されたときと同じ動きをします。 すなわち、チェックイン・チェックアウト時の変換が行われます。

バイナリファイルと判定された場合は、チェックイン・チェックアウト両方とも変換はされません。

.gitattributes で指定されなかったらどうなるの?

.gitattributes で指定のなかったファイルは、git config の core.autocrlf の設定に従い、改行コードが変換されます。

core.autocrlf については詳しくは説明しませんが、例えば

  • Windows PC が複数台あり、基本的には CRLF で扱いたいが、一部の PC だけは LF でチェックアウトさせたいファイルがある。
  • チェックアウトではなく、リモートリポジトリから直接 CRLF 形式のファイルを入手し、利用したい (例えば GitHub は生ファイルをダウンロードするボタンがありますね)。

といった場合には、.gitattributes には指定せず、core.autocrlf に任せることになるかと思います。 しかし、そのような運用は避けた方が良いと考えます。

core.autocrlf の設定はチェックアウトする環境ごとに存在するので、チームの誰かが設定漏れをしていると、意図しない改行コードでリポジトリに入ってしまう可能性があります。 私の経験上、ほぼ必ず設定漏れが発生するといってもいいでしょう。 なので、core.autocrlf には任せず、ちゃんと .gitattributes に記載して、リポジトリ全体として統制をかけることをおすすめします。

全てのファイルに対する指定

前節で、「core.autocrlf には任せず、ちゃんと .gitattributes に書くことをおすすめするよ」と言わせていただきました。 では、全てのファイルについていちいち記載しなくてはいけないのでしょうか。

そのリポジトリで扱われる全てのファイルを、抜け漏れなく列挙するのは大変ですね。 そこで、下記のように書くことで、その手間を省くことがよく行われます。

*     text=auto
*.bat text eol=crlf
*.sh  text eol=lf
*.md  text

1 行目で、全てのファイル (*) に対して、text=auto が指定されており、eol は指定されていません。 つまり「全てのファイルについて、テキストファイルかバイナリファイルかは Git の判断に任せ、テキストファイルの場合は環境に合わせた改行コードの処置を行う。」という意味ですね。

また、.gitattributes の記載は、前に書かれた設定が後に書かれた設定で上書きされる、いわゆる後勝ちになります。 例えば .sh ファイルは、1 行目のルール (* text=auto) にマッチしますが、3 行目のルール (*.sh text eol=lf) にもマッチするので、この場合は 3 行目のルールに従うことになります。 言い方を変えると、.bat と .sh と .md の 3 つの拡張子に該当しない全てのファイルは、1 行目のルールに従うことになります。

このように、最初に全てのファイル (*) にマッチするルールを設けることがよく行われます。 こうすることで、全ての拡張子に対するルールをいちいち書かなくてもよくなります。

テキストファイルとバイナリファイルの判定を間違える可能性

.gitattributes の定石として、先頭に * text=auto を指定することがあると紹介しました。 しかし text=auto を指定して、テキストファイルかバイナリファイルかを Git が判断する場合、 Git がその判断を間違える可能性もあります。

Git は、テキストファイルかバイナリファイルかは、ファイルの先頭 8000byte を調べ NUL (0x00) があるかどうかで判定しているようです。

https://qiita.com/okuoku/items/a21bfa68570ca67817ac

Git は、テキストファイルは UTF-8 互換の文字コードエンコードされているという前提で作られています。 UTF-8 エンコードされたテキストファイルには 0x00 が含まれていません。 一方、バイナリファイルには 0x00 が含まれている確率はとても高いです。 このことから 0x00 を判定基準に用いているのだと思います。

ただ、UTF-16 など 0x00 が含まれるテキストファイルもありますし、 逆に 0x00 が含まれないバイナリファイルもあります。 その場合、Git は判断を間違えます。

テキストファイルかバイナリファイルかの判断を間違えた場合、改行コードの変換がうまく機能しません。 特に、バイナリファイルなのにテキストファイルだと間違われた場合は致命的です。 チェックインの時にデータの中の 0x0D 0x0A という並びが 0x0A に変換されたり、逆にチェックアウトの時には 0x0A が 0x0D 0x0A に変換されてしまいます。 (バイナリデータが壊れてしまいます。)

そのような間違いが起こらないようにするために、テキストファイルかバイナリファイルかを明示的に指定するようにしましょう。

あるファイルがテキストファイルであるということを指定するには、既に説明したように text 属性を付ければよいです。

*.md  text

また、Git はテキストファイルは UTF-8 で扱う前提なので、それ以外の文字コードの場合には working-tree-encoding を指定するようにしましょう。 例えば次のように指定します。

*.ps1 text working-tree-encoding=UTF-16 eol=crlf

逆に、あるファイルがバイナリファイルであるということを明示的に指定するには binary を付けます。 なおbinary-text -diffという指定を行うマクロになります。

*.bmp binary

まとめ

ここまでの話をまとめると、冒頭のような例を実現するためには、次のような .gitattributes を書けば良いことになります。

*     text=auto
*.bat text eol=crlf
*.sh  text eol=lf
*.md  text
*.bmp binary

ここで、.md ファイルが UTF-8 で記述されていれば、「テキストファイルである」とわざわざ指定しなくてもよいでしょう。 * text=auto で正しく判定できるからです。 すなわち、次のようにしても OK です。

*     text=auto
*.bat text eol=crlf
*.sh  text eol=lf
*.bmp binary

この「改行コードは特に規定されておらず UTF-8 で記述されているテキストファイル」を明示的にテキストファイルと指定するか否かは好みによるところが大きいと思います。 それはチームごとに決めればよいかと思います。

autocrlf = false という運用について思うこと

概要

新しく開発チームに配属されたとき、 「まず git の設定を autocrlf = false にしてください」 というようなことを言われた人も多いかと思います。 autocrlf = false とするとこで、CRLF で扱われるべきファイルが CRLF として利用でき、 LF で扱われるべきファイルが LF として利用できるようにするためです。

そういうルールを見かけると、「あぁ .gitattributes のことを知らないんだなぁ」と思ってしまいます。 (かくいう私も .gitattributes のことを知らなくて、そういうルールを設けていたことを白状いたします。)

今となっては、チームで autocrlf = false に統一するという運用は効果が無いどころか、 むしろ悪い影響を与えるバッドプラクティスであるとさえ思っています。

なので、autocrlf = false で運用している人向けに、改行コードのことは .gitattributes で制御しなさい ...という話をします。

主なテキストファイルと改行コード

まず、各ファイル形式ごとにどのような改行コードとすべきか、いくつか例示しておきます。

ファイル形式 改行コード
.bat CRLF とすべき。
.sh LF とすべき。
.md 環境によって変えるべき。

前半の 2 つは改行コードが問題になる代表的な例です。 Windows 系のスクリプトファイルである .bat は CRLF とし、UNIX 系のスクリプトファイルである .sh は LF としないと、誤作動を起こす可能性があります。

.md は改行コードに関して特に決まりはありません。 そのようなファイルは、ファイルを利用する環境によって最適な改行コードで扱うべきでしょう。 エディタによっては、システム既定の改行コードで保存するものが多いです。 Windows 系の環境で利用する場合は CRLF、それ以外の環境では LF として扱うのが、いろいろ都合がよいかと思います。

これらのファイルを、Windows を利用する開発者、Linux (やMac) を利用する開発者のそれぞれの環境でチェックアウトしたときに、 どのような改行コードになっていてほしいかを下図に示します。

開発者B (Windows)
開発者B (Windows)
開発者A
(Linux)
開発者A...
Remote
Repository
Remote...
.sh
.sh
Checkout
Checkout
Checkout
Checkout
.bat (CRLF )
.bat (CRLF)
.sh (LF )
.sh (LF)
.md (LF )
.md (LF)
.bat (CRLF )
.bat (CRLF)
.sh (LF )
.sh (LF)
.md (CRLF )
.md (CRLF)
.md
.md
.bat
.bat
Text is not SVG - cannot display

.gitattributes での改行コードの制御

前述のように改行コードを制御するには、次のような .gitattributes をリポジトリのルートに置けばよいです。

*     text=auto
*.bat text eol=crlf
*.sh  text eol=lf

.gitattributes の詳しい書き方は別の記事にまとめましたので、ここでは述べません。 文字多めですが結構分かりやすいかと思います。頑張って読んでください。

gero-blog.hatenablog.com

autocrlf での運用はなぜダメなのか

① そもそも前述のような改行コードの制御ができない

そもそも autocrlf では、前述のような「一部のファイルは常に同じ改行コードを使い、他のファイルは環境によって改行コードを変えたい」ということを実現できません。

一部のファイルをどの環境でも同じ改行コードで使いたいからと言って autocrlf = false とすると、.md ファイルのようなファイルについては環境によって最適な改行コードで扱うことができません。 そうなると、.md ファイルを作るときは、それぞれの開発者のエディタによってばらばらの改行コードになってしまいます。 すると「ファイルを保存するときは LF (あるいは CRLF) になっているか確認すること」のような、各開発者に負担を強いるような不毛なルールが生まれてしまうのです。

逆に、環境によって最適な改行コードで扱うためには、Windows では autocrlf = true とし、Linux (Mac) では autocrlf = false または autocrlf = input とするべきですが、 そうすると今度は常に同じ改行コードで扱いたいファイルまでもが、環境によって変わってしまいます。

② 設定漏れが起こる

前述の①は、チームのメンバーが様々な環境を用いていた場合に問題になります。 チーム全員が同じ開発環境を用いるのであれば、autocrlf = false とする運用でも、理論上は問題にはなりません。 しかし、各メンバーがルールに従って運用できていれば問題ないのですが、 誰かが設定漏れを起こすと、たちまちリポジトリ内に異なる改行コードのファイルが混入してしまいます。

autocrlf の設定は、各開発者ごとに設定する必要があります。 このような各メンバー任せの運用は、設定漏れが起こります。

また「ファイルを保存するときは LF (あるいは CRLF) になっているか確認すること」のようなルールも、 各メンバーが注意しなければならないので確認漏れが起こります。

私の経験上、ほぼ必ず設定漏れは起こり、改行コードがぐちゃぐちゃのリポジトリが出来上がります。

まとめ

各メンバーが改行コードに気を配り、適切に設定を行わなければ成り立たないというような運用はナンセンスです。

そうではなく、.gitattributes を使って、リポジトリとして改行コードの統制をかけるようにしましょう。

例外発生時の各層の役割

ほとんどのソフトウェアのつくりは階層構造となっている。 最上位階層に UI が存在し、UI の操作によって下層のメソッドを呼び出し、 そのメソッドはさらに下層のメソッドを呼び出すこともある。 下図のように、呼び出したメソッドで例外が発生すると、 呼び出し階層を逆に辿るようにして例外が伝搬され、 何もしなければ最終的には UI 層に到達する。

UI
UI
UI
UI
UI
UI
例外発生
例外発生
呼び出し
呼び出し
例外送出
例外送出
Text is not SVG - cannot display

例外が発生した場合、中間層で例外を捕捉してエラーハンドリングすることもあるが、 UI 層まで伝搬させて、ユーザーにエラーダイアログとして表示することが多いだろう。

ここでは、発生した例外をエラーとしてユーザーに通知する場合の、階層の役割について説明する。

例外発生源の層

冒頭の画像の、「例外発生」と書いてある層である。

この層では、例外が発生した時点の状況について、最も情報を持っている。 例外が発生した直接的な原因や、解析に役立ちそうな情報など、 この層ではありったけの情報を例外に詰めるようにする。 例えば、設定可能な値の範囲を超えてしまったエラーであれば、設定しようとした値や許容されている上下限値など、 通信エラーであれば送信先の IP アドレスや送ろうとしたデータ、...などが解析に有用だと考えられる。

throw
throw
例外発生
例外発生
例外
Object
例外 Object
IPアドレス
ポート
IPアドレス...
パス
パス
設定しよう
とした値
設定しようとした値...
許容する
値の範囲
許容する値の範囲...
上位層へ
上位層へ
Text is not SVG - cannot display

例外に情報を詰めると言っても、例外メッセージにそれらの情報に含ませるということではない。 各種情報をテキストとして例外メッセージに埋め込んでしまうと、埋め込んだ情報を上位層で取り出すのが難しく、再利用性が下がってしまう。 埋め込む情報はそのオブジェクトのまま、例外に詰め込むようにしよう。 C# では Exception.Data というプロパティで、例外に情報を持たせることができる。

UI 層

UI 層は、下層から伝搬されてきた例外をもとに、 エラーメッセージを組み立て、ダイアログを表示する。 下位層で例外に様々な情報を詰め込まれているはずなので、 それらを取り出し、メッセージを構成する。

UI
UI
例外
Object
例外 Object
下位層から伝搬
下位層から伝搬
設定しよう
とした値
設定しようとした値...
許容する
値の範囲
許容する値の範囲...
文言の表示
文言の表示
------------
--------
---------...
Text is not SVG - cannot display

たとえば、「設定しようとした値」と「許容する値の範囲」を取り出し、 "「42」は設定できません。0~10の整数を入力してください。" というようにメッセージを組み立てることができる。

中間層

中間層では、例外発生源の層では入手できない情報を例外に詰め込み、再throwする。 中間層での情報の追加が必要なケースは、下記のような場合である。

UI
UI
UI
UI
UI
UI
例外発生
例外発生
呼び出し
呼び出し
例外送出
例外送出
呼び出し
呼び出し
例外送出
例外送出
例外
Object
例外 Object
例外
Object
例外 Object
やろうと
した操作
やろうとした操作...
やろうと
した操作
やろうとした操作...
Text is not SVG - cannot display

例外発生源のメソッドは同じだが、 そこに至るまでの呼び出しルートが複数あるというような状況が上図である。 ソースコード内で共通で使われるような処理は、しばしばユーティリティ化され、 このように複数のモジュールから呼び出されることがある。

例えば、「ディレクトリ作成」に関する処理が共通化され、複製と移動のそれぞれの処理で使われていた場合、 何らかの理由でディレクトリ作成に失敗したときに、よりユーザーフレンドリーなメッセージとするために、 下記のようにメッセージを出し分けたいことがあるかもしれない。

  • "「C:\foo\bar」にアクセス権がないため、コピーできませんでした。"
  • "「C:\foo\bar」にアクセス権がないため、移動できませんでした。"

このように、複製と移動でメッセージを出し分けたいが、共通化された「ディレクトリ作成」の層では、どちらの操作で使われているかは分からない。 スタックトレースなどを調べれば呼び出し元が分かるかもしれないが、そのような特殊な処理はするべきではないだろう。

このような場合に、やろうとしている操作が「複製」なのか「移動」なのか分かる情報を例外に詰めることで、UI層で適切なメッセージを組み立てることができるだろう。

Personal Access Token を用いる理由

GitHubリポジトリに対して git 操作をするとき、 ID/パスワード認証ではなく、Personal Access Token を用いた認証が求められます。 なぜそのような認証をしなければならないのか、初心者向けに説明します。

ID/パスワード認証

まずは古典的な本人確認手段である、 ID/パスワード認証について説明します。

GitHub にあるリポジトリを操作するとき、 第三者が不正にリポジトリを操作できないように、 本当に正しいユーザーであることを GitHub に証明しなければなりません。 そのために、Git 操作をするときに、 本人しか知りえないような文字列 (パスワード) を添付するという方法が用いられてきました。

ブラウザで GitHub にログインするとき、ID とパスワードを入力しますよね。 Git でクローンやプッシュをするときに、その ID とパスワードを使うという方法です。 パスワードはユーザーが生成し、事前に何らかの方法で GitHub に渡しておき、 GitHub は Git 操作リクエストとともに渡されてくるパスワードをチェックします。

GitHub
Repository
Repository
Git Clone
(ID:Bob, Pass:0d$a3Ve)
Git Clone...
Bob
Bob
Git Clone
(ID:Bob, Pass:abcdefg)
Git Clone...
OK
OK
NG
NG
Pass: 0d$a3Ve
Pass: 0d$a...
Text is not SVG - cannot display

Personal Access Token 認証

パスワード文字列では、認証用の文字列はユーザーが生成しました。 一方、Personal Access Token は GitHub が認証用の文字列 (トークン) を生成します。 トークンは「ghp_」から始まるランダムな文字列で、 ユーザーは事前にそのトークンを入手しておきます。 Git 操作リクエストを送る際にはそのトークンを添付します。

GitHub
Repository
Repository
Git Clone
(Token: ghp_3442af1a...)
Git Clone...
Bob
Bob
Git Clone
(Token: ghp_abcd1234...)
Git Clone...
OK
OK
NG
NG
Token:
ghp_3442af1a...
Token:...
Token:
ghp_3442af1a...
Token:...
Text is not SVG - cannot display

Personal Access Token が推奨される理由

なぜ GitHub はパスワード方式よりも Personal Access Token を推奨するのでしょう。 これまでの説明によると、認証用の文字列を、 ユーザーが生成するか GitHub が生成するかの違いしかないように見えます。

その理由が GitHub のブログに書かれています。

Token authentication requirements for Git operations - The GitHub Blog

GitHub は Personal Access Token の利点について 4 つ挙げています。

ユニーク

パスワードは様々なサービスやアプリで使い回してはいけません。 一つのパスワードが流出すると、他のサービスも不正にログインされてしまうからです。 そうとは分かっていても、使い回す人はとても多いです。

その点、Personal Access Token は GitHub 固有の文字列です。 トークンが漏れたからといって、他のサービスに被害が拡大することはありません。

ランダム

パスワードは、辞書に載っている単語や、 そういう単語の一部分を記号に置き換えたり何らかの加工を施した文字列、 何らかの規則性を持った文字列など、 人間が覚えやすいような文字列にすることが多いです。 そういうパスワードは、攻撃者にとっても解析しやすいです。

一方、Personal Access Token はランダム文字列であり、 攻撃者にとっても推測は難しいです。

限定的

パスワードが流出すると被害は甚大です。 アカウント設定を変えられて乗っ取られてしまったり、 もし管理者アカウントだった場合、リポジトリを削除されてしまうこともありえます。

一方、トークンには許可される操作を予め設定しておけるので、 トークンが流出したとしても許可している操作しかできません。 流出時の被害が抑えられます。

取り消し可能

パスワードが流出した疑いがあれば、新しいパスワードに変更します。 場合によってはアカウントを作り直します。 その変更作業はなかなか大変です。 ブラウザなど、そのアカウントを使う様々なツールにパスワードを登録しているかもしれませんが、 それらの登録内容の変更が必要です。

一方、トークンが流出した疑いがあるときは、 そのトークンだけを抹消すればよく、 アカウントの他の機能は今まで通り使い続けることができるでしょう。

まとめ

このように、ID/パスワード方式に比べ、Personal Access Token には セキュリティ面で利点があるとされます。

Personal Access Token を使うと不正アクセスの被害に遭わないというわけではなく、 被害に遭っても限定的な被害にとどめるための仕組み、だと思います。

例外メッセージをエラーメッセージに使わないようにしよう

例外にはメッセージが含まれています。 .NET では Exception.Message というプロパティで例外メッセージを取得することができます。 何か例外が起こった時、何も考えず Exception.Message をユーザーに表示していませんか。 開発者が使うツールならともかく、製品アプリケーションを開発するときにはそれはまずいです。

例外メッセージはユーザー向けではない

例外メッセージはユーザーにとっては不親切すぎます。 例えば、.NET でテキストファイルを読み込もうとしたときに表示される System.FileNotFoundException の例外メッセージは下記のようなものです。

Could not find file 'C:\foobar\file.txt'.

一方、このエラーをユーザーにダイアログで通知するとき、 表示される文言は、次のようによりユーザーフレンドリーな表現になるでしょう。

設定ファイルが読み込めないため、プログラムの起動を中止しました。
下記のファイルが存在すること、およびアクセス権があることを確認してください。
    C:\foobar\file.txt

ユーザーに表示するエラーメッセージには、原因だけでなく、 「そのエラーを解消するためにユーザーが次にとるべき行動」や 「やろうとしていた操作がそのエラーによりどうなったか」などが含まれることが多いです。

どうです、全然違うでしょう?

このように例外メッセージは、ユーザーに表示するには不親切だったり、 逆に、ユーザーには不要な細かい情報(変数内容など)が多く含まれていたりして、 ユーザー向けのメッセージとしては不向きなことが多いです。 例外メッセージは、開発者が読むログ出力などに用い、 ユーザーへのエラーメッセージは、別途文字列リソースを用意したほうが良いでしょう。

どう対応するか

Exception.Message をユーザー向けに使えないのなら、 どうやってエラーダイアログに表示する文言を取得するのでしょう。

Exception.Message にユーザー向けのメッセージを詰める案

例外インスタンスを生成するとき、引数で独自のメッセージを指定すれば、 Exception.Message にユーザー向けの文言を詰めることができます。

ですが、これはやめたほうがよいでしょう。

下位から上がってきた例外に対してメッセージを付加するには、 その例外をラップするような、新たな例外インスタンスを作成する必要があります。 メッセージを付加したいという理由だけで、わざわざ新たな例外インスタンスを作成するのはやめましょう。

try
{
    doSomething();
}
catch (FileNotFoundException e)
{
    throw new FileNotFoundException("新しいメッセージ", e);
}

例外の型に応じて、文言を決定する案

try-catch では、例外の型に応じて異なる処理をさせる仕組みがあります。 Exception.Message を使うのではなく、この分岐によって型ごとに文言を表示できます。

private void init()
{
    try
    {
        loadInitialSettings();
    }
    catch (FileNotFoundException e)
    {
        MessageBox.Show("ファイルが無いときのユーザー向けメッセージ");
    }
    catch (FormatException e)
    {
        MessageBox.Show("形式がおかしいときのユーザー向けメッセージ");
    }
    catch (~)
    {
        ~~
    }
}

ただし、これはユーザーに出し分けたいエラーの種類だけ、例外クラスが必要になります。 .NET 標準で用意されている例外だけでは、細かいメッセージの出し分けをすることはできません。 多くのカスタム例外を定義する必要があるでしょう。

「業務エラーの種類ごとに、それと対応したカスタム例外を作る」というのは、 例外とエラーが一対一に対応付くので分かりやすいといえば分かりやすいですが、 その数だけ例外を定義しなければならないので大変です。

また、メソッド呼び出し階層の下層の方では、まだどういう業務エラーにするべきか決まらない場合も多く、 結局、上位層で「例外を catch して別の例外に包んで再 throw」することが、 結構頻繁に起こるんじゃないかと予想しています。

「業務エラーの種類ごとに、それと対応したカスタム例外を作る」というようなプロジェクトに 私は参画したことが無いので、実際の良し悪しは分かりませんが、 Web を眺めているとそれはアンチパターンだという意見が多勢のように思います。

例外にエラーコードを持たせて、文言を決定する案

製品ソフトでエラーが起きると、エラーコードが付記されていることが多いです。 エラーコードを見ればエラー箇所や原因が切り分けでき、トラブルシュートできるようになっています。 エラーコードごとに表示する文言が決まっていれば、それに応じた文言を出すだけです。

.NET の例外には、追加で情報を格納できる Exception.Data プロパティがあります。 このプロパティは辞書型になっており、次のように追加の情報をセットできます。

try
{
    doSomething(filePath);
}
catch (FileNotFoundException e)
{
    e.Data["ErrorCode"] = 1234;
    e.Data["FilePath"] = filePath;
    throw;
}

これを上位層で catch したら、e.Data["ErrorCode"] の値を見て、 どのような文言を表示するのかを決定します。

エラーコードから文言を決定する判定ロジックは、 プログラムのあらゆる個所でばらばらに実装するのではなく、 クラスとして一カ所にまとめておくのが良いでしょう。

feature ブランチをマージする前のテスト

概要

git を使った複数人でのソフト開発時、 個別機能の開発を feature ブランチ (トピックブランチとも呼ばれる) で行い、 開発が終わると本流にマージされる、というブランチフローを採用することがほとんどだと思います。 そして、マージされる前には必ずテストを行います。 テストは手動・自動のどちらの場合もありますが、これに合格しないとマージできません。

このテストを行う場合は、マージ先ブランチの最新のコードを feature ブランチに取り込んでから動作確認しましょう。 その理由と方法について本記事で説明します。

なお、マージ先ブランチはブランチフローによって異なります。 feature ブランチは、git-flow であれば develop にマージしますし、GitHub Flow であれば main にマージしますね。 ここではよりシンプルにするために、main にマージするとして説明しますが、 マージ先ブランチ名が違うだけで git-flow の develop ブランチでも同じことが言えますので、適宜読み替えてください。

なぜマージした後のコードをテストするのか

main から 2 つのブランチが派生し、それぞれに下図のような変更がコミットされたとします。

int main()
{
sayHello("Geroshabu");
}

void sayHello(string name)
{
Console.WriteLine($"Hello {name}!");
}
int main()...
featureB
featureB
featureA
featureA
int main()
{
sayHello("Geroshabu");
}

void sayHello(string name)
{
if (name.Length > 10)
{
throw new ArgumentOutOfRangeException();
}
Console.WriteLine($"Hello {name}!");
}
int main()...
int main()
{
sayHello("Freddie Mercury");
sayHello("Geroshabu");
}

void sayHello(string name)
{
Console.WriteLine($"Hello {name}!");
}
int main()...
main
main
Text is not SVG - cannot display

featureA では名前の長さのチェックが入りました。 featureA の担当者は、現状のコードで問題が無いこと (長さチェックに引っかからないこと) を確認済みです。

一方、featureB では新たな名前の出力をしようとしています。 featureB の担当者は、新たな名前がコンソールに出力されることを確認済みです。

お互いに異なる行を変更しているので競合は発生しませんし、それぞれテスト済みなので、この 2 本のブランチはそのまま main にマージされました。

int main()
{
sayHello("Geroshabu");
}

void sayHello(string name)
{
Console.WriteLine($"Hello {name}!");
}
int main()...
1
1
featureB
featureB
2
2
3
3
featureA
featureA
int main()
{
sayHello("Geroshabu");
}

void sayHello(string name)
{
if (name.Length > 10)
{
throw new ArgumentOutOfRangeException();
}
Console.WriteLine($"Hello {name}!");
}
int main()...
int main()
{
sayHello("Freddie Mercury");
sayHello("Geroshabu");
}

void sayHello(string name)
{
if (name.Length > 10)
{
throw new ArgumentOutOfRangeException();
}
Console.WriteLine($"Hello {name}!");
}
int main()...
main
main
4
4
5
5
Text is not SVG - cannot display

ですが、改めて main を動作確認してみるとどうでしょう。 ArgumentOutOfRangeException が発生し、プログラムが強制終了してしまいます。 それぞれの担当者がきちんとテストしたのに、main ブランチにバグを混入させてしまいました。

それはなぜでしょう。 この場合、featureA をマージしてから featureB をマージしましたが、 後にマージした方に問題があります。

featureB の担当者が動作確認したのは、コミット「1」の状態から、sayHello("Freddie Mercury"); を追加したコードです。 しかし、動作確認するべきなのは、コミット「4」の状態から sayHello("Freddie Mercury"); を追加したコードだったのです。 すなわち、main と featureB がマージされた後のコミット「5」のコードを、動作保証すべきなのです。

マージ後のコードを、マージ前にテストする

前節では、コミット「5」のコードを動作保証すべきと書きましたが、 まだマージされていないのに、どうやってマージ後のコードを入手するのでしょうか。

それには、「最新の main の内容を featureB に取り込む」ということを行います。 具体的には、main ブランチを featureB ブランチにマージします。 そうすると、下図のコミット「5'」のように、main 側で施された変更が featureB に取り込まれます。

1
1
featureB
featureB
2
2
3
3
featureA
featureA
int main()
{
sayHello("Geroshabu");
}

void sayHello(string name)
{
if (name.Length > 10)
{
throw new ArgumentOutOfRangeException();
}
Console.WriteLine($"Hello {name}!");
}
int main()...
int main()
{
sayHello("Freddie Mercury");
sayHello("Geroshabu");
}

void sayHello(string name)
{
if (name.Length > 10)
{
throw new ArgumentOutOfRangeException();
}
Console.WriteLine($"Hello {name}!");
}
int main()...
main
main
4
4
5'
5'
int main()
{
sayHello("Freddie Mercury");
sayHello("Geroshabu");
}

void sayHello(string name)
{
Console.WriteLine($"Hello {name}!");
}
int main()...
5
5
int main()
{
sayHello("Freddie Mercury");
sayHello("Geroshabu");
}

void sayHello(string name)
{
if (name.Length > 10)
{
throw new ArgumentOutOfRangeException();
}
Console.WriteLine($"Hello {name}!");
}
int main()...
取り込む
取り込む
Text is not SVG - cannot display

このコミット「5'」のコードは、マージ後のコミット「5」のコードと同じです。 なのでコミット「5'」のコードを動作確認することにより、main にマージする前に、今回のバグを検出できたでしょう。

main を取り込んだらすぐ動作確認、マージしよう

さて、これまでの説明でお分かりの通り、feature ブランチを main にマージするときには、 その時点の最新の main を feature に取り込み、テストしなければいけません。 すなわち、最後に main を feature に取り込んでから、新たに main に更新が入ると、 次図のようにもう一度取り込みなおしてテストをする必要があります。

1
1
featureX
featureX
2
2
3
3
main
main
4
4
5
5
7
7
取り込む
取り込む
6
6
8
8
取り込む
取り込む
Text is not SVG - cannot display

テストを手動でやる場合、これはかなり面倒です。 そうならないよう、main を feature に取り込んだ後、速やかに動作確認を行い、main にマージするようにしましょう。

また次のような、なるべくテストを繰り返し行う手間を軽減する工夫もするとよいでしょう。

  • 手動テストを自動化する (これができれば理想ですね)
  • 「main に入った変更が、全然違うモジュールの変更であるなど、明らかに自分の変更箇所とは無関係」な場合は、レビューリーダーの判断の下、再度の動作確認を免除する
  • main を feature に取り込みテストする場合は、「これからテストをするからマージを控えてほしい」旨をチームメンバーに周知する

などですね。 この辺は各チームごとのルールになるので詳しくは説明しません。

git ブランチ運用の根底にある考え方

ソフトウェアの開発においては、何らかのルールに従って git を運用していきます。 多くのチームが git-flow や GitHub Flow といった有名な運用方法を採用しているかと思います。 ただ、そういう運用方法の表面的な流れとかを解説した記事は多いものの、 それらの運用をするとなぜ良いのか、初心者向けに解説した記事って無いなぁと思ったので作りました。 「git を使ったことはあるけどブランチは常に 1 本でやってきた」ぐらいの人を対象としています。

レベル1: バージョン管理ツールとしての git

git を最初に学んだときは、バージョン管理ツールだと教わるでしょう。 古来より「source_20240625.zip」のようにソースコード一式を zip アーカイブし、その名前でバージョン管理してきましたが、注意深く運用しないとすぐ破綻し、管理に大変な労力が必要でした。 そこで登場した svn や git のようなバージョン管理ツールは、そのような管理方法を置き換えるものとして取り入れられました。 ということで最もシンプルに、ソースコードの成長を一本道で記録していくことも多かったでしょう。

main20240624版20240625版20240701版20240710_Release_1.0.0

バージョン管理ツールを使えば、コード全体を簡単に以前のバージョンに戻せますし、以前の状態と差分を比較するのだって簡単です。

ただ、手動管理の煩わしさが軽減されたのも束の間、次の要求が出てきます。

レベル2: ひとつの開発を行うためのブランチ

バージョン管理が容易になったことで、バージョンを記録する単位も細かくなってきました。 一つの機能を開発するとき、少しずつ変更をコミットして、一つの機能を作り上げることが普通になっていきました。 実装の途中で、既存コードの構造を変更したり、実験的なコードを入れたりすることで、場合によっては既存のソフトウェアがうまく動かなくなることもあります。 その場合、並行で作業している他の開発者が困ります。

また、障害が発生したときの原因の切り分けと対策も大変です。 一本道のブランチに複数の機能のためのコミットが混在していると、ある機能だけ取り除きたいというようなケースに対応するのが大変です。

そこでいつしか、「機能ごとにブランチを分ける」運用になっていきました。 新たに機能の開発に着手するときは、本流である main からブランチを分岐させ、分岐したブランチにコミットしていきます。 一連のコミットを行い、そのブランチで実装が終わったなら、本流である main ブランチにマージさせます。 なお、この機能ごとのブランチを「featureブランチ」とか「トピックブランチ」と呼ばれることが多いです (以降、feature ブランチと記載する)。

mainfeatureAfeatureBInitial Commit機能A用の変更1機能B用の変更1機能B追加機能A用の変更2機能Aの追加

こうすることで、それぞれの機能の実装途中のコードが、main ブランチに入ることはなくなります。 機能の実装をし終えてから、初めて製品にその機能が追加されるのです。

また、ある機能だけを取り除きたいとなった場合でも、マージコミットだけを打ち消せばよいので楽です。

当然、main ブランチへ直接コミットすることは禁止です。 main ブランチには実装途中のコードを入れたくありませんからね。 必ず feature ブランチを作成し、main ブランチにその feature ブランチをマージさせることで、機能を追加していくことになります。

おそらく、ほとんどの git のブランチ運用は、この「機能ごとのブランチを作成し、製品ブランチにマージする」という運用になっているかと思います。 ここまでのことを知っていると、ブランチ運用の理解に役立つと思います。

レベル3: コード品質をどう保つか

さて、feature ブランチで一通り実装を終えた時点のコードは品質が高いでしょうか。 いいえ、その時点のコードはいち実装者が動作確認をした程度であり、多くのバグが含まれ、品質が高いとは言えないでしょう。 main ブランチで、コード品質をどう保っていくかという戦略について、2 つ紹介します。

レベル3-1: main にマージする前にワンクッション置く (develop ブランチ)

ここまで、feature を main にマージしていくとしていましたが、そうして出来上がる main ブランチは、各実装者が動作確認をした程度の機能の寄せ集めであり、バグが含まれているかもしれません。 なので、そのあとにレビューとか、しっかり評価をしてバグを取り除かなければ、製品として世に出すことはできません。

というわけで、ここでブランチの役割を再定義します。 main ブランチとは別に、develop ブランチが登場します。 ブランチの役割は下記となります。

  • main ブランチ: 製品コード。しっかり評価済みのコード。
  • develop ブランチ: まだバグが含まれているかもしれないコード。

feature をマージするのは、develop ブランチになります。 そうして出来上がった develop ブランチをリリース評価にかけ、評価に合格したものを main ブランチにマージします。 feature をいきなり main にマージするのではなく、develop というワンクッションを挟む感じですね。

リリースに必要な機能の開発が develop ブランチに揃うと、リリースを始めます。 基本的に評価は 1 回目で終わることはなく、バグ修正をしては 2 回目、3 回目と繰り返していきます。 特に組み込みソフトはハードと絡むことが多く、評価期間は長くなりがちです。 その間、次のリリースに向けて機能を入れたくても、評価中のソフトに関係ない機能を追加するわけにはいきませんので、各実装者は暇になってしまいます。

そこで、評価期間中でも次期リリース向けの機能を develop に追加できるよう、評価の実施は release ブランチというブランチを作成して行われることが多いです。

ここまでを下図にまとめました。

mainreleasedevelopfeatureAfeatureBfeatureCInitial Commit機能A用の変更機能Aの追加機能B用の変更機能Bの追加障害対応A次リリース向けの機能次リリースの機能の追加障害対応Bv1.0.0

これが、あの有名な git-flow (日本語訳) での品質担保の戦略です。 release ブランチで行った障害修正は develop にも書き戻すなどの細かい説明は省略しますが、「feature をまず develop にマージし、リリース評価に合格したものを main (製品) にマージする」ことで製品の品質を保とうとしています。

レベル3-2: feature マージ前に品質を高める

もう一つの戦略として、GitHub Flow やトランクベース開発などにみられる「feature を main にマージする前に、feature ブランチの品質を製品レベルまで高める」戦略があります。 feature を main にマージする前に、レビューや評価を実施し、合格したものだけを main にマージすることで、製品の品質を担保します。

ここで登場するのが「プルリクエス (マージリクエストとも言う)」という仕組みです。 プルリクエストは、あるブランチで行った一連の変更を、もう一方のブランチにマージしてよいかどうか、マージ前にレビューできる仕組みです。

実装者は feature ブランチに一連のコミットをし終わると、プルリクエストを作成し、チームメンバーにレビューを求めます。 このレビューで承認をもらえないと、main ブランチにマージすることは許されません。 レビューで差し戻しを食らった場合は、引き続きその feature ブランチで追加のコミットを行い、承認がもらえるまで品質を高め続けましょう。

評価も各 feature ブランチをマージする前に行います。 feature ごとにリリース評価をすることになりますので、手動ではやっていられません。 この戦略を採用する場合は、しっかりとした自動テストは必須となるでしょう。

補足: 開発しているものとの相性

「feature ブランチで品質を高める」ほうが「develop ブランチに機能を追加し release ブランチで評価を行い品質を高める」よりも軽量です。

品質はそこそこでも開発速度が重要視され、リリース頻度も高く、自動テストも作りやすいWebアプリのような製品は、軽量な「feature ブランチで品質を高める」方が相性がいいと思います。

一方、品質が重要視され、ハードと組み合わせての評価も必要な組み込みアプリのような製品は、より重厚な「develop ブランチに機能を追加し release ブランチで評価を行い品質を高める」方でガッツリ評価したりします。

また、両者のハイブリッドのようなフローも採用されます。 develop ブランチにはバグが含まれているかもしれないと言っても、feature ブランチをそのまま develop ブランチにマージしてしまうのは、さすがに develop ブランチの品質が悪くなりすぎます。 すると、その分 release ブランチでのリリース前評価と障害対応が大変になってしまいます。 なので、develop にマージする前にプルリクエストを作り、レビューや評価を行い、ある程度の品質まで上げてから develop に入れるようにしているチームも多いんじゃないでしょうか。

おわりに

チームで定められたブランチルールになんとなく従っているだけの人は、なぜそのルールになっているのか、そのルールの狙いを考えてみるのもいいかもしれません。 そのきっかけになれば幸いです。