🟫

Ruby ファイルのリネームと、それに対応する module の修正は別の commit にしよう

に公開

Ruby を使用したコーディングではディレクトリに応じた module を定義することが多いと思います。
例えば Rails で app/controllers/examples_controller.rb のようなコントローラーがあり、これの置き場所について controllers ディレクトリ直下ではなく、v1 ディレクトリを挟みたいとなったとします。
その場合 app/controllers/v1/examples_controller.rb にリネームした後、ファイル内では module V1 の階層を増やすことになると思います。
このとき、ファイルのリネームとファイル内の修正である module の追加は別の commit にしようという話になります。

なぜ別の commit にすべきか

ファイルに紐づく commit 履歴を途絶えさせないためです。
同じ commit にすると、修正前のファイルは削除され、新たに新規ファイルが作られたとみなされるため、これまでの Git の履歴とファイルが紐づかなくなります。
例えば、あるコードからそれが実装された過去のプルリクエストを参照したい場合、ファイルの履歴から直接たどることができなくなります。
作成時と同じ commit 内の削除されたファイルを参照すればたどることはできるので、方法がなくなるわけではないですが一手間多く必要になります。

実際に試してみる

リネームと module の追加を同じ commit にした場合と別の commit にした場合の git log git blame を確認してみます。
以下のように変更します。

修正前のコード

app/controllers/examples_controller.rb
class ExamplesController < ApplicationController
  before_action :set_example, only: %i[show]

  # GET /examples or /examples.json
  def index
    @examples = Example.all
  end

  # GET /examples/1 or /examples/1.json
  def show
  end

  private
  # Use callbacks to share common setup or constraints between actions.
  def set_example
    @example = Example.find(params.expect(:id))
  end
end

修正後のコード

app/controllers/v1/examples_controller.rb
+ module V1
    class ExamplesController < ApplicationController
      ...
    end
+ end

修正前のコードの Git 履歴

examples_controller.rb には User A, User B による修正履歴が存在します。
git log では分かりやすさのため、commit のハッシュ値、作者、メッセージのみを表示しています。)

> git log --follow --pretty=format:"%h %an %s" app/controllers/examples_controller.rb
af5487a User B Add show
34241b6 User A Add index
dc47018 User A Create examples_controller.rb
> git blame app/controllers/examples_controller.rb
dc470188 (User A 2025-07-27 17:46:02 +0900  1) class ExamplesController < ApplicationController
af5487a6 (User B 2025-07-27 18:07:18 +0900  2)   before_action :set_example, only: %i[show]
af5487a6 (User B 2025-07-27 18:07:18 +0900  3) 
34241b68 (User A 2025-07-27 18:05:13 +0900  4)   # GET /examples or /examples.json
34241b68 (User A 2025-07-27 18:05:13 +0900  5)   def index
34241b68 (User A 2025-07-27 18:05:13 +0900  6)     @examples = Example.all
34241b68 (User A 2025-07-27 18:05:13 +0900  7)   end
af5487a6 (User B 2025-07-27 18:07:18 +0900  8) 
af5487a6 (User B 2025-07-27 18:07:18 +0900  9)   # GET /examples/1 or /examples/1.json
af5487a6 (User B 2025-07-27 18:07:18 +0900 10)   def show
af5487a6 (User B 2025-07-27 18:07:18 +0900 11)   end
af5487a6 (User B 2025-07-27 18:07:18 +0900 12) 
af5487a6 (User B 2025-07-27 18:07:18 +0900 13)   private
af5487a6 (User B 2025-07-27 18:07:18 +0900 14)   # Use callbacks to share common setup or constraints between actions.
af5487a6 (User B 2025-07-27 18:07:18 +0900 15)   def set_example
af5487a6 (User B 2025-07-27 18:07:18 +0900 16)     @example = Example.find(params.expect(:id))
af5487a6 (User B 2025-07-27 18:07:18 +0900 17)   end
dc470188 (User A 2025-07-27 17:46:02 +0900 18) end

ファイルのリネームと module の追加は新たに User C が行います。

修正後のコードに対し git log を実行

ファイルのリネームと module の追加を行った後の git log の結果はそれぞれ以下のようになります。

同じ commit で修正した場合

> git log --follow --pretty=format:"%h %an %s" app/controllers/v1/examples_controller.rb
9d15b43 User C Rename path and add module

別の commit で修正した場合

> git log --follow --pretty=format:"%h %an %s" app/controllers/v1/examples_controller.rb
a864285 User C Add module
4bf7d17 User C Rename path
af5487a User B Add show
34241b6 User A Add index
dc47018 User A Create examples_controller.rb

上記の結果から、同じ commit だと examples_controller.rb は完全に新しいファイルとみなされ履歴が新しくなっています。
別の commit だと履歴が残っていることがわかります。

修正後のコードに対し git blame を実行

git blame はそれぞれ以下のようになります。

同じ commit で修正した場合

> git blame app/controllers/v1/examples_controller.rb
9d15b438 (User C 2025-07-27 18:22:03 +0900  1) module V1
9d15b438 (User C 2025-07-27 18:22:03 +0900  2)   class ExamplesController < ApplicationController
9d15b438 (User C 2025-07-27 18:22:03 +0900  3)     before_action :set_example, only: %i[show]
9d15b438 (User C 2025-07-27 18:22:03 +0900  4) 
9d15b438 (User C 2025-07-27 18:22:03 +0900  5)     # GET /examples or /examples.json
9d15b438 (User C 2025-07-27 18:22:03 +0900  6)     def index
9d15b438 (User C 2025-07-27 18:22:03 +0900  7)       @examples = Example.all
9d15b438 (User C 2025-07-27 18:22:03 +0900  8)     end
9d15b438 (User C 2025-07-27 18:22:03 +0900  9) 
9d15b438 (User C 2025-07-27 18:22:03 +0900 10)     # GET /examples/1 or /examples/1.json
9d15b438 (User C 2025-07-27 18:22:03 +0900 11)     def show
9d15b438 (User C 2025-07-27 18:22:03 +0900 12)     end
9d15b438 (User C 2025-07-27 18:22:03 +0900 13) 
9d15b438 (User C 2025-07-27 18:22:03 +0900 14)     private
9d15b438 (User C 2025-07-27 18:22:03 +0900 15)     # Use callbacks to share common setup or constraints between actions.
9d15b438 (User C 2025-07-27 18:22:03 +0900 16)     def set_example
9d15b438 (User C 2025-07-27 18:22:03 +0900 17)       @example = Example.find(params.expect(:id))
9d15b438 (User C 2025-07-27 18:22:03 +0900 18)     end
9d15b438 (User C 2025-07-27 18:22:03 +0900 19)   end
9d15b438 (User C 2025-07-27 18:22:03 +0900 20) end

別の commit で修正した場合

> git blame app/controllers/v1/examples_controller.rb
a864285a app/controllers/v1/examples_controller.rb (User C 2025-07-27 18:27:23 +0900  1) module V1
a864285a app/controllers/v1/examples_controller.rb (User C 2025-07-27 18:27:23 +0900  2)   class ExamplesController < ApplicationController
a864285a app/controllers/v1/examples_controller.rb (User C 2025-07-27 18:27:23 +0900  3)     before_action :set_example, only: %i[show]
af5487a6 app/controllers/examples_controller.rb    (User B 2025-07-27 18:07:18 +0900  4) 
a864285a app/controllers/v1/examples_controller.rb (User C 2025-07-27 18:27:23 +0900  5)     # GET /examples or /examples.json
a864285a app/controllers/v1/examples_controller.rb (User C 2025-07-27 18:27:23 +0900  6)     def index
a864285a app/controllers/v1/examples_controller.rb (User C 2025-07-27 18:27:23 +0900  7)       @examples = Example.all
a864285a app/controllers/v1/examples_controller.rb (User C 2025-07-27 18:27:23 +0900  8)     end
af5487a6 app/controllers/examples_controller.rb    (User B 2025-07-27 18:07:18 +0900  9) 
a864285a app/controllers/v1/examples_controller.rb (User C 2025-07-27 18:27:23 +0900 10)     # GET /examples/1 or /examples/1.json
a864285a app/controllers/v1/examples_controller.rb (User C 2025-07-27 18:27:23 +0900 11)     def show
a864285a app/controllers/v1/examples_controller.rb (User C 2025-07-27 18:27:23 +0900 12)     end
af5487a6 app/controllers/examples_controller.rb    (User B 2025-07-27 18:07:18 +0900 13) 
a864285a app/controllers/v1/examples_controller.rb (User C 2025-07-27 18:27:23 +0900 14)     private
a864285a app/controllers/v1/examples_controller.rb (User C 2025-07-27 18:27:23 +0900 15)     # Use callbacks to share common setup or constraints between actions.
a864285a app/controllers/v1/examples_controller.rb (User C 2025-07-27 18:27:23 +0900 16)     def set_example
a864285a app/controllers/v1/examples_controller.rb (User C 2025-07-27 18:27:23 +0900 17)       @example = Example.find(params.expect(:id))
a864285a app/controllers/v1/examples_controller.rb (User C 2025-07-27 18:27:23 +0900 18)     end
af5487a6 app/controllers/examples_controller.rb    (User B 2025-07-27 18:07:18 +0900 19)   end
dc470188 app/controllers/examples_controller.rb    (User A 2025-07-27 17:46:02 +0900 20) end

上記でも履歴が残っていることがわかりますが、-w で空白の差分を無視すると見やすくなります。

> git blame -w app/controllers/v1/examples_controller.rb
a864285a app/controllers/v1/examples_controller.rb (User C 2025-07-27 18:27:23 +0900  1) module V1
dc470188 app/controllers/examples_controller.rb    (User A 2025-07-27 17:46:02 +0900  2)   class ExamplesController < ApplicationController
af5487a6 app/controllers/examples_controller.rb    (User B 2025-07-27 18:07:18 +0900  3)     before_action :set_example, only: %i[show]
af5487a6 app/controllers/examples_controller.rb    (User B 2025-07-27 18:07:18 +0900  4) 
34241b68 app/controllers/examples_controller.rb    (User A 2025-07-27 18:05:13 +0900  5)     # GET /examples or /examples.json
34241b68 app/controllers/examples_controller.rb    (User A 2025-07-27 18:05:13 +0900  6)     def index
34241b68 app/controllers/examples_controller.rb    (User A 2025-07-27 18:05:13 +0900  7)       @examples = Example.all
34241b68 app/controllers/examples_controller.rb    (User A 2025-07-27 18:05:13 +0900  8)     end
af5487a6 app/controllers/examples_controller.rb    (User B 2025-07-27 18:07:18 +0900  9) 
af5487a6 app/controllers/examples_controller.rb    (User B 2025-07-27 18:07:18 +0900 10)     # GET /examples/1 or /examples/1.json
af5487a6 app/controllers/examples_controller.rb    (User B 2025-07-27 18:07:18 +0900 11)     def show
af5487a6 app/controllers/examples_controller.rb    (User B 2025-07-27 18:07:18 +0900 12)     end
af5487a6 app/controllers/examples_controller.rb    (User B 2025-07-27 18:07:18 +0900 13) 
af5487a6 app/controllers/examples_controller.rb    (User B 2025-07-27 18:07:18 +0900 14)     private
af5487a6 app/controllers/examples_controller.rb    (User B 2025-07-27 18:07:18 +0900 15)     # Use callbacks to share common setup or constraints between actions.
af5487a6 app/controllers/examples_controller.rb    (User B 2025-07-27 18:07:18 +0900 16)     def set_example
af5487a6 app/controllers/examples_controller.rb    (User B 2025-07-27 18:07:18 +0900 17)       @example = Example.find(params.expect(:id))
af5487a6 app/controllers/examples_controller.rb    (User B 2025-07-27 18:07:18 +0900 18)     end
dc470188 app/controllers/examples_controller.rb    (User A 2025-07-27 17:46:02 +0900 19)   end
a864285a app/controllers/v1/examples_controller.rb (User C 2025-07-27 18:27:23 +0900 20) end

なぜ別の commit だと履歴が残るのか

ファイルごとの履歴を途絶えさせないためには、ファイルの修正が削除→新規作成ではなく、リネームであると Git に判定させることが必要になります。
ただ Git ではファイルがどうリネームされたかという情報は保持しておらず、コマンド実行時にファイル差分を見比べてリネームかどうかを検出しています。
削除されたファイルと新規ファイルの差分を比較したとき類似度(similarity)という値が計算され、これが基準値を超えると同じファイルだと認識しリネームと判定されます。
この基準値が git diffgit log では 50% がデフォルトとなっています。

Git git-diff#Documentation

The default similarity index is 50%.

今回の場合で考えると、別の commit で実施した場合は当たり前ですがリネーム時のファイルの差分は存在しません。類似度は 100% となり、もちろんリネーム判定されます。

同じ commit で行った場合の類似度が一体いくつなのかが重要です。git diff-M オプションの基準値を減らしていって試してみます。

git diff -w -M3% head~
...

--- a/app/controllers/examples_controller.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-class ExamplesController < ApplicationController
-  before_action :set_example, only: %i[show]
-
-  # GET /examples or /examples.json
-  def index
-    @examples = Example.all
-  end
-
-  # GET /examples/1 or /examples/1.json
-  def show
-  end
-
-  private
-  # Use callbacks to share common setup or constraints between actions.
-  def set_example
-    @example = Example.find(params.expect(:id))
-  end
-end

...

--- /dev/null
+++ b/app/controllers/v1/examples_controller.rb
@@ -0,0 +1,20 @@
+module V1
+  class ExamplesController < ApplicationController
+    before_action :set_example, only: %i[show]
+
+    # GET /examples or /examples.json
+    def index
+      @examples = Example.all
+    end
+
+    # GET /examples/1 or /examples/1.json
+    def show
+    end
+
+    private
+    # Use callbacks to share common setup or constraints between actions.
+    def set_example
+      @example = Example.find(params.expect(:id))
+    end
+  end
+end
git diff -w -M2% head~
...

--- a/app/controllers/examples_controller.rb
+++ b/app/controllers/v1/examples_controller.rb
@@ -1,3 +1,4 @@
+module V1
   class ExamplesController < ApplicationController
     before_action :set_example, only: %i[show]
 
@@ -16,3 +17,4 @@ class ExamplesController < ApplicationController
       @example = Example.find(params.expect(:id))
     end
   end
+end

基準値 3% では削除→新規作成、基準値 2% ではリネームと判定されました。
つまり類似度は 2~3% ということになります。

ファイルの中身はほとんど変わっていないにも関わらず、Git 的には 2% ほどしか似ていないと判定されていることになります。

module を追加する場合、必然的にファイル内のほぼ全行のインデントが増加します。
上記の git diff では -w オプションで空白を無視しているので差分自体は多くないように見えますが、類似度の計算では空白は無視されておらず、ほぼ全行にわたり修正が存在し差分だらけだと判定されていると予想できます。
仮にエディタの Git プラグインや Github 等からも類似度の基準値を変えて結果を表示できたとしても、さすがに 2% まで下げてしまうと、他のなんでもない削除 & 新規追加ファイルまでリネーム判定されてしまいそうです。
そのため、類似度の計算方法が変わらない限りは、ファイルごとの履歴を残すにはリネームと module の追加は別 commit で行うことが求められそうです。

おわりに

Ruby ファイルのリネームと module の追加を別の commit にすべき理由を説明しました。
今回の内容は Ruby に限った話ではなく、リネームと同時にファイル差分が生じる状況においては同じことが言えそうです。
皆様の Git 操作の参考になれば幸いです。

Social PLUS Tech Blog

Discussion