Module: Mbeditor::GitService
- Included in:
- GitBlameService, GitCommitGraphService, GitDiffService, GitFileHistoryService
- Defined in:
- app/services/mbeditor/git_service.rb
Overview
Shared helpers for running git CLI commands read-only inside a repo. All public methods accept repo_path as their first argument so services stay stateless and composable.
Constant Summary collapse
- SAFE_GIT_REF =
Safe pattern for git ref names (branch, remote/branch, tag). Excludes @ to prevent reflog syntax like @-1 or @u.
%r{\A[\w./-]+\z}
Class Method Summary collapse
-
.ahead_behind(repo_path, upstream) ⇒ Object
Returns [ahead_count, behind_count] relative to upstream, or [0,0].
-
.current_branch(repo_path) ⇒ Object
Current branch name, or nil if not in a git repo.
-
.find_branch_base(repo_path, current_branch, candidates: nil) ⇒ Object
Returns [merge_base_sha, ref_name] of the first candidate base branch found, or [nil, nil] if none can be determined.
-
.parse_git_log(raw_output) ⇒ Object
Parse compact ‘git log –pretty=format:%H%x1f%s%x1f%an%x1f%aI%x1e` output.
-
.parse_git_log_with_parents(raw_output) ⇒ Object
Parse compact ‘git log –pretty=format:%H%x1f%P%x1f%s%x1f%an%x1f%aI%x1e` output.
-
.parse_name_status(output) ⇒ Object
Parse ‘git diff –name-status` output.
-
.parse_numstat(output) ⇒ Object
Parse ‘git diff –numstat` output.
-
.parse_porcelain_status(output) ⇒ Object
Parse ‘git status –porcelain` output.
-
.resolve_path(repo_path, relative) ⇒ Object
Resolve a file path safely within repo_path.
-
.run_git(repo_path, *args) ⇒ Object
Run an arbitrary git command inside
repo_path. -
.upstream_branch(repo_path) ⇒ Object
Upstream tracking branch for the current branch, e.g.
Class Method Details
.ahead_behind(repo_path, upstream) ⇒ Object
Returns [ahead_count, behind_count] relative to upstream, or [0,0].
47 48 49 50 51 52 53 54 55 56 |
# File 'app/services/mbeditor/git_service.rb', line 47 def ahead_behind(repo_path, upstream) return [0, 0] if upstream.blank? return [0, 0] unless upstream.match?(SAFE_GIT_REF) out, status = run_git(repo_path, "rev-list", "--left-right", "--count", "HEAD...#{upstream}") return [0, 0] unless status.success? parts = out.strip.split("\t", 2) [parts[0].to_i, parts[1].to_i] end |
.current_branch(repo_path) ⇒ Object
Current branch name, or nil if not in a git repo. Uses rev-parse for compatibility with Git < 2.22 (which lacks –show-current).
31 32 33 34 |
# File 'app/services/mbeditor/git_service.rb', line 31 def current_branch(repo_path) out, status = run_git(repo_path, "rev-parse", "--abbrev-ref", "HEAD") status.success? ? out.strip : nil end |
.find_branch_base(repo_path, current_branch, candidates: nil) ⇒ Object
Returns [merge_base_sha, ref_name] of the first candidate base branch found, or [nil, nil] if none can be determined. Candidates are tried in preference order; skips the current branch and refs whose merge-base equals HEAD.
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'app/services/mbeditor/git_service.rb', line 61 def find_branch_base(repo_path, current_branch, candidates: nil) candidates ||= Mbeditor.configuration.base_branch_candidates head_sha_out, = run_git(repo_path, "rev-parse", "HEAD") head_sha = head_sha_out.strip candidates.each do |ref| short = ref.delete_prefix("origin/") next if short == current_branch || ref == current_branch _o, st = run_git(repo_path, "rev-parse", "--verify", "--quiet", ref) next unless st.success? base_out, base_st = run_git(repo_path, "merge-base", "HEAD", ref) next unless base_st.success? sha = base_out.strip next unless sha.match?(/\A[0-9a-f]{40}\z/) next if sha == head_sha return [sha, ref] end [nil, nil] rescue StandardError [nil, nil] end |
.parse_git_log(raw_output) ⇒ Object
Parse compact ‘git log –pretty=format:%H%x1f%s%x1f%an%x1f%aI%x1e` output. Returns Array of hashes with string keys.
135 136 137 |
# File 'app/services/mbeditor/git_service.rb', line 135 def self.parse_git_log(raw_output) parse_log_entries(raw_output, with_parents: false) end |
.parse_git_log_with_parents(raw_output) ⇒ Object
Parse compact ‘git log –pretty=format:%H%x1f%P%x1f%s%x1f%an%x1f%aI%x1e` output. Returns Array of hashes with string keys.
129 130 131 |
# File 'app/services/mbeditor/git_service.rb', line 129 def self.parse_git_log_with_parents(raw_output) parse_log_entries(raw_output, with_parents: true) end |
.parse_name_status(output) ⇒ Object
Parse ‘git diff –name-status` output. Returns Array of { status: String, path: String }.
103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'app/services/mbeditor/git_service.rb', line 103 def parse_name_status(output) output.lines.filter_map do |line| parts = line.strip.split("\t") next if parts.empty? status = parts[0].to_s.strip path = parts.last.to_s.strip next if path.blank? { status: status, path: path } end end |
.parse_numstat(output) ⇒ Object
Parse ‘git diff –numstat` output. Returns Hash of path => { added: Integer, removed: Integer }.
118 119 120 121 122 123 124 125 |
# File 'app/services/mbeditor/git_service.rb', line 118 def parse_numstat(output) (output || "").lines.each_with_object({}) do |line, map| parts = line.strip.split("\t", 3) next if parts.length < 3 || parts[0] == "-" map[parts[2].strip] = { added: parts[0].to_i, removed: parts[1].to_i } end end |
.parse_porcelain_status(output) ⇒ Object
Parse ‘git status –porcelain` output. Returns Array of { status: String, path: String }.
90 91 92 93 94 95 96 97 98 99 |
# File 'app/services/mbeditor/git_service.rb', line 90 def parse_porcelain_status(output) output.lines.filter_map do |line| next if line.length < 4 path = line[3..].to_s.strip next if path.blank? { status: line[0..1].strip, path: path } end end |
.resolve_path(repo_path, relative) ⇒ Object
Resolve a file path safely within repo_path. Returns full path string or nil if the path escapes the root.
141 142 143 144 145 146 |
# File 'app/services/mbeditor/git_service.rb', line 141 def resolve_path(repo_path, relative) return nil if relative.blank? full = File.(relative.to_s, repo_path.to_s) full.start_with?(repo_path.to_s + "/") || full == repo_path.to_s ? full : nil end |
.run_git(repo_path, *args) ⇒ Object
Run an arbitrary git command inside repo_path. Returns [stdout, Process::Status]. stderr is discarded to prevent git diagnostic messages from leaking into the Rails server log. Honors config.git_timeout (seconds) when set.
20 21 22 23 24 25 26 27 |
# File 'app/services/mbeditor/git_service.rb', line 20 def run_git(repo_path, *args) timeout_secs = Mbeditor.configuration.git_timeout&.to_i timeout = timeout_secs && timeout_secs > 0 ? timeout_secs : nil result = ProcessRunner.call(["git", "-C", repo_path, *args], timeout: timeout) [result[:stdout], result[:exit_status]] rescue ProcessRunner::TimeoutError raise Timeout::Error, "git timed out after #{timeout_secs}s" end |
.upstream_branch(repo_path) ⇒ Object
Upstream tracking branch for the current branch, e.g. “origin/main”. Returns nil if the branch name contains characters outside SAFE_GIT_REF.
38 39 40 41 42 43 44 |
# File 'app/services/mbeditor/git_service.rb', line 38 def upstream_branch(repo_path) out, status = run_git(repo_path, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}") return nil unless status.success? ref = out.strip ref.match?(SAFE_GIT_REF) ? ref : nil end |