週刊とは何だったのか
前回までのまとめ
[1] stage/unstage基礎知識編
行単位のstage/unstageのためにやるべきこと5つ
1. 元になるpatchとしてdiffを出力する
2. stage/unstageしたい行を選ぶ
3. hunkのボディを編集する
4. hunkのヘッダーを再計算する
5. 最終的なpatchをapplyする
[2] stage/unstage不完全攻略編
JGitを使ってみたら無理だったので、git.exeを使おうというお話でした。
[3] stage/unstageパッチ編集編
3. hunkのボディを編集する
4. hunkのヘッダーを再計算する
今回は
1. 元になるpatchとしてdiffを出力する
diffだけじゃ足りない
diffを出すのは簡単です。
git diff -p
git diff -p --cached
これだと新規追加したばかりのuntrackedなファイルは含まれません。
なので少しだけ手間をかけます
git ls-files -o --exclude-standard
で一覧を取得して、次のコマンドでdiffを取得できます。
git diff --no-index /dev/null [untracked file name]
Windowsでも /dev/null で比較してくれるので安心してください。
差異を見るだけなら新規ファイルなのでファイルそのものを見ればいいんですが、
今の文脈で欲しいのはapplyできるpatch形式のdiffですので、ここで紹介しました。
diffをパースしてファイル名を取得(しない)
しかし、diffそのものだけでなく、いろんな場面でファイル名が必要になってきます。
コマンドで取得
git diff --name-status
git diff --name-status --cached
ステータス(新規追加、変更、リネーム、削除)も分かると便利なので
--name-only
オプションより--name-status
がいいでしょう。
ファイル名はdiffそのものに含まれていますが、困難な場合があります。
[M] 変更
diffの先頭はこんな感じになっています
diff --git a/target.txt b/target.txt
index 0b669b6..5c30060 100644
--- a/target.txt
+++ b/target.txt
これならdiffからでも簡単に分かりそうですね。
[A] 新規追加
新規追加の場合はこんな感じです。
diff --git a/end_with_linebreak/with space.txt b/end_with_linebreak/with space.txt
new file mode 100644
index 0000000..093dc04
--- /dev/null
+++ b/end_with_linebreak/with space.txt
この例にあるように、ファイル名にスペースが入ると、1行目から取得するのはちょっと面倒です。
なので、+++ b/
の行にあるファイル名を使うと楽です。
[R] リネーム
リネームだとこんな感じで、さらに情報が増えてるので楽勝です。
diff --git a/names.txt b/names_.txt
similarity index 96%
rename from names.txt
rename to names_.txt
index bcfb685..f915764 100644
--- a/names.txt
+++ b/names_.txt
なお、リネームは基本的にindexにあるファイルのdiffであらわれます。
indexに上がっていないworktree上のファイル、つまりgit diff
コマンドでは
- 削除(リネーム前のファイル名)
- 新規追加(リネーム後のファイル名)
という形で現れます。
2つのファイル名を指定して比較すればリネーム扱いのdiffを取れるかもしれませんが、
その2つのファイルをどうやって見出すのか私にはわかりません。
git diff --cached
の場合は、gitが見つけてくれます。
ちなみにバイナリだとこんな感じです。
rename from
rename to
が固定ではなく言語環境に依存してるとアレですが、
特に仕様の確認はしていません。
diff --git a/src/main/resources/icon.png b/src/main/resources/icon_white.png
similarity index 100%
rename from src/main/resources/icon.png
rename to src/main/resources/icon_white.png
[D] 削除
削除の場合はこんな感じです。
ついにファイル名が1行目だけになりました。
diff --git a/src/main/java/NewView.java b/src/main/java/NewView.java
deleted file mode 100644
index e69de29..0000000
要するに
-
+++ b/
があれば+++ b/
から取るのが簡単 -
+++ b/
が無くてrename from
rename to
があればそこから取る - 削除の場合は、どれも無いので
diff --git
の行から取得するしかないが、ファイルおよびディレクトリ名にスペースがある場合に備えてちょっとだけ頭をひねる必要がある
というわけなんですが、頭をひねりたくないので、おとなしくファイル名を取得します。
行単位ではないstage/unstage
作りたいのは行単位でstage/unstageできるアプリケーションです。
ですが、全てを行単位でやってると大変です。
hunk単位のstage/unstage
hunk単位であれば、diffのhunkを抜き出してgit apply
します。
これは行単位でstage/unstageする場合の
3. hunkのボディを編集する
4. hunkのヘッダーを再計算する
が省略されただけなので、特に問題はないはずです。
GUIとしても、初めからその機能を用意してました。
このgifだと、
hunkのヘッダー部分(黒い背景のところ)をクリックすると、
- 選択中の行が有ればその行を
- 選択中の行が無ければhunk全体を
それぞれstage/unstageしています。
ファイル単位でのstage/unstage
このアプリケーション自体のgitレポジトリでドッグフーディングしながら開発してると、
なかなか気づかなかったんですが、
新規プロジェクトだと、最初はどうしてもそれなりのファイル数になりますし、
新規追加ファイルは丸ごとstageしたいことがほとんどです。
ファイルごとに、あるいはファイルまとめて、stageしたくなります。
というわけで、さっき取得したファイル名を使います。
なんの変哲も無いコマンドですが、
git add -- [file paths]
git reset -- [file paths]
ファイル名には空白が含まれる場合がありますので、
常に1つ1つのファイル名をダブルクォーテーション""
で挟みます。
余談
gitの操作には、いくつかのフェーズがあります。
- stage/unstageからコミットまで
- コミット履歴の閲覧、操作
- ブランチの操作
gitのすべての操作を一つのアプリケーションで担うべきだとは私は(あんまり)考えません。
また、アプリケーション(あるいはその一部)がどういう単位で機能を切り出すべきか、
というのは、それなり(そして、一般に)難しい問題だと思います。
とはいえ、
行単位ではないものを含めてstage/unstageは1つのアプリケーションで行えるべきでしょう。
画像
いい感じのdiffを出せるといいですが、それは大変そうなので、
せめて単純に画像を表示したり、並べたりぐらいはしたいですね。
JavaだとImageIO.readが簡単そうでしたので、私はこれを使っています。
InputStreamかFileを渡すとBufferedImageを得られます。
存在しないファイル
現在ファイルとして存在してる場合は、単純にそのファイルのFileで問題ありません。
しかし、変更した場合や、画像を削除した場合、
変更前や削除前の画像はファイルとしては存在しません。
gitが変更あるいは削除を検知しているということは、
gitオブジェクトとしては存在するということなので、ハッシュが分かれば取得できます。
そしてハッシュはdiffの中に含まれているので、それを使います。
git cat-file -p [hash]
このコマンドはgitオブジェクト(圧縮済み)をブラウズするためのもので、
-p
オプションの場合は、ファイルの内容(展開済み)がそのまま得られます。
私はgitコマンドをこんな感じでJavaから叩いていますが、
このprocess.getInputStream()で得られるInputStremeをImageIO.readに渡すだけでした。
final Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec(command, null, workingDir.toFile());
similarity index 100%
ところが、単なるリネームの場合、なぜかdiffにハッシュが含まれません。
以下は、先述の例と同じdiffです。
diff --git a/src/main/resources/icon.png b/src/main/resources/icon_white.png
similarity index 100%
rename from src/main/resources/icon.png
rename to src/main/resources/icon_white.png
そこで、以下のコマンドでファイル名からハッシュを取得してから画像を取得します。
git ls-files -s [file path]
リネーム後もリネーム前もgitオブジェクトとして存在しているため、
どちらの名前を使っても問題無いはずです。
(なぜか私はリネーム後の名前を使っていました。)
メモリキャッシュ
例えばstage/unstageの前後で画像は変化しないので、キャッシュしておきたいですね。
何を基準にキャッシュするかというと、もちろんハッシュです。
リネームの場合を除くと、必ずdiffからハッシュを取得できるはずです。
ですが、混乱していたのか、何かの名残か、何も考えてなかったのか、
手元のコードでは、新規追加ファイルの場合にハッシュを取得しています。
参考までに、コマンドを紹介しておきます。
git hash-object [file path]
開発中のアプリケーションでは、
「選択中のファイルのdiffのみ表示する」という仕様にしています。
なので、キャッシュを使わないと、ファイル選択のたびに画像が読み込まれ作成されます。
手元の環境では、キャッシュを外すと、
外部コマンド待ちのときに出してる砂時計が一瞬見えるぐらいの違いです。
マシンの性能や画像の数によっては、キャッシュしないとつらいかなー、という印象です。
進捗
画面は開発中のものです。
実際の仕様とは異なる場合があります。
あとがき
次回
たぶん 2. stage/unstageしたい行を選ぶ
になると思います。
いろいろ忘れかけてるので、早めに書きたいです。
Swing
ところでSwingで書いてます。
Diffの表示と選択の部分は、最初はJListを使ってましたが、
JListはセル単位での更新ができないようなので、JTableに変えました。
画像の読み込みが終わったら画像のセルだけ表示と高さを変えたかったんです。
JListでやっていた表示とイベント処理をJTableで全く同じようにするのは、ちょっと手間でしたが、
幸いにも、JListでの画像表示に伴う表示のちらつきみたいな乱れを、JTable版では消せました。
(もったいなさと不安で、JList版にもフラグ1つで切り替えられるようにしていて、
さっきあらためて動作確認しました。)
RxJava/RxSwing
あと、僕はRxマンなので、RxJavaも使っています。
RxJava2.0対応のRxSwingが無かったので、Schedulerだけ自分で作りました。
SwingのUIスレッドであるEDT上で実行するSchedulerです。
RxJava1.0用のRxSwingと、RxAndroidを参考にしました。