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_numstat(output) ⇒ Object
Parse ‘git diff –numstat` 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].
65 66 67 68 69 70 71 72 73 74 |
# File 'app/services/mbeditor/git_service.rb', line 65 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).
49 50 51 52 |
# File 'app/services/mbeditor/git_service.rb', line 49 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.
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'app/services/mbeditor/git_service.rb', line 79 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.
125 126 127 |
# File 'app/services/mbeditor/git_service.rb', line 125 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.
119 120 121 |
# File 'app/services/mbeditor/git_service.rb', line 119 def self.parse_git_log_with_parents(raw_output) parse_log_entries(raw_output, with_parents: true) end |
.parse_numstat(output) ⇒ Object
Parse ‘git diff –numstat` output. Returns Hash of path => { added: Integer, removed: Integer }.
108 109 110 111 112 113 114 115 |
# File 'app/services/mbeditor/git_service.rb', line 108 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 |
.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.
131 132 133 134 135 136 |
# File 'app/services/mbeditor/git_service.rb', line 131 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 captured and discarded to prevent git diagnostic messages from leaking into the Rails server log. Honors config.git_timeout (seconds) when set.
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# File 'app/services/mbeditor/git_service.rb', line 21 def run_git(repo_path, *args) timeout_secs = Mbeditor.configuration.git_timeout&.to_i out = +""; timed_out = false; exit_status = nil Open3.popen3("git", "-C", repo_path, *args, pgroup: true) do |stdin, stdout, _stderr, wait_thr| stdin.close timer = if timeout_secs && timeout_secs > 0 Thread.new do sleep timeout_secs timed_out = true Process.kill("-KILL", wait_thr.pid) rescue Errno::ESRCH nil end end out = stdout.read exit_status = wait_thr.value timer&.kill end raise Timeout::Error, "git timed out after #{timeout_secs}s" if timed_out [out, exit_status] 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.
56 57 58 59 60 61 62 |
# File 'app/services/mbeditor/git_service.rb', line 56 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 |