mbeditor 0.3.7 → 0.3.9
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 +18 -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 +316 -133
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +25 -1
- data/app/assets/javascripts/mbeditor/editor_plugins.js +3 -3
- 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 +12 -5
- data/app/controllers/mbeditor/editors_controller.rb +134 -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,15 @@ 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
|
+
response[:total_count] = count_thread.value if count_thread
|
|
332
|
+
|
|
333
|
+
render json: response
|
|
300
334
|
rescue StandardError => e
|
|
301
335
|
render json: { error: e.message }, status: :unprocessable_content
|
|
302
336
|
end
|
|
@@ -305,9 +339,7 @@ module Mbeditor
|
|
|
305
339
|
def git_status
|
|
306
340
|
output, _err, status = Open3.capture3("git", "-C", workspace_root.to_s, "status", "--porcelain")
|
|
307
341
|
branch = GitService.current_branch(workspace_root.to_s) || ""
|
|
308
|
-
files = output
|
|
309
|
-
{ status: line[0..1].strip, path: line[3..].strip }
|
|
310
|
-
end
|
|
342
|
+
files = parse_porcelain_status(output)
|
|
311
343
|
render json: { ok: status.success?, files: files, branch: branch }
|
|
312
344
|
rescue StandardError => e
|
|
313
345
|
render json: { error: e.message }, status: :unprocessable_content
|
|
@@ -325,7 +357,7 @@ module Mbeditor
|
|
|
325
357
|
|
|
326
358
|
# Annotate each working-tree file with added/removed line counts
|
|
327
359
|
numstat_out, = Open3.capture3("git", "-C", repo, "diff", "--numstat", "HEAD")
|
|
328
|
-
numstat_map = parse_numstat(numstat_out)
|
|
360
|
+
numstat_map = GitService.parse_numstat(numstat_out)
|
|
329
361
|
working_tree = working_tree.map { |f| f.merge(numstat_map.fetch(f[:path], {})) }
|
|
330
362
|
|
|
331
363
|
upstream_output, _err, upstream_status = Open3.capture3("git", "-C", repo, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}")
|
|
@@ -339,7 +371,7 @@ module Mbeditor
|
|
|
339
371
|
|
|
340
372
|
# Determine the branch's fork point relative to a base branch (develop/main/master).
|
|
341
373
|
# This ensures History and Changes only show work unique to this branch.
|
|
342
|
-
base_sha, base_ref = find_branch_base(repo, branch)
|
|
374
|
+
base_sha, base_ref = GitService.find_branch_base(repo, branch)
|
|
343
375
|
|
|
344
376
|
if upstream_branch.present?
|
|
345
377
|
counts_output, _err, counts_status = Open3.capture3("git", "-C", repo, "rev-list", "--left-right", "--count", "HEAD...#{upstream_branch}")
|
|
@@ -350,7 +382,7 @@ module Mbeditor
|
|
|
350
382
|
end
|
|
351
383
|
|
|
352
384
|
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?
|
|
385
|
+
unpushed_commits = GitService.parse_git_log(unpushed_log_output) if unpushed_log_status.success?
|
|
354
386
|
end
|
|
355
387
|
|
|
356
388
|
# "Changes in Branch" — use the merge-base against the base branch when available
|
|
@@ -361,7 +393,7 @@ module Mbeditor
|
|
|
361
393
|
if unpushed_status.success?
|
|
362
394
|
unpushed_files = parse_name_status(unpushed_output)
|
|
363
395
|
unp_numstat_out, = Open3.capture3("git", "-C", repo, "diff", "--numstat", "#{diff_base}..HEAD")
|
|
364
|
-
unp_numstat_map = parse_numstat(unp_numstat_out)
|
|
396
|
+
unp_numstat_map = GitService.parse_numstat(unp_numstat_out)
|
|
365
397
|
unpushed_files = unpushed_files.map { |f| f.merge(unp_numstat_map.fetch(f[:path], {})) }
|
|
366
398
|
end
|
|
367
399
|
end
|
|
@@ -373,7 +405,7 @@ module Mbeditor
|
|
|
373
405
|
Open3.capture3("git", "-C", repo, "log", "--first-parent", branch, "-n", "100",
|
|
374
406
|
"--pretty=format:%H%x1f%s%x1f%an%x1f%aI%x1e")
|
|
375
407
|
end
|
|
376
|
-
branch_commits = branch_log_status.success? ? parse_git_log(branch_log_output) : []
|
|
408
|
+
branch_commits = branch_log_status.success? ? GitService.parse_git_log(branch_log_output) : []
|
|
377
409
|
|
|
378
410
|
redmine_ticket_id = nil
|
|
379
411
|
if Mbeditor.configuration.redmine_enabled
|
|
@@ -382,7 +414,7 @@ module Mbeditor
|
|
|
382
414
|
redmine_ticket_id = m[1] if m
|
|
383
415
|
else
|
|
384
416
|
branch_commits.each do |commit|
|
|
385
|
-
m = commit[
|
|
417
|
+
m = commit["title"]&.match(/#(\d+)/)
|
|
386
418
|
if m
|
|
387
419
|
redmine_ticket_id = m[1]
|
|
388
420
|
break
|
|
@@ -545,9 +577,10 @@ module Mbeditor
|
|
|
545
577
|
|
|
546
578
|
# Use a workspace-local tempfile so RuboCop's config discovery walks up
|
|
547
579
|
# from the source file's directory and finds the host app's .rubocop.yml.
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
580
|
+
Tempfile.create([".mbeditor_fix_", ext], File.dirname(path)) do |f|
|
|
581
|
+
f.write(code)
|
|
582
|
+
f.flush
|
|
583
|
+
tmpfile = f.path
|
|
551
584
|
|
|
552
585
|
cmd = rubocop_command + ["--no-server", "--cache", "false", "-A", "--no-color", tmpfile]
|
|
553
586
|
env = { 'RUBOCOP_CACHE_ROOT' => File.join(Dir.tmpdir, 'rubocop') }
|
|
@@ -561,8 +594,6 @@ module Mbeditor
|
|
|
561
594
|
corrected = File.read(tmpfile, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
562
595
|
fix = compute_text_edit(code, corrected)
|
|
563
596
|
render json: { fix: fix }
|
|
564
|
-
ensure
|
|
565
|
-
File.delete(tmpfile) if tmpfile && File.exist?(tmpfile)
|
|
566
597
|
end
|
|
567
598
|
rescue StandardError => e
|
|
568
599
|
render json: { error: e.message }, status: :unprocessable_content
|
|
@@ -608,10 +639,12 @@ module Mbeditor
|
|
|
608
639
|
code = params[:code].to_s
|
|
609
640
|
return render json: { error: "code required" }, status: :unprocessable_content if code.empty?
|
|
610
641
|
|
|
611
|
-
ext
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
642
|
+
ext = File.extname(File.basename(path))
|
|
643
|
+
Tempfile.create([".mbeditor_fmt_", ext], File.dirname(path)) do |f|
|
|
644
|
+
f.write(code)
|
|
645
|
+
f.flush
|
|
646
|
+
tmpfile = f.path
|
|
647
|
+
|
|
615
648
|
cmd = rubocop_command + ["--no-server", "--cache", "false", "-A", "--no-color", tmpfile]
|
|
616
649
|
env = { 'RUBOCOP_CACHE_ROOT' => File.join(Dir.tmpdir, 'rubocop') }
|
|
617
650
|
_out, _err, status = Open3.capture3(env, *cmd)
|
|
@@ -621,8 +654,6 @@ module Mbeditor
|
|
|
621
654
|
|
|
622
655
|
corrected = File.read(tmpfile, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
623
656
|
render json: { ok: true, content: corrected }
|
|
624
|
-
ensure
|
|
625
|
-
File.delete(tmpfile) if tmpfile && File.exist?(tmpfile)
|
|
626
657
|
end
|
|
627
658
|
rescue StandardError => e
|
|
628
659
|
render json: { error: e.message }, status: :unprocessable_content
|
|
@@ -679,7 +710,8 @@ module Mbeditor
|
|
|
679
710
|
|
|
680
711
|
begin
|
|
681
712
|
data = JSON.parse(raw)
|
|
682
|
-
rescue JSON::ParserError
|
|
713
|
+
rescue JSON::ParserError => e
|
|
714
|
+
Rails.logger.warn("[mbeditor] search: malformed rg JSON line: #{e.message}")
|
|
683
715
|
next
|
|
684
716
|
end
|
|
685
717
|
next unless data["type"] == "match"
|
|
@@ -694,7 +726,7 @@ module Mbeditor
|
|
|
694
726
|
end
|
|
695
727
|
else
|
|
696
728
|
args = ["grep", "-rn", "-F"]
|
|
697
|
-
excluded_dirnames.each { |d| args << "--exclude-dir=#{d}" }
|
|
729
|
+
excluded_dirnames.select { |d| d.match?(/\A[\w.\/-]+\z/) }.each { |d| args << "--exclude-dir=#{d}" }
|
|
698
730
|
args += [query, workspace_root.to_s]
|
|
699
731
|
|
|
700
732
|
IO.popen(args, err: File::NULL) do |io|
|
|
@@ -722,6 +754,30 @@ module Mbeditor
|
|
|
722
754
|
results
|
|
723
755
|
end
|
|
724
756
|
|
|
757
|
+
# Count total matching lines across the workspace using rg --count (or grep -c).
|
|
758
|
+
# Fast: rg just counts without extracting context. Runs in a background thread.
|
|
759
|
+
def count_search_results(query)
|
|
760
|
+
total = 0
|
|
761
|
+
if RG_AVAILABLE
|
|
762
|
+
args = ["rg", "--count", "--no-ignore"]
|
|
763
|
+
excluded_paths.each { |p| args << "--glob=!#{p}" }
|
|
764
|
+
args += ["--", query, workspace_root.to_s]
|
|
765
|
+
IO.popen(args, err: File::NULL) do |io|
|
|
766
|
+
io.each_line { |line| total += line.strip.split(":").last.to_i rescue 0 }
|
|
767
|
+
end
|
|
768
|
+
else
|
|
769
|
+
args = ["grep", "-rc", "-F"]
|
|
770
|
+
excluded_dirnames.each { |d| args << "--exclude-dir=#{d}" }
|
|
771
|
+
args += [query, workspace_root.to_s]
|
|
772
|
+
IO.popen(args, err: File::NULL) do |io|
|
|
773
|
+
io.each_line { |line| total += line.strip.split(":").last.to_i rescue 0 }
|
|
774
|
+
end
|
|
775
|
+
end
|
|
776
|
+
total
|
|
777
|
+
rescue StandardError
|
|
778
|
+
0
|
|
779
|
+
end
|
|
780
|
+
|
|
725
781
|
def build_tree(dir, max_depth: 10, depth: 0)
|
|
726
782
|
return [] if depth >= max_depth
|
|
727
783
|
|
|
@@ -761,27 +817,29 @@ module Mbeditor
|
|
|
761
817
|
end
|
|
762
818
|
|
|
763
819
|
def run_with_timeout(env, cmd, stdin_data:)
|
|
764
|
-
|
|
765
|
-
timed_out = false
|
|
820
|
+
timeout_seconds = Mbeditor.configuration.lint_timeout&.to_i
|
|
821
|
+
output = +""; timed_out = false
|
|
766
822
|
|
|
767
823
|
Open3.popen3(env, *cmd, pgroup: true) do |stdin, stdout, _stderr, wait_thr|
|
|
768
824
|
stdin.write(stdin_data)
|
|
769
825
|
stdin.close
|
|
770
826
|
|
|
771
|
-
timer =
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
827
|
+
timer = if timeout_seconds && timeout_seconds > 0
|
|
828
|
+
Thread.new do
|
|
829
|
+
sleep timeout_seconds
|
|
830
|
+
timed_out = true
|
|
831
|
+
Process.kill('-KILL', wait_thr.pid)
|
|
832
|
+
rescue Errno::ESRCH
|
|
833
|
+
nil
|
|
834
|
+
end
|
|
777
835
|
end
|
|
778
836
|
|
|
779
837
|
output = stdout.read
|
|
780
838
|
wait_thr.value
|
|
781
|
-
timer
|
|
839
|
+
timer&.kill
|
|
782
840
|
end
|
|
783
841
|
|
|
784
|
-
raise "RuboCop timed out after #{
|
|
842
|
+
raise "RuboCop timed out after #{timeout_seconds} seconds" if timed_out
|
|
785
843
|
|
|
786
844
|
output
|
|
787
845
|
end
|
|
@@ -856,48 +914,46 @@ module Mbeditor
|
|
|
856
914
|
candidate.exist? ? ".rubocop.yml" : nil
|
|
857
915
|
end
|
|
858
916
|
|
|
917
|
+
PROBE_MUTEX = Mutex.new
|
|
918
|
+
private_constant :PROBE_MUTEX
|
|
919
|
+
|
|
859
920
|
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
|
|
921
|
+
key = Mbeditor.configuration.rubocop_command.to_s
|
|
922
|
+
probe_cached(:@rubocop_available_cache, key) do
|
|
867
923
|
_out, _err, status = Open3.capture3(*rubocop_command, "--version")
|
|
868
924
|
status.success?
|
|
869
|
-
rescue StandardError
|
|
870
|
-
false
|
|
871
925
|
end
|
|
872
926
|
end
|
|
873
927
|
|
|
874
928
|
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
|
|
929
|
+
cmd = haml_lint_command
|
|
930
|
+
key = cmd.join(" ")
|
|
931
|
+
probe_cached(:@haml_lint_available_cache, key) do
|
|
883
932
|
_out, _err, status = Open3.capture3(*cmd, "--version")
|
|
884
933
|
status.success?
|
|
885
|
-
rescue StandardError
|
|
886
|
-
false
|
|
887
934
|
end
|
|
888
935
|
end
|
|
889
936
|
|
|
890
937
|
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
|
|
938
|
+
key = workspace_root.to_s
|
|
939
|
+
probe_cached(:@git_available_cache, key) do
|
|
897
940
|
_out, _err, status = Open3.capture3("git", "-C", key, "rev-parse", "--is-inside-work-tree")
|
|
898
941
|
status.success?
|
|
899
|
-
|
|
900
|
-
|
|
942
|
+
end
|
|
943
|
+
end
|
|
944
|
+
|
|
945
|
+
def probe_cached(ivar, key, &block)
|
|
946
|
+
PROBE_MUTEX.synchronize do
|
|
947
|
+
cache = self.class.instance_variable_get(ivar) ||
|
|
948
|
+
self.class.instance_variable_set(ivar, {})
|
|
949
|
+
unless cache.key?(key)
|
|
950
|
+
cache[key] = begin
|
|
951
|
+
block.call
|
|
952
|
+
rescue StandardError
|
|
953
|
+
false
|
|
954
|
+
end
|
|
955
|
+
end
|
|
956
|
+
cache[key]
|
|
901
957
|
end
|
|
902
958
|
end
|
|
903
959
|
|
|
@@ -952,8 +1008,13 @@ module Mbeditor
|
|
|
952
1008
|
end
|
|
953
1009
|
|
|
954
1010
|
def parse_porcelain_status(output)
|
|
955
|
-
output.lines.
|
|
956
|
-
|
|
1011
|
+
output.lines.filter_map do |line|
|
|
1012
|
+
next if line.length < 4
|
|
1013
|
+
|
|
1014
|
+
path = line[3..].to_s.strip
|
|
1015
|
+
next if path.blank?
|
|
1016
|
+
|
|
1017
|
+
{ status: line[0..1].strip, path: path }
|
|
957
1018
|
end
|
|
958
1019
|
end
|
|
959
1020
|
|
|
@@ -970,29 +1031,6 @@ module Mbeditor
|
|
|
970
1031
|
end
|
|
971
1032
|
end
|
|
972
1033
|
|
|
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
1034
|
def monaco_worker_file
|
|
997
1035
|
engine_path = Mbeditor::Engine.root.join("public", "monaco_worker.js")
|
|
998
1036
|
return engine_path if engine_path.file?
|
|
@@ -1000,38 +1038,6 @@ module Mbeditor
|
|
|
1000
1038
|
Rails.root.join("public", "monaco_worker.js")
|
|
1001
1039
|
end
|
|
1002
1040
|
|
|
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
1041
|
def resolve_monaco_asset_path(asset_path)
|
|
1036
1042
|
return nil if asset_path.blank?
|
|
1037
1043
|
|
|
@@ -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
|