LoginSignup
1
2

More than 5 years have passed since last update.

週刊 git GUIクライアントを作る [3] stage/unstageパッチ編集編

Last updated at Posted at 2017-10-15

前回までのまとめ

週刊になってないので、おさらいします。

[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を使おうというお話でした。

至高のライブラリ探索の旅に出るという道もありますが、
JGitに限らず、libgit2へのbindingその他のライブラリでも、
機能が足りないという問題は出てくる可能性があります。
そこで、次号からは、最高の機能を誇るgitアプリケーションであるgit.exeを使います。

今回は

3. hunkのボディを編集する
4. hunkのヘッダーを再計算する

上記について、git-guiのコードをJavaに移植したコード辺をお披露目して、
そのままだと残ってしまう課題についての対応策を扱います。

確認環境

tool version
OS Windows 10 Home
git.exe 2.14.1.windows.1
git-gui 0.21
SourceTree 2.3.1.0 Windows版 2.3.1.0

hunkの編集

git-guiを読む

gitにはGUIアプリケーションとしてgit-guiが同梱されています。
このgit-guiでは、hunkの行を選択するとコンテクストメニューから「Stage Lines For Commit」ができます。
これはまさしく我々がやりたいことなので、ここのコードを読んでみましょう。
diff.tclというファイルのapply_range_or_lineという関数(?)です。
Tclの基本文法とTkのテキストウィジェットをチラ見すれば読めます。

4J

テキストウィジェットの機能にかなり依存しているため、
慣れていない人間に読みづらいのはともかく、他言語や他環境への移植も面倒です。
というわけで、こちらにJavaへの翻案を用意しました。
(これぐらい自分でイチから書けよ、と言われそうな気もしますが)

diff.tclとの相違点は、以下の通りです。

  • 複数のhunkを扱わず、1つのhunkのみ扱う
  • コマンドまでは作成せず、パッチのみ作成

前提の説明も兼ねてメソッドの引数だけ説明すると、

    /**
     * @param diffHeader diff全体のヘッダー
     * @param hunkHeader hunkのヘッダー
     * @param hunkLines  hunkのボディ
     * @param range      選択範囲
     * @param unstage    unstageかどうか
     * @return 選択範囲を反映したパッチ文字列
     */
    static String edit(final String diffHeader,
                       final String hunkHeader,
                       final List<String> hunkLines,
                       final Range range,
                       final boolean unstage) {

final String diffHeader,
final String hunkHeader,
final List hunkLines,

最初の3つは、git diffで出力されたパッチを分解して抽出したものです。
hunkが1つの場合は、単に分解するだけです。
1. 元になるpatchとしてdiffを出力するの結果ですね。

final Range range

選択範囲は、両端ともにinclusiveです。
2. stage/unstageしたい行を選ぶというGUIでの操作に合わせました。

final boolean unstage

stageするかどうかではなく、unstageかどうかを表すbooleanで扱っているのは、
自然にそうなってしまったんですが、あまり良くない感じはします。

課題

さて、hunkの編集は基本的にはgit-guiの真似をするだけなんですが、
それだとうまくいかない場合があります。

なお、先ほど紹介したgistのコードには、こうした課題の解決方法を含めていません。
git-guiを参考にした箇所と、独自の対応を切り離しておきたいからです。

末尾に改行が存在しないファイル

詳細

失敗する場合と、一見おかしいが正しい挙動の2パターンがあります。
ここでは失敗する場合のみ扱います。

変更前あるいは変更後のファイル末尾に改行が存在しない場合、
diffおよびパッチではファイル末尾行の次の行に\ No newline at end of fileが挿入されます。

末尾がいろいろあるので混乱しないように注意しましょう。

  • 変更前のファイルの末尾
  • 変更後のファイルの末尾
  • パッチの末尾

例えばgit diff --cached test.txtで上記のdiffつまりパッチが出力される場合に、

diff --git a/test.txt b/test.txt
index 85954ea..b3acb2e 100644
--- a/test.txt
+++ b/test.txt
@@ -1,5 +1,5 @@
-1
+one
 2
 3
 4
-5
\ No newline at end of file
+five
\ No newline at end of file

以下の部分だけunstageしたいとします。

-1
+one

git reset -p を経由して編集する場合に出るquick guideを参考にして、

# To remove '+' lines, make them ' ' lines (context).
# To remove '-' lines, delete them.

何も考えずに編集すると以下のようになりますが、

@@ -1,5 +1,5 @@
-1
+one
 2
 3
 4
\ No newline at end of file
 five
\ No newline at end of file

パッチにファイル末尾の行が含まれない場合、\ No newline at end of fileは意味を成さないので削除するのが正解です。

@@ -1,5 +1,5 @@
-1
+one
 2
 3
 4
 five
\ No newline at end of file

他のGUIクライアントでは

git-guiでは、上記のようなunstageの場合、
つまり、パッチの途中に不要な\ No newline at end of file が存在する場合は失敗しますが、
stageの場合、つまり、パッチの末尾\ No newline at end of file が存在する場合は成功します。

SourceTreeでは、stageもunstageも成功します。

どうやって解決するか

\ No newline at end of fileの直前にある連続する差分行の塊を、パッチから全て削除する場合、
無意味になってしまう\ No newline at end of fileも削除します。

stageの場合を例にとります。

まず基本を確認しておくと、
stageしたい範囲に含まれる行は、元となるパッチの表現をそのまま使います。
stageしたい範囲に含まれない行、つまり、stageしない行のうち、「+から始まる行」は、
パッチのapply先であるindexに存在しないため、行ごとパッチから削除します。

\ No newline at end of fileの直前に「+から始まる行」が1行だけある場合に、
その行をstageしないのであれば、その行だけでなく\ No newline at end of fileも削除します。

\ No newline at end of fileの直前に「+から始まる行」が2行以上続いている場合、
その連続する行(以下、「塊」と呼ぶ)の全体がstageしたい範囲に含まれないのであれば、
\ No newline at end of fileも削除します。
その塊の一部がstageしたい範囲に含まれる場合、\ No newline at end of fileは有意味なままなので残します。

新規ファイルの部分的なstage

詳細

hunk編集という意味では何の問題もありませんが、
他のGUIクライアントが対応していないため、ここで触れておきます。

他のGUIクライアントでは

git-guiもSourceTreeも対応していません。
どちらも表示内容に\ No newline at end of fileが含まれないことから
そもそもdiffではなくファイル内容をそのまま表示しているんだろうと思われます。

どうやって解決するか

前号で書いたコマンドでdiffを取得します。

gitコマンドでuntracked filesのdiffを出すのは少し面倒です。

git ls-files -o --exclude-standard

で一覧を取得して、次のコマンドでdiffを取得できます。

git diff --no-index /dev/null [untracked file name]

あとは、通常のdiffと同じです。

ところで、\ No newline at end of fileの扱いをどうするか、という悩みがあります。
例えば以下のdiffに相当する新規ファイルがあるとします。

diff --git a/end_without_linebreak/test.txt b/end_without_linebreak/test.txt
new file mode 100644
index 0000000..15c491c
--- /dev/null
+++ b/end_without_linebreak/test.txt
@@ -0,0 +1,5 @@
+first
+second
+third
+forth
+fifth
\ No newline at end of file

このうち、次の2行だけstageしたい場合、

+first
+second

上述の\ No newline at end of fileの扱いを考慮しつつhunkを編集すると、以下のようになります。

@@ -0,0 +1,2 @@
+first
+second
\ No newline at end of file

上記hunkを含んだパッチを適用した結果、worktreeには以下のdiffが生まれます。

diff --git a/end_without_linebreak/test.txt b/end_without_linebreak/test.txt
index 0bfc124..15c491c 100644
--- a/end_without_linebreak/test.txt
+++ b/end_without_linebreak/test.txt
@@ -1,2 +1,5 @@
 first
-second
\ No newline at end of file
+second
+third
+forth
+fifth
\ No newline at end of file

しかし、以下のようにhunkを編集してパッチを作成・適用して、

@@ -0,0 +1,2 @@
+first
+second

worktreeが以下の状態となるほうが、すっきりします。

diff --git a/end_without_linebreak/test.txt b/end_without_linebreak/test.txt
index 66a52ee..15c491c 100644
--- a/end_without_linebreak/test.txt
+++ b/end_without_linebreak/test.txt
@@ -1,2 +1,5 @@
 first
 second
+third
+forth
+fifth
\ No newline at end of file

この挙動だと、ファイル末尾に改行コードが存在しないことが求められる場合には困ります
そういう状況自体が不幸なんじゃないかという気がしますが、大人しく諦めましょう。

いずれにせよ、より面倒な「新規ファイルの部分的なunstage」と挙動を合わせたほうがいいかと思います。

新規ファイルの部分的なunstage

詳細

これはgitのcliでもできません。
error: new file [ファイル名] depends on old contentsというエラーが出ます
おそらく、/dev/null に対して差分となるパッチを適用する、という形になるからだと思います。

他のGUIクライアントでは

git-guiだと、何もメッセージを出さずに失敗します。

SourceTree の場合、Windows版のSourceTree 2.3.1.0で確認した限りでは、
新規ファイルのunstageはファイル単位のみで、
行単位で部分的にunstageする機能は無いようです。

これはgitに無い機能と言えそうなので「対応しない」というのも十分にアリかと思います。

どうやって解決するか

一度ファイル全体をunstage(git reset [ファイル名])してから、
選択範囲以外をstageすることで対応できます。

①hunkを全てunstageする
②選択行のを全てstageする
③選択行のを全てstageする

または

①hunkを全てunstageする
②選択行のを全てstageする
③選択行のを全てstageする

のどちらかになりますが、
いずれにせよ、いろんな面倒があるので、詳しい解説はしません。

進捗

というわけで、やってるぞ感だけ見せておきます。
左がworktreeおよびuntracked fileのdiffで
右がindexのdiffです。

進捗.gif

あとがき

次回予告

次はおそらく、
1. 元になるpatchとしてdiffを出力する
の話を書きます。

その次ぐらいに予定している、
2. stage/unstageしたい行を選ぶ
の話は主に、SwingのJListでのマウスドラッグによる複数選択について、になると思います。
そこそこ苦労しました。知らないコンポーネントで簡単にできたりしたらどうしよう。

「パッチ行をコミット予定に加える」

git-guiのレポジトリには
https://github.com/git/git/tree/master/git-gui/po に各言語ごとのメッセージが定義されていて、
日本語だとStage Lines For Commitは「パッチ行をコミット予定に加える」のようですが、
git-guiの言語切替の方法を私は把握しておらず、未確認です。

Git for Windows付属のGit GUIを日本語化する方法によると、

Ver.2.x.xのバイナリには、多言語表示用のメッセージ定義ファイルが同梱されていない

だそうです。
自分では使わないので調べてませんが、この状況が続いているとするともったいないですね。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2