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). Rejects refs containing whitespace, NUL, shell metacharacters, or git reflog syntax (e.g. “@sequences beyond the trailing “@{u”).

%r{\A[\w./-]+\z}

Class Method Summary collapse

Class Method Details

.ahead_behind(repo_path, upstream) ⇒ Object

Returns [ahead_count, behind_count] relative to upstream, or [0,0].



43
44
45
46
47
48
49
50
51
52
# File 'app/services/mbeditor/git_service.rb', line 43

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).



27
28
29
30
# File 'app/services/mbeditor/git_service.rb', line 27

def current_branch(repo_path)
  out, status = run_git(repo_path, "rev-parse", "--abbrev-ref", "HEAD")
  status.success? ? out.strip : nil
end

.parse_git_log(raw_output) ⇒ Object

Parse compact ‘git log –pretty=format:%H%x1f%s%x1f%an%x1f%aI%x1e` output.



72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'app/services/mbeditor/git_service.rb', line 72

def self.parse_git_log(raw_output)
  raw_output.split("\x1e").map do |entry|
    fields = entry.strip.split("\x1f", 4)
    next unless fields.length == 4

    {
      "hash"   => fields[0],
      "title"  => fields[1],
      "author" => fields[2],
      "date"   => fields[3]
    }
  end.compact
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.



56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'app/services/mbeditor/git_service.rb', line 56

def self.parse_git_log_with_parents(raw_output)
  raw_output.split("\x1e").map do |entry|
    fields = entry.strip.split("\x1f", 5)
    next unless fields.length == 5

    {
      "hash"    => fields[0],
      "parents" => fields[1].split.reject(&:blank?),
      "title"   => fields[2],
      "author"  => fields[3],
      "date"    => fields[4]
    }
  end.compact
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.



88
89
90
91
92
93
# File 'app/services/mbeditor/git_service.rb', line 88

def resolve_path(repo_path, relative)
  return nil if relative.blank?

  full = File.expand_path(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.



20
21
22
23
# File 'app/services/mbeditor/git_service.rb', line 20

def run_git(repo_path, *args)
  out, _err, status = Open3.capture3("git", "-C", repo_path, *args)
  [out, 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.



34
35
36
37
38
39
40
# File 'app/services/mbeditor/git_service.rb', line 34

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