Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 13338] ファイルの概要

このコミットは、EmacsのGoモード(go-mode.el)におけるgofmtのパッチ適用処理に関するバグ修正です。具体的には、gofmtが生成する差分(diff)のヘッダーに含まれる一時ファイルパスの処理が原因で、/tmpディレクトリ内に存在するGoファイルに対してgofmtを適用する際に発生していた問題を解決します。

コミット

commit 44a3a58e451bcabad67fbd31e203a4f9f1ba2eae
Author: Jean-Marc Eurin <jmeurin@google.com>
Date:   Wed Jun 13 10:25:00 2012 -0400

    misc/emacs: Fix a failure when /tmp/<file>.go exists.
    
    R=sameer
    CC=golang-dev
    https://golang.org/cl/6296060

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/44a3a58e451bcabad67fbd31e203a4f9f1ba2eae

元コミット内容

misc/emacs: Fix a failure when /tmp/<file>.go exists.

このコミットメッセージは、/tmpディレクトリに存在するGoファイルに対してgofmtを適用する際に発生する不具合を修正することを明確に示しています。

変更の背景

Go言語の公式ツールであるgofmtは、Goソースコードを標準的なスタイルに自動整形するツールです。Emacsのgo-mode.elは、このgofmtをEmacsエディタ内で利用するための機能を提供しています。通常、gofmtはファイルの内容を標準入力から受け取り、整形された内容を標準出力に出力するか、-dオプションを付けて差分(diff)を出力します。

go-mode.elは、ユーザーがgofmtを実行した際に、現在のバッファの内容をgofmtに渡し、gofmt -dが生成した差分を解析し、その差分を現在のバッファに適用することで、コードの整形を実現しています。

問題は、gofmtが標準入力から内容を受け取って差分を生成する際、差分ヘッダー(--- a/path/to/file+++ b/path/to/file の行)に一時ファイル名(例: /tmp/gofmtXXXXX)を使用することがある点にありました。go-mode.elgofmt-apply-patch関数は、この一時ファイル名を実際のファイルパスに置き換える処理を行っていましたが、そのロジックに不備があり、特に/tmpディレクトリに存在するGoファイルに対してgofmtを適用しようとすると、パッチの適用に失敗する問題が発生していました。

具体的な失敗の原因は、元のコードが一時ファイルパスを「/tmp/ + ファイル名」という形式に変換しようとしていたことにあります。もし編集中のファイル自体が/tmp/foo.goのようなパスであった場合、gofmtが生成する一時ファイルパス(例: /tmp/gofmt12345)を、元のコードは「/tmp/ + foo.go」つまり/tmp/foo.goに変換しようとします。しかし、この変換ロジックが、gofmtが生成するdiffの実際の構造や、ファイルが/tmpに存在する場合のパス解決と合致せず、パッチの適用が正しく行われない状況が生じていました。

前提知識の解説

  • Emacs Lisp (Elisp): Emacsエディタの拡張機能や設定を記述するために使用されるプログラミング言語です。go-mode.elはこのElispで書かれています。
  • gofmt: Go言語の公式ツールで、Goソースコードを自動的に整形します。-dオプションを付けると、整形前後の差分(diff形式)を出力します。
  • Diff形式: ファイルの変更内容を示す標準的なテキスト形式です。通常、変更された行の前に+-が付き、変更されたファイルのパスは--- a/path/to/file(変更前)と+++ b/path/to/file(変更後)のようなヘッダーで示されます。
  • 一時ファイル: プログラムが一時的にデータを保存するために作成するファイルです。gofmtが標準入力から処理を行う際、内部的に一時ファイルを使用することがあります。
  • replace-regexp (Emacs Lisp関数): 指定された正規表現にマッチする文字列を、別の文字列に置換するEmacs Lispの関数です。
  • file-name-nondirectory (Emacs Lisp関数): ファイルパスからディレクトリ部分を除外し、ファイル名のみを返す関数です。例: (file-name-nondirectory "/path/to/file.go")"file.go" を返します。
  • point-min / point-max (Emacs Lisp関数): 現在のバッファの先頭(最小ポイント)と末尾(最大ポイント)を返します。replace-regexpなどの関数で置換範囲を指定する際に使用されます。

技術的詳細

このコミットの核心は、gofmt-apply-patch関数におけるdiffヘッダーのパス置換ロジックの改善です。

gofmt-apply-patch関数は、gofmt -dの出力であるdiffをpatchbuf(パッチが格納されたバッファ)から読み込み、それをsrcbuf(元のGoコードが格納されたバッファ)に適用します。

元のコードでは、以下の2段階の置換を行っていました。

  1. (replace-string gofmt-stdin-tag filename nil min (point-max))
    • gofmt-stdin-tagという内部的なタグ(おそらくgofmtが標準入力処理時にdiffヘッダーに埋め込む可能性のある文字列、例: <stdin>-)を、filename(ディレクトリ部分を除いたファイル名)に置換しようとしていました。
  2. (replace-regexp "^--- /tmp/gofmt[0-9]*" (concat "--- /tmp/" filename) nil min (point-max))
    • 正規表現^--- /tmp/gofmt[0-9]*にマッチする文字列(gofmtが生成する一時ファイルパスのヘッダー)を、--- /tmp/filename(ディレクトリ部分を除いたファイル名)を結合したパスに置換していました。

この2番目の置換が問題でした。filename変数はfile-name-nondirectoryによってファイル名のみに変換されていたため、例えば/home/user/project/main.goというファイルの場合、filenamemain.goとなります。このとき、置換後のパスは--- /tmp/main.goとなります。しかし、実際のファイルパスは/home/user/project/main.goであり、gofmtが生成するdiffは、一時ファイルパスを実際のファイルパスにマッピングする必要があります。--- /tmp/main.goというパスは、元のファイルが/tmpディレクトリにない限り、正しくありません。特に、元のファイルが/tmpディレクトリに存在する場合(例: /tmp/test.go)、この置換はさらに混乱を招く可能性がありました。

新しいコードでは、この問題を解決するために以下の変更が行われました。

  1. letブロックとreplace-string gofmt-stdin-tagの行が削除されました。
    • これは、gofmt-stdin-tagによる置換が不要になったか、あるいは問題の原因となっていたことを示唆しています。gofmtの出力形式が変更され、gofmt-stdin-tagのようなプレースホルダーが使われなくなったか、あるいは次のreplace-regexpで十分になったと考えられます。
  2. replace-regexpの置換文字列が(concat "--- " filename)に変更されました。
    • ここで重要なのは、filename変数がgofmt-apply-patch関数の引数として渡される絶対パス(例: /home/user/project/main.go)を指すようになった点です。これにより、gofmtが生成する一時ファイルパス(例: --- /tmp/gofmt12345)は、直接--- /home/user/project/main.goのような正しい絶対パスに置換されるようになります。これにより、ファイルがどこにあっても、gofmtのdiffが正しく適用されるようになります。

この変更により、gofmtが一時ファイル名を使用するdiffを生成した場合でも、Emacsのgo-modeがそのdiffを正しく解釈し、実際のファイルパスにマッピングして適用できるようになり、/tmpディレクトリ内のファイルに対するgofmtの適用失敗が解消されました。

コアとなるコードの変更箇所

misc/emacs/go-mode.elファイルのgofmt-apply-patch関数内。

--- a/misc/emacs/go-mode.el
+++ b/misc/emacs/go-mode.el
@@ -817,13 +817,10 @@ Replace the current buffer on success; display errors on failure."
 
 (defun gofmt-apply-patch (filename srcbuf patchbuf)
   (require 'diff-mode)
-  ;; apply all the patch hunks and restore the mark and point
+  ;; apply all the patch hunks
   (with-current-buffer patchbuf
-    (let ((filename (file-name-nondirectory filename))
-          (min (point-min)))
-      (replace-string gofmt-stdin-tag  filename nil min (point-max))
-      (replace-regexp "^--- /tmp/gofmt[0-9]*" (concat "--- /tmp/" filename)
-                      nil min (point-max)))
+    (replace-regexp "^--- /tmp/gofmt[0-9]*" (concat "--- " filename)
+                      nil (point-min) (point-max))
     (condition-case nil
         (while t
           (diff-hunk-next)

コアとなるコードの解説

変更されたのはgofmt-apply-patch関数内の以下の部分です。

変更前:

    (let ((filename (file-name-nondirectory filename))
          (min (point-min)))
      (replace-string gofmt-stdin-tag  filename nil min (point-max))
      (replace-regexp "^--- /tmp/gofmt[0-9]*" (concat "--- /tmp/" filename)
                      nil min (point-max)))

このブロックでは、まずfilename変数をfile-name-nondirectoryでファイル名のみに再定義し、gofmt-stdin-tagの置換と、/tmp/gofmtから始まるパスの置換を行っていました。特に、後者の置換で--- /tmp/にファイル名を結合していた点が問題でした。

変更後:

    (replace-regexp "^--- /tmp/gofmt[0-9]*" (concat "--- " filename)
                      nil (point-min) (point-max))

変更後では、letブロックが削除され、gofmt-stdin-tagの置換もなくなりました。そして、replace-regexpの置換文字列が(concat "--- " filename)に簡略化されました。ここで使われているfilenameは、gofmt-apply-patch関数の引数として渡された元のファイルへの絶対パスです。これにより、gofmtが生成する一時ファイルパスのヘッダーが、編集中のファイルの正しい絶対パスに置換されるようになり、パッチ適用時のパス解決の不整合が解消されました。

関連リンク

参考にした情報源リンク

  • Go言語の公式リポジトリ: https://github.com/golang/go
  • Emacsのgo-mode.elソースコード(コミット前後の比較)
  • gofmtの動作に関する一般的な情報(標準入力と一時ファイルの使用)
  • Emacs Lispの関数に関するドキュメント