mbeditor 0.3.8 → 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/CHANGELOG.md +22 -0
- data/app/assets/javascripts/mbeditor/application_iife_head.js +7 -0
- data/app/assets/javascripts/mbeditor/components/CodeReviewPanel.js +1 -1
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +52 -11
- data/app/assets/javascripts/mbeditor/components/GitPanel.js +14 -4
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +499 -149
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +39 -1
- data/app/assets/javascripts/mbeditor/components/TabBar.js +3 -2
- data/app/assets/javascripts/mbeditor/editor_store.js +10 -2
- data/app/assets/javascripts/mbeditor/file_service.js +23 -23
- data/app/assets/javascripts/mbeditor/git_service.js +7 -11
- data/app/assets/javascripts/mbeditor/search_service.js +45 -12
- data/app/assets/javascripts/mbeditor/tab_manager.js +3 -3
- data/app/assets/stylesheets/mbeditor/editor.css +89 -6
- data/app/controllers/mbeditor/editors_controller.rb +139 -128
- data/app/controllers/mbeditor/git_controller.rb +5 -40
- data/app/services/mbeditor/git_blame_service.rb +6 -0
- data/app/services/mbeditor/git_commit_graph_service.rb +2 -0
- data/app/services/mbeditor/git_service.rb +97 -28
- data/app/services/mbeditor/redmine_service.rb +7 -0
- data/app/services/mbeditor/ruby_definition_service.rb +23 -2
- data/lib/mbeditor/configuration.rb +7 -1
- data/lib/mbeditor/engine.rb +27 -0
- data/lib/mbeditor/version.rb +3 -1
- data/lib/mbeditor.rb +2 -0
- metadata +2 -2
|
@@ -15,7 +15,6 @@ module Mbeditor
|
|
|
15
15
|
IMAGE_EXTENSIONS = %w[png jpg jpeg gif svg ico webp bmp avif].freeze
|
|
16
16
|
MAX_OPEN_FILE_SIZE_BYTES = 5 * 1024 * 1024
|
|
17
17
|
RG_AVAILABLE = system("which rg > /dev/null 2>&1")
|
|
18
|
-
RUBOCOP_TIMEOUT_SECONDS = 15
|
|
19
18
|
|
|
20
19
|
# GET /mbeditor — renders the IDE shell
|
|
21
20
|
def index
|
|
@@ -59,14 +58,27 @@ module Mbeditor
|
|
|
59
58
|
end
|
|
60
59
|
rescue Errno::ENOENT
|
|
61
60
|
render json: {}
|
|
61
|
+
rescue JSON::ParserError
|
|
62
|
+
render json: {}
|
|
62
63
|
rescue StandardError => e
|
|
63
64
|
render json: { error: e.message }, status: :unprocessable_content
|
|
64
65
|
end
|
|
65
66
|
|
|
67
|
+
STATE_MAX_BYTES = 1 * 1024 * 1024
|
|
68
|
+
|
|
66
69
|
# POST /mbeditor/state — save workspace state
|
|
67
70
|
def save_state
|
|
71
|
+
payload = params[:state].to_json
|
|
72
|
+
return render json: { error: "State payload too large" }, status: :content_too_large if payload.bytesize > STATE_MAX_BYTES
|
|
73
|
+
|
|
68
74
|
path = workspace_root.join("tmp", "mbeditor_workspace.json")
|
|
69
|
-
|
|
75
|
+
FileUtils.mkdir_p(workspace_root.join("tmp"))
|
|
76
|
+
File.open(path, File::RDWR | File::CREAT) do |f|
|
|
77
|
+
f.flock(File::LOCK_EX)
|
|
78
|
+
f.truncate(0)
|
|
79
|
+
f.rewind
|
|
80
|
+
f.write(payload)
|
|
81
|
+
end
|
|
70
82
|
render json: { ok: true }
|
|
71
83
|
rescue StandardError => e
|
|
72
84
|
render json: { error: e.message }, status: :unprocessable_content
|
|
@@ -93,11 +105,19 @@ module Mbeditor
|
|
|
93
105
|
branch = sanitize_branch_name(params[:branch])
|
|
94
106
|
return render json: { error: "Invalid branch name" }, status: :bad_request unless branch
|
|
95
107
|
|
|
108
|
+
payload = params[:state].to_unsafe_h
|
|
109
|
+
return render json: { error: "State payload too large" }, status: :content_too_large if payload.to_json.bytesize > STATE_MAX_BYTES
|
|
110
|
+
|
|
96
111
|
path = workspace_root.join("tmp", "mbeditor_branch_states.json")
|
|
97
112
|
FileUtils.mkdir_p(workspace_root.join("tmp"))
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
113
|
+
File.open(path, File::RDWR | File::CREAT) do |f|
|
|
114
|
+
f.flock(File::LOCK_EX)
|
|
115
|
+
existing = f.size > 0 ? JSON.parse(f.read) : {}
|
|
116
|
+
existing[branch] = payload
|
|
117
|
+
f.truncate(0)
|
|
118
|
+
f.rewind
|
|
119
|
+
f.write(existing.to_json)
|
|
120
|
+
end
|
|
101
121
|
render json: { ok: true }
|
|
102
122
|
rescue StandardError => e
|
|
103
123
|
render json: { error: e.message }, status: :unprocessable_content
|
|
@@ -113,10 +133,18 @@ module Mbeditor
|
|
|
113
133
|
return render json: { pruned: [] } unless status.success?
|
|
114
134
|
|
|
115
135
|
local_branches = out.split("\n").map(&:strip).reject(&:empty?)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
136
|
+
pruned = []
|
|
137
|
+
File.open(state_path, File::RDWR) do |f|
|
|
138
|
+
f.flock(File::LOCK_EX)
|
|
139
|
+
all = JSON.parse(f.read) rescue {}
|
|
140
|
+
pruned = all.keys - local_branches
|
|
141
|
+
if pruned.any?
|
|
142
|
+
pruned.each { |b| all.delete(b) }
|
|
143
|
+
f.truncate(0)
|
|
144
|
+
f.rewind
|
|
145
|
+
f.write(all.to_json)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
120
148
|
render json: { pruned: pruned }
|
|
121
149
|
rescue StandardError => e
|
|
122
150
|
render json: { error: e.message }, status: :unprocessable_content
|
|
@@ -294,9 +322,20 @@ module Mbeditor
|
|
|
294
322
|
return render json: [] if query.blank?
|
|
295
323
|
return render json: { error: "Query too long" }, status: :bad_request if query.length > 500
|
|
296
324
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
325
|
+
# On first page, count total matches in parallel with fetching results.
|
|
326
|
+
count_thread = offset == 0 ? Thread.new { count_search_results(query) } : nil
|
|
327
|
+
|
|
328
|
+
results = stream_search_results(query, needed)
|
|
329
|
+
has_more = results.length > offset + limit
|
|
330
|
+
response = { results: results[offset, limit] || [], has_more: has_more }
|
|
331
|
+
if count_thread
|
|
332
|
+
# Give the count thread up to 100 ms; omit total_count when it hasn't finished yet
|
|
333
|
+
# so the first page is never blocked by the counting subprocess.
|
|
334
|
+
count_thread.join(0.1)
|
|
335
|
+
response[:total_count] = count_thread.value unless count_thread.alive?
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
render json: response
|
|
300
339
|
rescue StandardError => e
|
|
301
340
|
render json: { error: e.message }, status: :unprocessable_content
|
|
302
341
|
end
|
|
@@ -305,9 +344,7 @@ module Mbeditor
|
|
|
305
344
|
def git_status
|
|
306
345
|
output, _err, status = Open3.capture3("git", "-C", workspace_root.to_s, "status", "--porcelain")
|
|
307
346
|
branch = GitService.current_branch(workspace_root.to_s) || ""
|
|
308
|
-
files = output
|
|
309
|
-
{ status: line[0..1].strip, path: line[3..].strip }
|
|
310
|
-
end
|
|
347
|
+
files = parse_porcelain_status(output)
|
|
311
348
|
render json: { ok: status.success?, files: files, branch: branch }
|
|
312
349
|
rescue StandardError => e
|
|
313
350
|
render json: { error: e.message }, status: :unprocessable_content
|
|
@@ -325,7 +362,7 @@ module Mbeditor
|
|
|
325
362
|
|
|
326
363
|
# Annotate each working-tree file with added/removed line counts
|
|
327
364
|
numstat_out, = Open3.capture3("git", "-C", repo, "diff", "--numstat", "HEAD")
|
|
328
|
-
numstat_map = parse_numstat(numstat_out)
|
|
365
|
+
numstat_map = GitService.parse_numstat(numstat_out)
|
|
329
366
|
working_tree = working_tree.map { |f| f.merge(numstat_map.fetch(f[:path], {})) }
|
|
330
367
|
|
|
331
368
|
upstream_output, _err, upstream_status = Open3.capture3("git", "-C", repo, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}")
|
|
@@ -339,7 +376,7 @@ module Mbeditor
|
|
|
339
376
|
|
|
340
377
|
# Determine the branch's fork point relative to a base branch (develop/main/master).
|
|
341
378
|
# This ensures History and Changes only show work unique to this branch.
|
|
342
|
-
base_sha, base_ref = find_branch_base(repo, branch)
|
|
379
|
+
base_sha, base_ref = GitService.find_branch_base(repo, branch)
|
|
343
380
|
|
|
344
381
|
if upstream_branch.present?
|
|
345
382
|
counts_output, _err, counts_status = Open3.capture3("git", "-C", repo, "rev-list", "--left-right", "--count", "HEAD...#{upstream_branch}")
|
|
@@ -350,7 +387,7 @@ module Mbeditor
|
|
|
350
387
|
end
|
|
351
388
|
|
|
352
389
|
unpushed_log_output, _err, unpushed_log_status = Open3.capture3("git", "-C", repo, "log", "#{upstream_branch}..HEAD", "--pretty=format:%H%x1f%s%x1f%an%x1f%aI%x1e")
|
|
353
|
-
unpushed_commits = parse_git_log(unpushed_log_output) if unpushed_log_status.success?
|
|
390
|
+
unpushed_commits = GitService.parse_git_log(unpushed_log_output) if unpushed_log_status.success?
|
|
354
391
|
end
|
|
355
392
|
|
|
356
393
|
# "Changes in Branch" — use the merge-base against the base branch when available
|
|
@@ -361,7 +398,7 @@ module Mbeditor
|
|
|
361
398
|
if unpushed_status.success?
|
|
362
399
|
unpushed_files = parse_name_status(unpushed_output)
|
|
363
400
|
unp_numstat_out, = Open3.capture3("git", "-C", repo, "diff", "--numstat", "#{diff_base}..HEAD")
|
|
364
|
-
unp_numstat_map = parse_numstat(unp_numstat_out)
|
|
401
|
+
unp_numstat_map = GitService.parse_numstat(unp_numstat_out)
|
|
365
402
|
unpushed_files = unpushed_files.map { |f| f.merge(unp_numstat_map.fetch(f[:path], {})) }
|
|
366
403
|
end
|
|
367
404
|
end
|
|
@@ -373,7 +410,7 @@ module Mbeditor
|
|
|
373
410
|
Open3.capture3("git", "-C", repo, "log", "--first-parent", branch, "-n", "100",
|
|
374
411
|
"--pretty=format:%H%x1f%s%x1f%an%x1f%aI%x1e")
|
|
375
412
|
end
|
|
376
|
-
branch_commits = branch_log_status.success? ? parse_git_log(branch_log_output) : []
|
|
413
|
+
branch_commits = branch_log_status.success? ? GitService.parse_git_log(branch_log_output) : []
|
|
377
414
|
|
|
378
415
|
redmine_ticket_id = nil
|
|
379
416
|
if Mbeditor.configuration.redmine_enabled
|
|
@@ -382,7 +419,7 @@ module Mbeditor
|
|
|
382
419
|
redmine_ticket_id = m[1] if m
|
|
383
420
|
else
|
|
384
421
|
branch_commits.each do |commit|
|
|
385
|
-
m = commit[
|
|
422
|
+
m = commit["title"]&.match(/#(\d+)/)
|
|
386
423
|
if m
|
|
387
424
|
redmine_ticket_id = m[1]
|
|
388
425
|
break
|
|
@@ -545,9 +582,10 @@ module Mbeditor
|
|
|
545
582
|
|
|
546
583
|
# Use a workspace-local tempfile so RuboCop's config discovery walks up
|
|
547
584
|
# from the source file's directory and finds the host app's .rubocop.yml.
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
585
|
+
Tempfile.create([".mbeditor_fix_", ext], File.dirname(path)) do |f|
|
|
586
|
+
f.write(code)
|
|
587
|
+
f.flush
|
|
588
|
+
tmpfile = f.path
|
|
551
589
|
|
|
552
590
|
cmd = rubocop_command + ["--no-server", "--cache", "false", "-A", "--no-color", tmpfile]
|
|
553
591
|
env = { 'RUBOCOP_CACHE_ROOT' => File.join(Dir.tmpdir, 'rubocop') }
|
|
@@ -561,8 +599,6 @@ module Mbeditor
|
|
|
561
599
|
corrected = File.read(tmpfile, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
562
600
|
fix = compute_text_edit(code, corrected)
|
|
563
601
|
render json: { fix: fix }
|
|
564
|
-
ensure
|
|
565
|
-
File.delete(tmpfile) if tmpfile && File.exist?(tmpfile)
|
|
566
602
|
end
|
|
567
603
|
rescue StandardError => e
|
|
568
604
|
render json: { error: e.message }, status: :unprocessable_content
|
|
@@ -608,10 +644,12 @@ module Mbeditor
|
|
|
608
644
|
code = params[:code].to_s
|
|
609
645
|
return render json: { error: "code required" }, status: :unprocessable_content if code.empty?
|
|
610
646
|
|
|
611
|
-
ext
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
647
|
+
ext = File.extname(File.basename(path))
|
|
648
|
+
Tempfile.create([".mbeditor_fmt_", ext], File.dirname(path)) do |f|
|
|
649
|
+
f.write(code)
|
|
650
|
+
f.flush
|
|
651
|
+
tmpfile = f.path
|
|
652
|
+
|
|
615
653
|
cmd = rubocop_command + ["--no-server", "--cache", "false", "-A", "--no-color", tmpfile]
|
|
616
654
|
env = { 'RUBOCOP_CACHE_ROOT' => File.join(Dir.tmpdir, 'rubocop') }
|
|
617
655
|
_out, _err, status = Open3.capture3(env, *cmd)
|
|
@@ -621,8 +659,6 @@ module Mbeditor
|
|
|
621
659
|
|
|
622
660
|
corrected = File.read(tmpfile, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
623
661
|
render json: { ok: true, content: corrected }
|
|
624
|
-
ensure
|
|
625
|
-
File.delete(tmpfile) if tmpfile && File.exist?(tmpfile)
|
|
626
662
|
end
|
|
627
663
|
rescue StandardError => e
|
|
628
664
|
render json: { error: e.message }, status: :unprocessable_content
|
|
@@ -679,7 +715,8 @@ module Mbeditor
|
|
|
679
715
|
|
|
680
716
|
begin
|
|
681
717
|
data = JSON.parse(raw)
|
|
682
|
-
rescue JSON::ParserError
|
|
718
|
+
rescue JSON::ParserError => e
|
|
719
|
+
Rails.logger.warn("[mbeditor] search: malformed rg JSON line: #{e.message}")
|
|
683
720
|
next
|
|
684
721
|
end
|
|
685
722
|
next unless data["type"] == "match"
|
|
@@ -694,7 +731,7 @@ module Mbeditor
|
|
|
694
731
|
end
|
|
695
732
|
else
|
|
696
733
|
args = ["grep", "-rn", "-F"]
|
|
697
|
-
excluded_dirnames.each { |d| args << "--exclude-dir=#{d}" }
|
|
734
|
+
excluded_dirnames.select { |d| d.match?(/\A[\w.\/-]+\z/) }.each { |d| args << "--exclude-dir=#{d}" }
|
|
698
735
|
args += [query, workspace_root.to_s]
|
|
699
736
|
|
|
700
737
|
IO.popen(args, err: File::NULL) do |io|
|
|
@@ -722,6 +759,30 @@ module Mbeditor
|
|
|
722
759
|
results
|
|
723
760
|
end
|
|
724
761
|
|
|
762
|
+
# Count total matching lines across the workspace using rg --count (or grep -c).
|
|
763
|
+
# Fast: rg just counts without extracting context. Runs in a background thread.
|
|
764
|
+
def count_search_results(query)
|
|
765
|
+
total = 0
|
|
766
|
+
if RG_AVAILABLE
|
|
767
|
+
args = ["rg", "--count", "--no-ignore"]
|
|
768
|
+
excluded_paths.each { |p| args << "--glob=!#{p}" }
|
|
769
|
+
args += ["--", query, workspace_root.to_s]
|
|
770
|
+
IO.popen(args, err: File::NULL) do |io|
|
|
771
|
+
io.each_line { |line| total += line.strip.split(":").last.to_i rescue 0 }
|
|
772
|
+
end
|
|
773
|
+
else
|
|
774
|
+
args = ["grep", "-rc", "-F"]
|
|
775
|
+
excluded_dirnames.each { |d| args << "--exclude-dir=#{d}" }
|
|
776
|
+
args += [query, workspace_root.to_s]
|
|
777
|
+
IO.popen(args, err: File::NULL) do |io|
|
|
778
|
+
io.each_line { |line| total += line.strip.split(":").last.to_i rescue 0 }
|
|
779
|
+
end
|
|
780
|
+
end
|
|
781
|
+
total
|
|
782
|
+
rescue StandardError
|
|
783
|
+
0
|
|
784
|
+
end
|
|
785
|
+
|
|
725
786
|
def build_tree(dir, max_depth: 10, depth: 0)
|
|
726
787
|
return [] if depth >= max_depth
|
|
727
788
|
|
|
@@ -761,27 +822,29 @@ module Mbeditor
|
|
|
761
822
|
end
|
|
762
823
|
|
|
763
824
|
def run_with_timeout(env, cmd, stdin_data:)
|
|
764
|
-
|
|
765
|
-
timed_out = false
|
|
825
|
+
timeout_seconds = Mbeditor.configuration.lint_timeout&.to_i
|
|
826
|
+
output = +""; timed_out = false
|
|
766
827
|
|
|
767
828
|
Open3.popen3(env, *cmd, pgroup: true) do |stdin, stdout, _stderr, wait_thr|
|
|
768
829
|
stdin.write(stdin_data)
|
|
769
830
|
stdin.close
|
|
770
831
|
|
|
771
|
-
timer =
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
832
|
+
timer = if timeout_seconds && timeout_seconds > 0
|
|
833
|
+
Thread.new do
|
|
834
|
+
sleep timeout_seconds
|
|
835
|
+
timed_out = true
|
|
836
|
+
Process.kill('-KILL', wait_thr.pid)
|
|
837
|
+
rescue Errno::ESRCH
|
|
838
|
+
nil
|
|
839
|
+
end
|
|
777
840
|
end
|
|
778
841
|
|
|
779
842
|
output = stdout.read
|
|
780
843
|
wait_thr.value
|
|
781
|
-
timer
|
|
844
|
+
timer&.kill
|
|
782
845
|
end
|
|
783
846
|
|
|
784
|
-
raise "RuboCop timed out after #{
|
|
847
|
+
raise "RuboCop timed out after #{timeout_seconds} seconds" if timed_out
|
|
785
848
|
|
|
786
849
|
output
|
|
787
850
|
end
|
|
@@ -856,48 +919,46 @@ module Mbeditor
|
|
|
856
919
|
candidate.exist? ? ".rubocop.yml" : nil
|
|
857
920
|
end
|
|
858
921
|
|
|
922
|
+
PROBE_MUTEX = Mutex.new
|
|
923
|
+
private_constant :PROBE_MUTEX
|
|
924
|
+
|
|
859
925
|
def rubocop_available?
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
key = Mbeditor.configuration.rubocop_command.to_s
|
|
863
|
-
cache = self.class.instance_variable_get(:@rubocop_available_cache) ||
|
|
864
|
-
self.class.instance_variable_set(:@rubocop_available_cache, {})
|
|
865
|
-
return cache[key] if cache.key?(key)
|
|
866
|
-
cache[key] = begin
|
|
926
|
+
key = Mbeditor.configuration.rubocop_command.to_s
|
|
927
|
+
probe_cached(:@rubocop_available_cache, key) do
|
|
867
928
|
_out, _err, status = Open3.capture3(*rubocop_command, "--version")
|
|
868
929
|
status.success?
|
|
869
|
-
rescue StandardError
|
|
870
|
-
false
|
|
871
930
|
end
|
|
872
931
|
end
|
|
873
932
|
|
|
874
933
|
def haml_lint_available?
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
key = cmd.join(" ")
|
|
879
|
-
cache = self.class.instance_variable_get(:@haml_lint_available_cache) ||
|
|
880
|
-
self.class.instance_variable_set(:@haml_lint_available_cache, {})
|
|
881
|
-
return cache[key] if cache.key?(key)
|
|
882
|
-
cache[key] = begin
|
|
934
|
+
cmd = haml_lint_command
|
|
935
|
+
key = cmd.join(" ")
|
|
936
|
+
probe_cached(:@haml_lint_available_cache, key) do
|
|
883
937
|
_out, _err, status = Open3.capture3(*cmd, "--version")
|
|
884
938
|
status.success?
|
|
885
|
-
rescue StandardError
|
|
886
|
-
false
|
|
887
939
|
end
|
|
888
940
|
end
|
|
889
941
|
|
|
890
942
|
def git_available?
|
|
891
|
-
|
|
892
|
-
key
|
|
893
|
-
cache = self.class.instance_variable_get(:@git_available_cache) ||
|
|
894
|
-
self.class.instance_variable_set(:@git_available_cache, {})
|
|
895
|
-
return cache[key] if cache.key?(key)
|
|
896
|
-
cache[key] = begin
|
|
943
|
+
key = workspace_root.to_s
|
|
944
|
+
probe_cached(:@git_available_cache, key) do
|
|
897
945
|
_out, _err, status = Open3.capture3("git", "-C", key, "rev-parse", "--is-inside-work-tree")
|
|
898
946
|
status.success?
|
|
899
|
-
|
|
900
|
-
|
|
947
|
+
end
|
|
948
|
+
end
|
|
949
|
+
|
|
950
|
+
def probe_cached(ivar, key, &block)
|
|
951
|
+
PROBE_MUTEX.synchronize do
|
|
952
|
+
cache = self.class.instance_variable_get(ivar) ||
|
|
953
|
+
self.class.instance_variable_set(ivar, {})
|
|
954
|
+
unless cache.key?(key)
|
|
955
|
+
cache[key] = begin
|
|
956
|
+
block.call
|
|
957
|
+
rescue StandardError
|
|
958
|
+
false
|
|
959
|
+
end
|
|
960
|
+
end
|
|
961
|
+
cache[key]
|
|
901
962
|
end
|
|
902
963
|
end
|
|
903
964
|
|
|
@@ -952,8 +1013,13 @@ module Mbeditor
|
|
|
952
1013
|
end
|
|
953
1014
|
|
|
954
1015
|
def parse_porcelain_status(output)
|
|
955
|
-
output.lines.
|
|
956
|
-
|
|
1016
|
+
output.lines.filter_map do |line|
|
|
1017
|
+
next if line.length < 4
|
|
1018
|
+
|
|
1019
|
+
path = line[3..].to_s.strip
|
|
1020
|
+
next if path.blank?
|
|
1021
|
+
|
|
1022
|
+
{ status: line[0..1].strip, path: path }
|
|
957
1023
|
end
|
|
958
1024
|
end
|
|
959
1025
|
|
|
@@ -970,29 +1036,6 @@ module Mbeditor
|
|
|
970
1036
|
end
|
|
971
1037
|
end
|
|
972
1038
|
|
|
973
|
-
def parse_numstat(output)
|
|
974
|
-
(output || "").lines.each_with_object({}) do |line, map|
|
|
975
|
-
parts = line.strip.split("\t", 3)
|
|
976
|
-
next if parts.length < 3 || parts[0] == "-"
|
|
977
|
-
|
|
978
|
-
map[parts[2].strip] = { added: parts[0].to_i, removed: parts[1].to_i }
|
|
979
|
-
end
|
|
980
|
-
end
|
|
981
|
-
|
|
982
|
-
def parse_git_log(output)
|
|
983
|
-
output.split("\x1e").filter_map do |entry|
|
|
984
|
-
fields = entry.strip.split("\x1f", 4)
|
|
985
|
-
next unless fields.length == 4
|
|
986
|
-
|
|
987
|
-
{
|
|
988
|
-
hash: fields[0],
|
|
989
|
-
title: fields[1],
|
|
990
|
-
author: fields[2],
|
|
991
|
-
date: fields[3]
|
|
992
|
-
}
|
|
993
|
-
end
|
|
994
|
-
end
|
|
995
|
-
|
|
996
1039
|
def monaco_worker_file
|
|
997
1040
|
engine_path = Mbeditor::Engine.root.join("public", "monaco_worker.js")
|
|
998
1041
|
return engine_path if engine_path.file?
|
|
@@ -1000,38 +1043,6 @@ module Mbeditor
|
|
|
1000
1043
|
Rails.root.join("public", "monaco_worker.js")
|
|
1001
1044
|
end
|
|
1002
1045
|
|
|
1003
|
-
# Returns [merge_base_sha, ref_name] of the first candidate base branch found,
|
|
1004
|
-
# or [nil, nil] if none can be determined. Candidates are tried in preference order:
|
|
1005
|
-
# origin/develop → origin/main → origin/master → develop → main → master.
|
|
1006
|
-
# Skips refs that ARE the current branch and refs where the merge-base equals HEAD
|
|
1007
|
-
# (meaning the current branch is behind or at the same point as that ref).
|
|
1008
|
-
def find_branch_base(repo, current_branch)
|
|
1009
|
-
candidates = %w[origin/develop origin/main origin/master develop main master]
|
|
1010
|
-
head_sha_out, = Open3.capture3("git", "-C", repo, "rev-parse", "HEAD")
|
|
1011
|
-
head_sha = head_sha_out.strip
|
|
1012
|
-
|
|
1013
|
-
candidates.each do |ref|
|
|
1014
|
-
short = ref.delete_prefix("origin/")
|
|
1015
|
-
next if short == current_branch || ref == current_branch
|
|
1016
|
-
|
|
1017
|
-
_o, _e, st = Open3.capture3("git", "-C", repo, "rev-parse", "--verify", "--quiet", ref)
|
|
1018
|
-
next unless st.success?
|
|
1019
|
-
|
|
1020
|
-
base_out, _e, base_st = Open3.capture3("git", "-C", repo, "merge-base", "HEAD", ref)
|
|
1021
|
-
next unless base_st.success?
|
|
1022
|
-
|
|
1023
|
-
sha = base_out.strip
|
|
1024
|
-
next unless sha.match?(/\A[0-9a-f]{40}\z/)
|
|
1025
|
-
next if sha == head_sha # branch is at/behind this ref — no unique commits
|
|
1026
|
-
|
|
1027
|
-
return [sha, ref]
|
|
1028
|
-
end
|
|
1029
|
-
|
|
1030
|
-
[nil, nil]
|
|
1031
|
-
rescue StandardError
|
|
1032
|
-
[nil, nil]
|
|
1033
|
-
end
|
|
1034
|
-
|
|
1035
1046
|
def resolve_monaco_asset_path(asset_path)
|
|
1036
1047
|
return nil if asset_path.blank?
|
|
1037
1048
|
|
|
@@ -26,7 +26,8 @@ module Mbeditor
|
|
|
26
26
|
head = nil if head == 'WORKING'
|
|
27
27
|
# Allow full/short SHA hashes plus common git ref formats: branch names,
|
|
28
28
|
# HEAD, remote tracking refs, parent notation (sha^, sha~N) and tags.
|
|
29
|
-
|
|
29
|
+
# @ is excluded to block reflog syntax like @{-1} or HEAD@{2}.
|
|
30
|
+
valid_ref = /\A[a-zA-Z0-9._\-\/\^~]+\z/
|
|
30
31
|
if [base, head].any? { |s| s && (s.length > 200 || !s.match?(valid_ref)) }
|
|
31
32
|
return render json: { error: 'Invalid ref' }, status: :bad_request
|
|
32
33
|
end
|
|
@@ -88,15 +89,7 @@ module Mbeditor
|
|
|
88
89
|
"diff-tree", "--no-commit-id", "-r", "--numstat", sha
|
|
89
90
|
)
|
|
90
91
|
|
|
91
|
-
numstat_map = {}
|
|
92
|
-
if numstat_status.success?
|
|
93
|
-
numstat_output.lines.each do |line|
|
|
94
|
-
parts = line.strip.split("\t", 3)
|
|
95
|
-
next if parts.length < 3 || parts[0] == "-"
|
|
96
|
-
|
|
97
|
-
numstat_map[parts[2].strip] = { "added" => parts[0].to_i, "removed" => parts[1].to_i }
|
|
98
|
-
end
|
|
99
|
-
end
|
|
92
|
+
numstat_map = numstat_status.success? ? GitService.parse_numstat(numstat_output) : {}
|
|
100
93
|
|
|
101
94
|
files = []
|
|
102
95
|
if files_status.success?
|
|
@@ -136,7 +129,7 @@ module Mbeditor
|
|
|
136
129
|
else
|
|
137
130
|
repo = workspace_root.to_s
|
|
138
131
|
branch = GitService.current_branch(repo)
|
|
139
|
-
base_sha, = find_branch_base(repo, branch)
|
|
132
|
+
base_sha, = GitService.find_branch_base(repo, branch)
|
|
140
133
|
|
|
141
134
|
if base_sha.present?
|
|
142
135
|
out, _err, status = Open3.capture3("git", "-C", repo, "diff", "#{base_sha}..HEAD")
|
|
@@ -153,7 +146,7 @@ module Mbeditor
|
|
|
153
146
|
out, _err, status = Open3.capture3("git", "-C", repo, "diff", "#{upstream}..HEAD")
|
|
154
147
|
out = status.success? ? out : ""
|
|
155
148
|
else
|
|
156
|
-
|
|
149
|
+
return render plain: "", content_type: "text/plain"
|
|
157
150
|
end
|
|
158
151
|
end
|
|
159
152
|
end
|
|
@@ -202,33 +195,5 @@ module Mbeditor
|
|
|
202
195
|
relative_path(full)
|
|
203
196
|
end
|
|
204
197
|
|
|
205
|
-
# Returns [merge_base_sha, ref_name] of the first candidate base branch found,
|
|
206
|
-
# or [nil, nil] if none can be determined.
|
|
207
|
-
def find_branch_base(repo, current_branch)
|
|
208
|
-
candidates = %w[origin/develop origin/main origin/master develop main master]
|
|
209
|
-
head_sha_out, = Open3.capture3("git", "-C", repo, "rev-parse", "HEAD")
|
|
210
|
-
head_sha = head_sha_out.strip
|
|
211
|
-
|
|
212
|
-
candidates.each do |ref|
|
|
213
|
-
short = ref.delete_prefix("origin/")
|
|
214
|
-
next if short == current_branch || ref == current_branch
|
|
215
|
-
|
|
216
|
-
_o, _e, st = Open3.capture3("git", "-C", repo, "rev-parse", "--verify", "--quiet", ref)
|
|
217
|
-
next unless st.success?
|
|
218
|
-
|
|
219
|
-
base_out, _e, base_st = Open3.capture3("git", "-C", repo, "merge-base", "HEAD", ref)
|
|
220
|
-
next unless base_st.success?
|
|
221
|
-
|
|
222
|
-
sha = base_out.strip
|
|
223
|
-
next unless sha.match?(/\A[0-9a-f]{40}\z/)
|
|
224
|
-
next if sha == head_sha
|
|
225
|
-
|
|
226
|
-
return [sha, ref]
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
[nil, nil]
|
|
230
|
-
rescue StandardError
|
|
231
|
-
[nil, nil]
|
|
232
|
-
end
|
|
233
198
|
end
|
|
234
199
|
end
|