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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
|
+
require "timeout"
|
|
4
5
|
|
|
5
6
|
module Mbeditor
|
|
6
7
|
# Shared helpers for running git CLI commands read-only inside a repo.
|
|
@@ -10,16 +11,37 @@ module Mbeditor
|
|
|
10
11
|
module_function
|
|
11
12
|
|
|
12
13
|
# Safe pattern for git ref names (branch, remote/branch, tag).
|
|
13
|
-
#
|
|
14
|
-
# git reflog syntax (e.g. "@{" sequences beyond the trailing "@{u}").
|
|
14
|
+
# Excludes @ to prevent reflog syntax like @{-1} or @{u}.
|
|
15
15
|
SAFE_GIT_REF = %r{\A[\w./-]+\z}
|
|
16
16
|
|
|
17
17
|
# Run an arbitrary git command inside +repo_path+.
|
|
18
18
|
# Returns [stdout, Process::Status]. stderr is captured and discarded to
|
|
19
19
|
# prevent git diagnostic messages from leaking into the Rails server log.
|
|
20
|
+
# Honors config.git_timeout (seconds) when set.
|
|
20
21
|
def run_git(repo_path, *args)
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
timeout_secs = Mbeditor.configuration.git_timeout&.to_i
|
|
23
|
+
out = +""; timed_out = false; exit_status = nil
|
|
24
|
+
|
|
25
|
+
Open3.popen3("git", "-C", repo_path, *args, pgroup: true) do |stdin, stdout, _stderr, wait_thr|
|
|
26
|
+
stdin.close
|
|
27
|
+
|
|
28
|
+
timer = if timeout_secs && timeout_secs > 0
|
|
29
|
+
Thread.new do
|
|
30
|
+
sleep timeout_secs
|
|
31
|
+
timed_out = true
|
|
32
|
+
Process.kill("-KILL", wait_thr.pid)
|
|
33
|
+
rescue Errno::ESRCH
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
out = stdout.read
|
|
39
|
+
exit_status = wait_thr.value
|
|
40
|
+
timer&.kill
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
raise Timeout::Error, "git timed out after #{timeout_secs}s" if timed_out
|
|
44
|
+
[out, exit_status]
|
|
23
45
|
end
|
|
24
46
|
|
|
25
47
|
# Current branch name, or nil if not in a git repo.
|
|
@@ -51,36 +73,57 @@ module Mbeditor
|
|
|
51
73
|
[parts[0].to_i, parts[1].to_i]
|
|
52
74
|
end
|
|
53
75
|
|
|
76
|
+
# Returns [merge_base_sha, ref_name] of the first candidate base branch found,
|
|
77
|
+
# or [nil, nil] if none can be determined. Candidates are tried in preference
|
|
78
|
+
# order; skips the current branch and refs whose merge-base equals HEAD.
|
|
79
|
+
def find_branch_base(repo_path, current_branch, candidates: nil)
|
|
80
|
+
candidates ||= Mbeditor.configuration.base_branch_candidates
|
|
81
|
+
head_sha_out, = run_git(repo_path, "rev-parse", "HEAD")
|
|
82
|
+
head_sha = head_sha_out.strip
|
|
83
|
+
|
|
84
|
+
candidates.each do |ref|
|
|
85
|
+
short = ref.delete_prefix("origin/")
|
|
86
|
+
next if short == current_branch || ref == current_branch
|
|
87
|
+
|
|
88
|
+
_o, st = run_git(repo_path, "rev-parse", "--verify", "--quiet", ref)
|
|
89
|
+
next unless st.success?
|
|
90
|
+
|
|
91
|
+
base_out, base_st = run_git(repo_path, "merge-base", "HEAD", ref)
|
|
92
|
+
next unless base_st.success?
|
|
93
|
+
|
|
94
|
+
sha = base_out.strip
|
|
95
|
+
next unless sha.match?(/\A[0-9a-f]{40}\z/)
|
|
96
|
+
next if sha == head_sha
|
|
97
|
+
|
|
98
|
+
return [sha, ref]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
[nil, nil]
|
|
102
|
+
rescue StandardError
|
|
103
|
+
[nil, nil]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Parse `git diff --numstat` output.
|
|
107
|
+
# Returns Hash of path => { added: Integer, removed: Integer }.
|
|
108
|
+
def parse_numstat(output)
|
|
109
|
+
(output || "").lines.each_with_object({}) do |line, map|
|
|
110
|
+
parts = line.strip.split("\t", 3)
|
|
111
|
+
next if parts.length < 3 || parts[0] == "-"
|
|
112
|
+
|
|
113
|
+
map[parts[2].strip] = { added: parts[0].to_i, removed: parts[1].to_i }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
54
117
|
# Parse compact `git log --pretty=format:%H%x1f%P%x1f%s%x1f%an%x1f%aI%x1e` output.
|
|
55
|
-
# Returns Array of hashes.
|
|
118
|
+
# Returns Array of hashes with string keys.
|
|
56
119
|
def self.parse_git_log_with_parents(raw_output)
|
|
57
|
-
raw_output
|
|
58
|
-
fields = entry.strip.split("\x1f", 5)
|
|
59
|
-
next unless fields.length == 5
|
|
60
|
-
|
|
61
|
-
{
|
|
62
|
-
"hash" => fields[0],
|
|
63
|
-
"parents" => fields[1].split.reject(&:blank?),
|
|
64
|
-
"title" => fields[2],
|
|
65
|
-
"author" => fields[3],
|
|
66
|
-
"date" => fields[4]
|
|
67
|
-
}
|
|
68
|
-
end.compact
|
|
120
|
+
parse_log_entries(raw_output, with_parents: true)
|
|
69
121
|
end
|
|
70
122
|
|
|
71
123
|
# Parse compact `git log --pretty=format:%H%x1f%s%x1f%an%x1f%aI%x1e` output.
|
|
124
|
+
# Returns Array of hashes with string keys.
|
|
72
125
|
def self.parse_git_log(raw_output)
|
|
73
|
-
raw_output
|
|
74
|
-
fields = entry.strip.split("\x1f", 4)
|
|
75
|
-
next unless fields.length == 4
|
|
76
|
-
|
|
77
|
-
{
|
|
78
|
-
"hash" => fields[0],
|
|
79
|
-
"title" => fields[1],
|
|
80
|
-
"author" => fields[2],
|
|
81
|
-
"date" => fields[3]
|
|
82
|
-
}
|
|
83
|
-
end.compact
|
|
126
|
+
parse_log_entries(raw_output, with_parents: false)
|
|
84
127
|
end
|
|
85
128
|
|
|
86
129
|
# Resolve a file path safely within repo_path. Returns full path string or
|
|
@@ -91,5 +134,31 @@ module Mbeditor
|
|
|
91
134
|
full = File.expand_path(relative.to_s, repo_path.to_s)
|
|
92
135
|
full.start_with?(repo_path.to_s + "/") || full == repo_path.to_s ? full : nil
|
|
93
136
|
end
|
|
137
|
+
|
|
138
|
+
def self.parse_log_entries(raw_output, with_parents:)
|
|
139
|
+
field_count = with_parents ? 5 : 4
|
|
140
|
+
raw_output.split("\x1e").filter_map do |entry|
|
|
141
|
+
fields = entry.strip.split("\x1f", field_count)
|
|
142
|
+
next unless fields.length == field_count
|
|
143
|
+
|
|
144
|
+
if with_parents
|
|
145
|
+
{
|
|
146
|
+
"hash" => fields[0],
|
|
147
|
+
"parents" => fields[1].split.reject(&:blank?),
|
|
148
|
+
"title" => fields[2],
|
|
149
|
+
"author" => fields[3],
|
|
150
|
+
"date" => fields[4]
|
|
151
|
+
}
|
|
152
|
+
else
|
|
153
|
+
{
|
|
154
|
+
"hash" => fields[0],
|
|
155
|
+
"title" => fields[1],
|
|
156
|
+
"author" => fields[2],
|
|
157
|
+
"date" => fields[3]
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
private_class_method :parse_log_entries
|
|
94
163
|
end
|
|
95
164
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "net/http"
|
|
4
|
+
require "openssl"
|
|
4
5
|
require "uri"
|
|
5
6
|
require "json"
|
|
6
7
|
|
|
@@ -42,6 +43,9 @@ module Mbeditor
|
|
|
42
43
|
raise RedmineConfigError, "redmine_url is not configured" if config.redmine_url.blank?
|
|
43
44
|
raise RedmineConfigError, "redmine_api_key is not configured" if config.redmine_api_key.blank?
|
|
44
45
|
|
|
46
|
+
uri = URI.parse(config.redmine_url.to_s.chomp("/"))
|
|
47
|
+
raise RedmineConfigError, "redmine_url must use http or https scheme" unless %w[http https].include?(uri.scheme)
|
|
48
|
+
|
|
45
49
|
fetch_issue(config.redmine_url, config.redmine_api_key)
|
|
46
50
|
end
|
|
47
51
|
|
|
@@ -52,6 +56,9 @@ module Mbeditor
|
|
|
52
56
|
|
|
53
57
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
54
58
|
http.use_ssl = uri.scheme == "https"
|
|
59
|
+
if uri.scheme == "https"
|
|
60
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
61
|
+
end
|
|
55
62
|
http.open_timeout = TIMEOUT_SECONDS
|
|
56
63
|
http.read_timeout = TIMEOUT_SECONDS
|
|
57
64
|
|
|
@@ -22,6 +22,7 @@ module Mbeditor
|
|
|
22
22
|
class RubyDefinitionService
|
|
23
23
|
MAX_RESULTS = 20
|
|
24
24
|
MAX_COMMENT_LOOKAHEAD = 15
|
|
25
|
+
MAX_FILES_SCANNED = 10_000
|
|
25
26
|
|
|
26
27
|
# In-process file-index cache.
|
|
27
28
|
# Structure: { absolute_path => { mtime: Float, lines: [String], all_defs: { method_name => [line, ...] } } }
|
|
@@ -101,8 +102,11 @@ module Mbeditor
|
|
|
101
102
|
def call
|
|
102
103
|
self.class.load_disk_cache_once
|
|
103
104
|
|
|
104
|
-
results
|
|
105
|
-
@new_entries
|
|
105
|
+
results = []
|
|
106
|
+
@new_entries = false
|
|
107
|
+
files_scanned = 0
|
|
108
|
+
|
|
109
|
+
evict_deleted_cache_entries
|
|
106
110
|
|
|
107
111
|
Find.find(@workspace_root) do |path|
|
|
108
112
|
# Prune excluded directories
|
|
@@ -120,6 +124,12 @@ module Mbeditor
|
|
|
120
124
|
rel = relative_path(path)
|
|
121
125
|
next if excluded_rel_path?(rel, File.basename(path))
|
|
122
126
|
|
|
127
|
+
files_scanned += 1
|
|
128
|
+
if files_scanned > MAX_FILES_SCANNED
|
|
129
|
+
Rails.logger.warn("[mbeditor] RubyDefinitionService: workspace exceeds #{MAX_FILES_SCANNED} .rb files; stopping scan early")
|
|
130
|
+
break
|
|
131
|
+
end
|
|
132
|
+
|
|
123
133
|
begin
|
|
124
134
|
cached = cache_entry_for(path)
|
|
125
135
|
next unless cached
|
|
@@ -147,6 +157,17 @@ module Mbeditor
|
|
|
147
157
|
|
|
148
158
|
private
|
|
149
159
|
|
|
160
|
+
# Remove cache entries for files that no longer exist on disk.
|
|
161
|
+
def evict_deleted_cache_entries
|
|
162
|
+
stale_keys = self.class.mutex.synchronize do
|
|
163
|
+
self.class.file_cache.keys.select { |p| !File.exist?(p) }
|
|
164
|
+
end
|
|
165
|
+
return if stale_keys.empty?
|
|
166
|
+
|
|
167
|
+
self.class.mutex.synchronize { stale_keys.each { |k| self.class.file_cache.delete(k) } }
|
|
168
|
+
@new_entries = true
|
|
169
|
+
end
|
|
170
|
+
|
|
150
171
|
# Returns the cached index entry for +path+, rebuilding it if the file has
|
|
151
172
|
# been modified since the last parse. Returns nil on any read/parse error.
|
|
152
173
|
def cache_entry_for(path)
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Mbeditor
|
|
2
4
|
class Configuration
|
|
3
5
|
attr_accessor :allowed_environments, :workspace_root, :excluded_paths, :rubocop_command,
|
|
4
6
|
:redmine_enabled, :redmine_url, :redmine_api_key, :redmine_ticket_source,
|
|
5
7
|
:test_framework, :test_command, :test_timeout,
|
|
6
|
-
:authenticate_with
|
|
8
|
+
:authenticate_with,
|
|
9
|
+
:lint_timeout, :base_branch_candidates, :git_timeout
|
|
7
10
|
|
|
8
11
|
def initialize
|
|
9
12
|
@allowed_environments = [:development]
|
|
@@ -17,6 +20,9 @@ module Mbeditor
|
|
|
17
20
|
@test_framework = nil # :minitest or :rspec — auto-detected when nil
|
|
18
21
|
@test_command = nil # e.g. "bundle exec ruby -Itest" or "bundle exec rspec"
|
|
19
22
|
@test_timeout = 60 # seconds
|
|
23
|
+
@lint_timeout = 15 # seconds for RuboCop/haml-lint subprocesses
|
|
24
|
+
@base_branch_candidates = %w[origin/develop origin/main origin/master develop main master]
|
|
25
|
+
@git_timeout = nil # seconds; nil disables (no timeout on git subprocesses)
|
|
20
26
|
end
|
|
21
27
|
end
|
|
22
28
|
end
|
data/lib/mbeditor/engine.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "mbeditor/rack/silence_ping_request"
|
|
2
4
|
require "mbeditor/rack/handle_pending_migrations"
|
|
3
5
|
|
|
@@ -24,6 +26,31 @@ module Mbeditor
|
|
|
24
26
|
config.after_initialize do
|
|
25
27
|
Mbeditor::RubyDefinitionService.cache_path =
|
|
26
28
|
Rails.root.join("tmp", "mbeditor_ruby_defs.json").to_s
|
|
29
|
+
|
|
30
|
+
cfg = Mbeditor.configuration
|
|
31
|
+
|
|
32
|
+
if cfg.workspace_root.present? && !File.directory?(cfg.workspace_root.to_s)
|
|
33
|
+
raise ArgumentError, "[mbeditor] config.workspace_root is set to '#{cfg.workspace_root}' but that path is not a directory"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
if cfg.redmine_enabled
|
|
37
|
+
require "uri"
|
|
38
|
+
if cfg.redmine_url.blank?
|
|
39
|
+
Rails.logger.warn("[mbeditor] redmine_enabled is true but redmine_url is not configured")
|
|
40
|
+
else
|
|
41
|
+
begin
|
|
42
|
+
uri = URI.parse(cfg.redmine_url.to_s)
|
|
43
|
+
unless %w[http https].include?(uri.scheme)
|
|
44
|
+
Rails.logger.warn("[mbeditor] redmine_url must use http or https scheme")
|
|
45
|
+
end
|
|
46
|
+
rescue URI::InvalidURIError
|
|
47
|
+
Rails.logger.warn("[mbeditor] redmine_url is not a valid URI")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
if cfg.redmine_api_key.blank?
|
|
51
|
+
Rails.logger.warn("[mbeditor] redmine_enabled is true but redmine_api_key is not configured")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
27
54
|
end
|
|
28
55
|
|
|
29
56
|
initializer "mbeditor.assets.precompile" do |app|
|
data/lib/mbeditor/version.rb
CHANGED
data/lib/mbeditor.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mbeditor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oliver Noonan
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|