ruvim 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/AGENTS.md +18 -6
- data/README.md +15 -1
- data/docs/binding.md +16 -0
- data/docs/command.md +78 -4
- data/docs/config.md +10 -2
- data/docs/spec.md +60 -9
- data/docs/tutorial.md +24 -0
- data/docs/vim_diff.md +18 -8
- data/lib/ruvim/app.rb +290 -8
- data/lib/ruvim/buffer.rb +14 -2
- data/lib/ruvim/cli.rb +6 -0
- data/lib/ruvim/editor.rb +12 -1
- data/lib/ruvim/file_watcher.rb +243 -0
- data/lib/ruvim/git/blame.rb +245 -0
- data/lib/ruvim/git/branch.rb +97 -0
- data/lib/ruvim/git/commit.rb +102 -0
- data/lib/ruvim/git/diff.rb +129 -0
- data/lib/ruvim/git/handler.rb +84 -0
- data/lib/ruvim/git/log.rb +41 -0
- data/lib/ruvim/git/status.rb +103 -0
- data/lib/ruvim/global_commands.rb +176 -42
- data/lib/ruvim/highlighter.rb +3 -1
- data/lib/ruvim/input.rb +1 -0
- data/lib/ruvim/lang/diff.rb +41 -0
- data/lib/ruvim/lang/json.rb +34 -0
- data/lib/ruvim/rich_view/json_renderer.rb +131 -0
- data/lib/ruvim/rich_view/jsonl_renderer.rb +57 -0
- data/lib/ruvim/rich_view.rb +16 -0
- data/lib/ruvim/screen.rb +9 -12
- data/lib/ruvim/version.rb +1 -1
- data/lib/ruvim.rb +10 -0
- data/test/app_completion_test.rb +25 -0
- data/test/app_scenario_test.rb +169 -0
- data/test/cli_test.rb +14 -0
- data/test/clipboard_test.rb +67 -0
- data/test/command_line_test.rb +118 -0
- data/test/config_dsl_test.rb +87 -0
- data/test/display_width_test.rb +41 -0
- data/test/file_watcher_test.rb +197 -0
- data/test/follow_test.rb +199 -0
- data/test/git_blame_test.rb +713 -0
- data/test/highlighter_test.rb +44 -0
- data/test/indent_test.rb +86 -0
- data/test/rich_view_test.rb +256 -0
- data/test/search_option_test.rb +19 -0
- data/test/test_helper.rb +9 -0
- metadata +17 -1
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
|
|
7
|
+
class GitBlameTest < Minitest::Test
|
|
8
|
+
def setup
|
|
9
|
+
@app = RuVim::App.new(clean: true)
|
|
10
|
+
@editor = @app.instance_variable_get(:@editor)
|
|
11
|
+
@dispatcher = @app.instance_variable_get(:@dispatcher)
|
|
12
|
+
@editor.materialize_intro_buffer!
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def feed(*keys)
|
|
16
|
+
keys.each { |k| @app.send(:handle_key, k) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def drain_git_stream!
|
|
20
|
+
threads = @app.instance_variable_get(:@git_stream_threads)
|
|
21
|
+
threads&.each_value(&:join)
|
|
22
|
+
@app.send(:drain_stream_events!)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# --- Parsing ---
|
|
26
|
+
|
|
27
|
+
def test_parse_porcelain_basic
|
|
28
|
+
porcelain = <<~PORCELAIN
|
|
29
|
+
abc1234abc1234abc1234abc1234abc1234abc123 1 1 1
|
|
30
|
+
author Alice
|
|
31
|
+
author-mail <alice@example.com>
|
|
32
|
+
author-time 1700000000
|
|
33
|
+
author-tz +0900
|
|
34
|
+
committer Alice
|
|
35
|
+
committer-mail <alice@example.com>
|
|
36
|
+
committer-time 1700000000
|
|
37
|
+
committer-tz +0900
|
|
38
|
+
summary Initial commit
|
|
39
|
+
filename foo.rb
|
|
40
|
+
\thello world
|
|
41
|
+
PORCELAIN
|
|
42
|
+
|
|
43
|
+
entries = RuVim::Git::Blame.parse_porcelain(porcelain)
|
|
44
|
+
assert_equal 1, entries.length
|
|
45
|
+
e = entries.first
|
|
46
|
+
assert_equal "abc1234a", e[:short_hash]
|
|
47
|
+
assert_equal "Alice", e[:author]
|
|
48
|
+
assert_equal "Initial commit", e[:summary]
|
|
49
|
+
assert_equal "hello world", e[:text]
|
|
50
|
+
assert_equal 1, e[:orig_line]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_parse_porcelain_multi_line
|
|
54
|
+
porcelain = <<~PORCELAIN
|
|
55
|
+
aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111 1 1 2
|
|
56
|
+
author Alice
|
|
57
|
+
author-mail <alice@example.com>
|
|
58
|
+
author-time 1700000000
|
|
59
|
+
author-tz +0900
|
|
60
|
+
committer Alice
|
|
61
|
+
committer-mail <alice@example.com>
|
|
62
|
+
committer-time 1700000000
|
|
63
|
+
committer-tz +0900
|
|
64
|
+
summary First
|
|
65
|
+
filename foo.rb
|
|
66
|
+
\tline one
|
|
67
|
+
aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111 2 2
|
|
68
|
+
\tline two
|
|
69
|
+
PORCELAIN
|
|
70
|
+
|
|
71
|
+
entries = RuVim::Git::Blame.parse_porcelain(porcelain)
|
|
72
|
+
assert_equal 2, entries.length
|
|
73
|
+
assert_equal "line one", entries[0][:text]
|
|
74
|
+
assert_equal "line two", entries[1][:text]
|
|
75
|
+
assert_equal "aaaa1111", entries[0][:short_hash]
|
|
76
|
+
assert_equal "aaaa1111", entries[1][:short_hash]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def test_format_blame_lines
|
|
80
|
+
entries = [
|
|
81
|
+
{ short_hash: "abc12345", author: "Alice", date: "2023-11-14", text: "hello", orig_line: 1, hash: "abc12345" * 5 },
|
|
82
|
+
{ short_hash: "def67890", author: "Bob", date: "2023-11-15", text: "world", orig_line: 2, hash: "def67890" * 5 },
|
|
83
|
+
]
|
|
84
|
+
lines = RuVim::Git::Blame.format_lines(entries)
|
|
85
|
+
assert_equal 2, lines.length
|
|
86
|
+
assert_includes lines[0], "abc12345"
|
|
87
|
+
assert_includes lines[0], "Alice"
|
|
88
|
+
assert_includes lines[0], "hello"
|
|
89
|
+
assert_includes lines[1], "Bob"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# --- Status filename parsing ---
|
|
93
|
+
|
|
94
|
+
def test_parse_status_modified_line
|
|
95
|
+
assert_equal "lib/ruvim/app.rb", RuVim::Git::Status.parse_filename("\tmodified: lib/ruvim/app.rb")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def test_parse_status_new_file_line
|
|
99
|
+
assert_equal "foo.rb", RuVim::Git::Status.parse_filename("\tnew file: foo.rb")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def test_parse_status_untracked_line
|
|
103
|
+
assert_equal "bar.txt", RuVim::Git::Status.parse_filename("\tbar.txt")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_parse_status_header_returns_nil
|
|
107
|
+
assert_nil RuVim::Git::Status.parse_filename("Changes not staged for commit:")
|
|
108
|
+
assert_nil RuVim::Git::Status.parse_filename(" (use \"git add <file>...\")")
|
|
109
|
+
assert_nil RuVim::Git::Status.parse_filename("On branch master")
|
|
110
|
+
assert_nil RuVim::Git::Status.parse_filename("")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# --- Integration with App (using git repo) ---
|
|
114
|
+
|
|
115
|
+
def test_ctrl_g_enters_git_command_mode
|
|
116
|
+
feed(:ctrl_g)
|
|
117
|
+
assert_equal :command_line, @editor.mode
|
|
118
|
+
assert_equal ":git ", @editor.command_line.content
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def test_git_blame_via_ex_git_subcommand
|
|
122
|
+
Dir.mktmpdir do |dir|
|
|
123
|
+
setup_git_repo(dir, "test_file.txt", "line1\nline2\n")
|
|
124
|
+
|
|
125
|
+
file_path = File.join(dir, "test_file.txt")
|
|
126
|
+
buf = @editor.add_buffer_from_file(file_path)
|
|
127
|
+
@editor.switch_to_buffer(buf.id)
|
|
128
|
+
|
|
129
|
+
@dispatcher.dispatch_ex(@editor, "git blame")
|
|
130
|
+
|
|
131
|
+
blame_buf = @editor.current_buffer
|
|
132
|
+
assert_equal :blame, blame_buf.kind
|
|
133
|
+
assert_equal 2, blame_buf.line_count
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def test_git_blame_opens_blame_buffer
|
|
138
|
+
Dir.mktmpdir do |dir|
|
|
139
|
+
setup_git_repo(dir, "test_file.txt", "line1\nline2\nline3\n")
|
|
140
|
+
|
|
141
|
+
file_path = File.join(dir, "test_file.txt")
|
|
142
|
+
buf = @editor.add_buffer_from_file(file_path)
|
|
143
|
+
@editor.switch_to_buffer(buf.id)
|
|
144
|
+
|
|
145
|
+
@dispatcher.dispatch_ex(@editor, "git blame")
|
|
146
|
+
|
|
147
|
+
blame_buf = @editor.current_buffer
|
|
148
|
+
assert_equal :blame, blame_buf.kind
|
|
149
|
+
assert blame_buf.readonly?
|
|
150
|
+
refute blame_buf.modifiable?
|
|
151
|
+
assert_match(/\[Blame\]/, blame_buf.name)
|
|
152
|
+
assert_equal 3, blame_buf.line_count
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def test_git_blame_buffer_local_bindings
|
|
157
|
+
Dir.mktmpdir do |dir|
|
|
158
|
+
setup_git_repo(dir, "test_file.txt", "line1\nline2\n")
|
|
159
|
+
|
|
160
|
+
file_path = File.join(dir, "test_file.txt")
|
|
161
|
+
buf = @editor.add_buffer_from_file(file_path)
|
|
162
|
+
@editor.switch_to_buffer(buf.id)
|
|
163
|
+
|
|
164
|
+
@dispatcher.dispatch_ex(@editor, "git blame")
|
|
165
|
+
|
|
166
|
+
blame_buf = @editor.current_buffer
|
|
167
|
+
assert_equal :blame, blame_buf.kind
|
|
168
|
+
|
|
169
|
+
# 'c' should be bound to git.blame.commit for this buffer
|
|
170
|
+
km = @app.instance_variable_get(:@keymaps)
|
|
171
|
+
match = km.resolve_with_context(:normal, ["c"], editor: @editor)
|
|
172
|
+
assert_equal :match, match.status
|
|
173
|
+
assert_equal "git.blame.commit", match.invocation.id
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def test_git_blame_commit_opens_show_buffer
|
|
178
|
+
Dir.mktmpdir do |dir|
|
|
179
|
+
setup_git_repo(dir, "test_file.txt", "line1\nline2\n")
|
|
180
|
+
|
|
181
|
+
file_path = File.join(dir, "test_file.txt")
|
|
182
|
+
buf = @editor.add_buffer_from_file(file_path)
|
|
183
|
+
@editor.switch_to_buffer(buf.id)
|
|
184
|
+
|
|
185
|
+
@dispatcher.dispatch_ex(@editor, "git blame")
|
|
186
|
+
assert_equal :blame, @editor.current_buffer.kind
|
|
187
|
+
|
|
188
|
+
# Press 'c' to show commit
|
|
189
|
+
feed("c")
|
|
190
|
+
|
|
191
|
+
show_buf = @editor.current_buffer
|
|
192
|
+
assert_equal :git_show, show_buf.kind
|
|
193
|
+
refute show_buf.modifiable?
|
|
194
|
+
assert_match(/\[Commit\]/, show_buf.name)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def test_git_blame_prev_and_back
|
|
199
|
+
Dir.mktmpdir do |dir|
|
|
200
|
+
setup_git_repo_multi_commits(dir, "test_file.txt")
|
|
201
|
+
|
|
202
|
+
file_path = File.join(dir, "test_file.txt")
|
|
203
|
+
buf = @editor.add_buffer_from_file(file_path)
|
|
204
|
+
@editor.switch_to_buffer(buf.id)
|
|
205
|
+
|
|
206
|
+
@dispatcher.dispatch_ex(@editor, "git blame")
|
|
207
|
+
blame_buf = @editor.current_buffer
|
|
208
|
+
assert_equal :blame, blame_buf.kind
|
|
209
|
+
|
|
210
|
+
original_lines = blame_buf.lines.dup
|
|
211
|
+
|
|
212
|
+
# Press 'p' to go to previous blame
|
|
213
|
+
feed("p")
|
|
214
|
+
assert_equal :blame, @editor.current_buffer.kind
|
|
215
|
+
refute_equal original_lines, @editor.current_buffer.lines
|
|
216
|
+
|
|
217
|
+
# Press 'P' to go back
|
|
218
|
+
feed("P")
|
|
219
|
+
assert_equal :blame, @editor.current_buffer.kind
|
|
220
|
+
assert_equal original_lines, @editor.current_buffer.lines
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def test_git_unknown_subcommand_shows_error
|
|
225
|
+
@dispatcher.dispatch_ex(@editor, "git unknown")
|
|
226
|
+
assert_match(/Unknown Git subcommand/, @editor.message)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def test_git_no_subcommand_shows_list
|
|
230
|
+
@dispatcher.dispatch_ex(@editor, "git")
|
|
231
|
+
assert_match(/blame/, @editor.message)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# --- GitStatus ---
|
|
235
|
+
|
|
236
|
+
def test_git_status
|
|
237
|
+
Dir.mktmpdir do |dir|
|
|
238
|
+
setup_git_repo(dir, "test_file.txt", "line1\n")
|
|
239
|
+
|
|
240
|
+
file_path = File.join(dir, "test_file.txt")
|
|
241
|
+
buf = @editor.add_buffer_from_file(file_path)
|
|
242
|
+
@editor.switch_to_buffer(buf.id)
|
|
243
|
+
|
|
244
|
+
@dispatcher.dispatch_ex(@editor, "git status")
|
|
245
|
+
|
|
246
|
+
status_buf = @editor.current_buffer
|
|
247
|
+
assert_equal :git_status, status_buf.kind
|
|
248
|
+
assert status_buf.readonly?
|
|
249
|
+
assert_match(/\[Git Status\]/, status_buf.name)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def test_git_status_enter_opens_file
|
|
254
|
+
Dir.mktmpdir do |dir|
|
|
255
|
+
setup_git_repo(dir, "test_file.txt", "line1\n")
|
|
256
|
+
# Create an uncommitted change so it shows in status
|
|
257
|
+
File.write(File.join(dir, "test_file.txt"), "modified\n")
|
|
258
|
+
|
|
259
|
+
file_path = File.join(dir, "test_file.txt")
|
|
260
|
+
buf = @editor.add_buffer_from_file(file_path)
|
|
261
|
+
@editor.switch_to_buffer(buf.id)
|
|
262
|
+
|
|
263
|
+
@dispatcher.dispatch_ex(@editor, "git status")
|
|
264
|
+
assert_equal :git_status, @editor.current_buffer.kind
|
|
265
|
+
|
|
266
|
+
# Move to a line containing the modified file
|
|
267
|
+
status_buf = @editor.current_buffer
|
|
268
|
+
target_line = status_buf.lines.index { |l| l.include?("test_file.txt") }
|
|
269
|
+
assert target_line, "Expected test_file.txt in status output"
|
|
270
|
+
@editor.current_window.cursor_y = target_line
|
|
271
|
+
|
|
272
|
+
feed(:enter)
|
|
273
|
+
|
|
274
|
+
# Should have opened test_file.txt
|
|
275
|
+
assert_equal File.join(dir, "test_file.txt"), @editor.current_buffer.path
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# --- GitDiff ---
|
|
280
|
+
|
|
281
|
+
def test_git_diff
|
|
282
|
+
Dir.mktmpdir do |dir|
|
|
283
|
+
setup_git_repo(dir, "test_file.txt", "line1\n")
|
|
284
|
+
# Make an uncommitted change
|
|
285
|
+
File.write(File.join(dir, "test_file.txt"), "modified\n")
|
|
286
|
+
|
|
287
|
+
file_path = File.join(dir, "test_file.txt")
|
|
288
|
+
buf = @editor.add_buffer_from_file(file_path)
|
|
289
|
+
@editor.switch_to_buffer(buf.id)
|
|
290
|
+
|
|
291
|
+
@dispatcher.dispatch_ex(@editor, "git diff")
|
|
292
|
+
|
|
293
|
+
diff_buf = @editor.current_buffer
|
|
294
|
+
assert_equal :git_diff, diff_buf.kind
|
|
295
|
+
assert diff_buf.readonly?
|
|
296
|
+
assert_match(/\[Git Diff\]/, diff_buf.name)
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def test_git_diff_parse_location_on_hunk_header
|
|
301
|
+
lines = [
|
|
302
|
+
"diff --git a/lib/foo.rb b/lib/foo.rb",
|
|
303
|
+
"--- a/lib/foo.rb",
|
|
304
|
+
"+++ b/lib/foo.rb",
|
|
305
|
+
"@@ -1,3 +1,4 @@",
|
|
306
|
+
" line1",
|
|
307
|
+
"+added",
|
|
308
|
+
" line2",
|
|
309
|
+
" line3",
|
|
310
|
+
]
|
|
311
|
+
# On hunk header line → file at start of hunk
|
|
312
|
+
file, line = RuVim::Git::Diff.parse_location(lines, 3)
|
|
313
|
+
assert_equal "lib/foo.rb", file
|
|
314
|
+
assert_equal 1, line
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def test_git_diff_parse_location_on_context_line
|
|
318
|
+
lines = [
|
|
319
|
+
"diff --git a/lib/foo.rb b/lib/foo.rb",
|
|
320
|
+
"--- a/lib/foo.rb",
|
|
321
|
+
"+++ b/lib/foo.rb",
|
|
322
|
+
"@@ -1,3 +1,4 @@",
|
|
323
|
+
" line1",
|
|
324
|
+
"+added",
|
|
325
|
+
" line2",
|
|
326
|
+
]
|
|
327
|
+
# On " line2" (index 6): new-side lines are " line1"(+1), "+added"(+2), " line2"(+3)
|
|
328
|
+
file, line = RuVim::Git::Diff.parse_location(lines, 6)
|
|
329
|
+
assert_equal "lib/foo.rb", file
|
|
330
|
+
assert_equal 3, line
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def test_git_diff_parse_location_on_added_line
|
|
334
|
+
lines = [
|
|
335
|
+
"diff --git a/lib/foo.rb b/lib/foo.rb",
|
|
336
|
+
"--- a/lib/foo.rb",
|
|
337
|
+
"+++ b/lib/foo.rb",
|
|
338
|
+
"@@ -1,3 +1,4 @@",
|
|
339
|
+
" line1",
|
|
340
|
+
"+added",
|
|
341
|
+
" line2",
|
|
342
|
+
]
|
|
343
|
+
file, line = RuVim::Git::Diff.parse_location(lines, 5)
|
|
344
|
+
assert_equal "lib/foo.rb", file
|
|
345
|
+
assert_equal 2, line
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def test_git_diff_parse_location_on_deleted_line
|
|
349
|
+
lines = [
|
|
350
|
+
"diff --git a/lib/foo.rb b/lib/foo.rb",
|
|
351
|
+
"--- a/lib/foo.rb",
|
|
352
|
+
"+++ b/lib/foo.rb",
|
|
353
|
+
"@@ -1,3 +1,3 @@",
|
|
354
|
+
" line1",
|
|
355
|
+
"-removed",
|
|
356
|
+
" line2",
|
|
357
|
+
]
|
|
358
|
+
# Deleted line → jump to the next new-side line position
|
|
359
|
+
file, line = RuVim::Git::Diff.parse_location(lines, 5)
|
|
360
|
+
assert_equal "lib/foo.rb", file
|
|
361
|
+
assert_equal 2, line
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def test_git_diff_parse_location_on_diff_header
|
|
365
|
+
lines = [
|
|
366
|
+
"diff --git a/lib/foo.rb b/lib/foo.rb",
|
|
367
|
+
"index abc..def 100644",
|
|
368
|
+
"--- a/lib/foo.rb",
|
|
369
|
+
"+++ b/lib/foo.rb",
|
|
370
|
+
"@@ -1,3 +1,3 @@",
|
|
371
|
+
]
|
|
372
|
+
# On "diff --git" line → file, line 1
|
|
373
|
+
file, line = RuVim::Git::Diff.parse_location(lines, 0)
|
|
374
|
+
assert_equal "lib/foo.rb", file
|
|
375
|
+
assert_equal 1, line
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def test_git_diff_parse_location_returns_nil_for_empty
|
|
379
|
+
assert_nil RuVim::Git::Diff.parse_location([], 0)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def test_git_diff_enter_opens_file
|
|
383
|
+
Dir.mktmpdir do |dir|
|
|
384
|
+
setup_git_repo(dir, "test_file.txt", "line1\nline2\nline3\n")
|
|
385
|
+
File.write(File.join(dir, "test_file.txt"), "line1\nmodified\nline3\n")
|
|
386
|
+
|
|
387
|
+
file_path = File.join(dir, "test_file.txt")
|
|
388
|
+
buf = @editor.add_buffer_from_file(file_path)
|
|
389
|
+
@editor.switch_to_buffer(buf.id)
|
|
390
|
+
|
|
391
|
+
@dispatcher.dispatch_ex(@editor, "git diff")
|
|
392
|
+
assert_equal :git_diff, @editor.current_buffer.kind
|
|
393
|
+
|
|
394
|
+
# Find a line with the modified content
|
|
395
|
+
diff_buf = @editor.current_buffer
|
|
396
|
+
target_line = diff_buf.lines.index { |l| l.start_with?("+modified") }
|
|
397
|
+
assert target_line, "Expected +modified in diff output"
|
|
398
|
+
@editor.current_window.cursor_y = target_line
|
|
399
|
+
|
|
400
|
+
feed(:enter)
|
|
401
|
+
|
|
402
|
+
# Should have opened test_file.txt
|
|
403
|
+
assert_equal File.join(dir, "test_file.txt"), @editor.current_buffer.path
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def test_git_diff_clean_shows_message
|
|
408
|
+
Dir.mktmpdir do |dir|
|
|
409
|
+
setup_git_repo(dir, "test_file.txt", "line1\n")
|
|
410
|
+
|
|
411
|
+
file_path = File.join(dir, "test_file.txt")
|
|
412
|
+
buf = @editor.add_buffer_from_file(file_path)
|
|
413
|
+
@editor.switch_to_buffer(buf.id)
|
|
414
|
+
|
|
415
|
+
@dispatcher.dispatch_ex(@editor, "git diff")
|
|
416
|
+
|
|
417
|
+
# Should stay on original buffer, no diff buffer opened
|
|
418
|
+
assert_equal :file, @editor.current_buffer.kind
|
|
419
|
+
assert_match(/clean/, @editor.message)
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# --- GitLog ---
|
|
424
|
+
|
|
425
|
+
def test_git_log
|
|
426
|
+
Dir.mktmpdir do |dir|
|
|
427
|
+
setup_git_repo(dir, "test_file.txt", "line1\n")
|
|
428
|
+
|
|
429
|
+
file_path = File.join(dir, "test_file.txt")
|
|
430
|
+
buf = @editor.add_buffer_from_file(file_path)
|
|
431
|
+
@editor.switch_to_buffer(buf.id)
|
|
432
|
+
|
|
433
|
+
@dispatcher.dispatch_ex(@editor, "git log")
|
|
434
|
+
drain_git_stream!
|
|
435
|
+
|
|
436
|
+
log_buf = @editor.current_buffer
|
|
437
|
+
assert_equal :git_log, log_buf.kind
|
|
438
|
+
assert log_buf.readonly?
|
|
439
|
+
assert_match(/\[Git Log\]/, log_buf.name)
|
|
440
|
+
assert log_buf.lines.any? { |l| l.include?("initial") }
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def test_git_log_with_p_flag
|
|
445
|
+
Dir.mktmpdir do |dir|
|
|
446
|
+
setup_git_repo(dir, "test_file.txt", "line1\n")
|
|
447
|
+
|
|
448
|
+
file_path = File.join(dir, "test_file.txt")
|
|
449
|
+
buf = @editor.add_buffer_from_file(file_path)
|
|
450
|
+
@editor.switch_to_buffer(buf.id)
|
|
451
|
+
|
|
452
|
+
@dispatcher.dispatch_ex(@editor, "git log -p")
|
|
453
|
+
drain_git_stream!
|
|
454
|
+
|
|
455
|
+
log_buf = @editor.current_buffer
|
|
456
|
+
assert_equal :git_log, log_buf.kind
|
|
457
|
+
# -p shows diffs, so output should include diff markers
|
|
458
|
+
assert log_buf.lines.any? { |l| l.start_with?("diff") || l.start_with?("+++") || l.start_with?("---") }
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# --- GitBranch ---
|
|
463
|
+
|
|
464
|
+
def test_git_branch
|
|
465
|
+
Dir.mktmpdir do |dir|
|
|
466
|
+
setup_git_repo(dir, "test_file.txt", "line1\n")
|
|
467
|
+
|
|
468
|
+
file_path = File.join(dir, "test_file.txt")
|
|
469
|
+
buf = @editor.add_buffer_from_file(file_path)
|
|
470
|
+
@editor.switch_to_buffer(buf.id)
|
|
471
|
+
|
|
472
|
+
@dispatcher.dispatch_ex(@editor, "git branch")
|
|
473
|
+
|
|
474
|
+
branch_buf = @editor.current_buffer
|
|
475
|
+
assert_equal :git_branch, branch_buf.kind
|
|
476
|
+
assert branch_buf.readonly?
|
|
477
|
+
assert_match(/\[Git Branch\]/, branch_buf.name)
|
|
478
|
+
assert branch_buf.lines.any? { |l| l.include?("master") || l.include?("main") }
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def test_git_branch_parse_name
|
|
483
|
+
assert_equal "master", RuVim::Git::Branch.parse_branch_name("* master\t2025-03-06\tInitial")
|
|
484
|
+
assert_equal "feature", RuVim::Git::Branch.parse_branch_name(" feature\t2025-03-05\tAdd feature")
|
|
485
|
+
assert_nil RuVim::Git::Branch.parse_branch_name("")
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def test_git_branch_enter_checks_out
|
|
489
|
+
Dir.mktmpdir do |dir|
|
|
490
|
+
setup_git_repo(dir, "test_file.txt", "line1\n")
|
|
491
|
+
# Create a second branch
|
|
492
|
+
Dir.chdir(dir) do
|
|
493
|
+
system("git branch test-branch", exception: true)
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
file_path = File.join(dir, "test_file.txt")
|
|
497
|
+
buf = @editor.add_buffer_from_file(file_path)
|
|
498
|
+
@editor.switch_to_buffer(buf.id)
|
|
499
|
+
|
|
500
|
+
@dispatcher.dispatch_ex(@editor, "git branch")
|
|
501
|
+
assert_equal :git_branch, @editor.current_buffer.kind
|
|
502
|
+
|
|
503
|
+
branch_buf = @editor.current_buffer
|
|
504
|
+
target_line = branch_buf.lines.index { |l| l.include?("test-branch") }
|
|
505
|
+
assert target_line, "Expected test-branch in branch output"
|
|
506
|
+
@editor.current_window.cursor_y = target_line
|
|
507
|
+
|
|
508
|
+
feed(:enter)
|
|
509
|
+
|
|
510
|
+
# Branch buffer should be closed, and we should have switched
|
|
511
|
+
refute_equal :git_branch, @editor.current_buffer.kind
|
|
512
|
+
# Verify checkout happened
|
|
513
|
+
Dir.chdir(dir) do
|
|
514
|
+
current = `git rev-parse --abbrev-ref HEAD`.strip
|
|
515
|
+
assert_equal "test-branch", current
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# --- GitCommit ---
|
|
521
|
+
|
|
522
|
+
def test_git_commit_prepare
|
|
523
|
+
Dir.mktmpdir do |dir|
|
|
524
|
+
setup_git_repo(dir, "test_file.txt", "line1\n")
|
|
525
|
+
File.write(File.join(dir, "test_file.txt"), "modified\n")
|
|
526
|
+
Dir.chdir(dir) { system("git add test_file.txt", exception: true) }
|
|
527
|
+
|
|
528
|
+
lines, root, err = RuVim::Git::Commit.prepare(File.join(dir, "test_file.txt"))
|
|
529
|
+
assert_nil err
|
|
530
|
+
assert_equal dir, File.realpath(root)
|
|
531
|
+
assert_equal "", lines.first # Empty line for message
|
|
532
|
+
assert lines.any? { |l| l.start_with?("#") }
|
|
533
|
+
end
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def test_git_commit_extract_message
|
|
537
|
+
lines = ["Fix the bug", "", "Detailed description", "# comment line", "# another"]
|
|
538
|
+
msg = RuVim::Git::Commit.extract_message(lines)
|
|
539
|
+
assert_equal "Fix the bug\n\nDetailed description", msg
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def test_git_commit_extract_message_empty
|
|
543
|
+
lines = ["# comment only", "# another"]
|
|
544
|
+
msg = RuVim::Git::Commit.extract_message(lines)
|
|
545
|
+
assert_equal "", msg
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def test_git_commit_opens_buffer
|
|
549
|
+
Dir.mktmpdir do |dir|
|
|
550
|
+
setup_git_repo(dir, "test_file.txt", "line1\n")
|
|
551
|
+
|
|
552
|
+
file_path = File.join(dir, "test_file.txt")
|
|
553
|
+
buf = @editor.add_buffer_from_file(file_path)
|
|
554
|
+
@editor.switch_to_buffer(buf.id)
|
|
555
|
+
|
|
556
|
+
@dispatcher.dispatch_ex(@editor, "git commit")
|
|
557
|
+
|
|
558
|
+
commit_buf = @editor.current_buffer
|
|
559
|
+
assert_equal :git_commit, commit_buf.kind
|
|
560
|
+
refute commit_buf.readonly?
|
|
561
|
+
assert commit_buf.modifiable?
|
|
562
|
+
assert_equal :insert, @editor.mode
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def test_git_commit_via_write
|
|
567
|
+
Dir.mktmpdir do |dir|
|
|
568
|
+
setup_git_repo(dir, "test_file.txt", "line1\n")
|
|
569
|
+
File.write(File.join(dir, "test_file.txt"), "modified\n")
|
|
570
|
+
Dir.chdir(dir) { system("git add test_file.txt", exception: true) }
|
|
571
|
+
|
|
572
|
+
file_path = File.join(dir, "test_file.txt")
|
|
573
|
+
buf = @editor.add_buffer_from_file(file_path)
|
|
574
|
+
@editor.switch_to_buffer(buf.id)
|
|
575
|
+
|
|
576
|
+
@dispatcher.dispatch_ex(@editor, "git commit")
|
|
577
|
+
assert_equal :git_commit, @editor.current_buffer.kind
|
|
578
|
+
|
|
579
|
+
# Type a commit message on the first line
|
|
580
|
+
commit_buf = @editor.current_buffer
|
|
581
|
+
commit_buf.lines[0] = "Test commit message"
|
|
582
|
+
|
|
583
|
+
# :w should trigger the commit
|
|
584
|
+
@dispatcher.dispatch_ex(@editor, "w")
|
|
585
|
+
|
|
586
|
+
# Commit buffer should be closed
|
|
587
|
+
refute_equal :git_commit, @editor.current_buffer.kind
|
|
588
|
+
|
|
589
|
+
# Verify commit happened
|
|
590
|
+
Dir.chdir(dir) do
|
|
591
|
+
log = `git log --oneline -1`.strip
|
|
592
|
+
assert_includes log, "Test commit message"
|
|
593
|
+
end
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def test_git_commit_empty_message_aborts
|
|
598
|
+
Dir.mktmpdir do |dir|
|
|
599
|
+
setup_git_repo(dir, "test_file.txt", "line1\n")
|
|
600
|
+
|
|
601
|
+
file_path = File.join(dir, "test_file.txt")
|
|
602
|
+
buf = @editor.add_buffer_from_file(file_path)
|
|
603
|
+
@editor.switch_to_buffer(buf.id)
|
|
604
|
+
|
|
605
|
+
@dispatcher.dispatch_ex(@editor, "git commit")
|
|
606
|
+
assert_equal :git_commit, @editor.current_buffer.kind
|
|
607
|
+
|
|
608
|
+
# Don't type anything, just try to write
|
|
609
|
+
@dispatcher.dispatch_ex(@editor, "w")
|
|
610
|
+
|
|
611
|
+
# Should still be on commit buffer (abort didn't close it)
|
|
612
|
+
assert_equal :git_commit, @editor.current_buffer.kind
|
|
613
|
+
assert_match(/Empty commit message/, @editor.message)
|
|
614
|
+
end
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
# --- Close with Esc/C-c ---
|
|
618
|
+
|
|
619
|
+
def test_esc_closes_git_blame_buffer
|
|
620
|
+
Dir.mktmpdir do |dir|
|
|
621
|
+
setup_git_repo(dir, "test_file.txt", "line1\n")
|
|
622
|
+
|
|
623
|
+
file_path = File.join(dir, "test_file.txt")
|
|
624
|
+
buf = @editor.add_buffer_from_file(file_path)
|
|
625
|
+
original_buf_id = buf.id
|
|
626
|
+
@editor.switch_to_buffer(buf.id)
|
|
627
|
+
|
|
628
|
+
@dispatcher.dispatch_ex(@editor, "git blame")
|
|
629
|
+
assert_equal :blame, @editor.current_buffer.kind
|
|
630
|
+
|
|
631
|
+
feed(:escape)
|
|
632
|
+
assert_equal original_buf_id, @editor.current_buffer.id
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def test_esc_closes_git_status_buffer
|
|
637
|
+
Dir.mktmpdir do |dir|
|
|
638
|
+
setup_git_repo(dir, "test_file.txt", "line1\n")
|
|
639
|
+
|
|
640
|
+
file_path = File.join(dir, "test_file.txt")
|
|
641
|
+
buf = @editor.add_buffer_from_file(file_path)
|
|
642
|
+
original_buf_id = buf.id
|
|
643
|
+
@editor.switch_to_buffer(buf.id)
|
|
644
|
+
|
|
645
|
+
@dispatcher.dispatch_ex(@editor, "git status")
|
|
646
|
+
assert_equal :git_status, @editor.current_buffer.kind
|
|
647
|
+
|
|
648
|
+
feed(:escape)
|
|
649
|
+
assert_equal original_buf_id, @editor.current_buffer.id
|
|
650
|
+
end
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
def test_esc_closes_git_log_buffer
|
|
654
|
+
Dir.mktmpdir do |dir|
|
|
655
|
+
setup_git_repo(dir, "test_file.txt", "line1\n")
|
|
656
|
+
|
|
657
|
+
file_path = File.join(dir, "test_file.txt")
|
|
658
|
+
buf = @editor.add_buffer_from_file(file_path)
|
|
659
|
+
original_buf_id = buf.id
|
|
660
|
+
@editor.switch_to_buffer(buf.id)
|
|
661
|
+
|
|
662
|
+
@dispatcher.dispatch_ex(@editor, "git log")
|
|
663
|
+
assert_equal :git_log, @editor.current_buffer.kind
|
|
664
|
+
|
|
665
|
+
feed(:escape)
|
|
666
|
+
assert_equal original_buf_id, @editor.current_buffer.id
|
|
667
|
+
end
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
# --- Error cases ---
|
|
671
|
+
|
|
672
|
+
def test_git_blame_on_non_git_file_shows_error
|
|
673
|
+
Dir.mktmpdir do |dir|
|
|
674
|
+
file_path = File.join(dir, "no_git.txt")
|
|
675
|
+
File.write(file_path, "hello\n")
|
|
676
|
+
|
|
677
|
+
buf = @editor.add_buffer_from_file(file_path)
|
|
678
|
+
@editor.switch_to_buffer(buf.id)
|
|
679
|
+
|
|
680
|
+
@dispatcher.dispatch_ex(@editor, "git blame")
|
|
681
|
+
|
|
682
|
+
# Should remain on original buffer (blame failed)
|
|
683
|
+
assert_equal :file, @editor.current_buffer.kind
|
|
684
|
+
end
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
private
|
|
688
|
+
|
|
689
|
+
def setup_git_repo(dir, filename, content)
|
|
690
|
+
Dir.chdir(dir) do
|
|
691
|
+
system("git init -q .", exception: true)
|
|
692
|
+
system("git config user.email test@test.com", exception: true)
|
|
693
|
+
system("git config user.name Test", exception: true)
|
|
694
|
+
File.write(filename, content)
|
|
695
|
+
system("git add #{filename}", exception: true)
|
|
696
|
+
system("git commit -q -m 'initial' --no-gpg-sign", exception: true)
|
|
697
|
+
end
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
def setup_git_repo_multi_commits(dir, filename)
|
|
701
|
+
Dir.chdir(dir) do
|
|
702
|
+
system("git init -q .", exception: true)
|
|
703
|
+
system("git config user.email test@test.com", exception: true)
|
|
704
|
+
system("git config user.name Test", exception: true)
|
|
705
|
+
File.write(filename, "original line\n")
|
|
706
|
+
system("git add #{filename}", exception: true)
|
|
707
|
+
system("git commit -q -m 'first commit' --no-gpg-sign", exception: true)
|
|
708
|
+
File.write(filename, "modified line\n")
|
|
709
|
+
system("git add #{filename}", exception: true)
|
|
710
|
+
system("git commit -q -m 'second commit' --no-gpg-sign", exception: true)
|
|
711
|
+
end
|
|
712
|
+
end
|
|
713
|
+
end
|