Class: Kettle::Dev::GitAdapter
- Inherits:
-
Object
- Object
- Kettle::Dev::GitAdapter
- Defined in:
- lib/kettle/dev/git_adapter.rb
Overview
Minimal Git adapter used by kettle-dev to avoid invoking live shell commands directly from the higher-level library code. In tests, mock this adapter’s methods to prevent any real network or repository mutations.
Behavior:
-
Prefer the ‘git’ gem when available.
-
If the ‘git’ gem is not present (LoadError), fall back to shelling out to the system ‘git` executable for the small set of operations we need.
Public API is intentionally small and only includes what we need right now.
Instance Method Summary collapse
-
#blame_porcelain(path) ⇒ String
Return the raw ‘git blame –porcelain` output for a single tracked file.
-
#capture(args) ⇒ Array<(String, Boolean)>
Execute a git command and capture its stdout and success flag.
-
#checkout(branch) ⇒ Boolean
Checkout the given branch.
-
#clean? ⇒ Boolean
Determine whether the working tree is clean (no unstaged, staged, or untracked changes).
-
#current_branch ⇒ String?
Current branch name, or nil on error.
-
#fetch(remote, ref = nil) ⇒ Boolean
Fetch a ref from a remote (or everything if ref is nil).
-
#initialize ⇒ void
constructor
Create a new adapter rooted at the current working directory.
-
#ls_files ⇒ Array<String>
Return the list of files currently tracked by git.
-
#pull(remote, branch) ⇒ Boolean
Pull from a remote/branch.
-
#push(remote, branch, force: false) ⇒ Boolean
Push a branch to a remote.
-
#push_tags(remote) ⇒ Boolean
Push all tags to a remote.
- #remote_url(name) ⇒ String?
-
#remotes ⇒ Array<String>
List of remote names.
-
#remotes_with_urls ⇒ Hash{String=>String}
Remote name => fetch URL.
Constructor Details
#initialize ⇒ void
Create a new adapter rooted at the current working directory.
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/kettle/dev/git_adapter.rb', line 55 def initialize begin # Allow users/CI to opt out of using the 'git' gem even when available. # Set KETTLE_DEV_DISABLE_GIT_GEM to a truthy value ("1", "true", "yes") to force CLI backend. env_val = ENV["KETTLE_DEV_DISABLE_GIT_GEM"] # Ruby 2.3 compatibility: String#match? was added in 2.4; use Regexp#=== / =~ instead disable_gem = env_val && !!(/\A(1|true|yes)\z/i =~ env_val) if disable_gem @backend = :cli else Kernel.require "git" @backend = :gem @git = ::Git.open(Dir.pwd) end rescue LoadError => e Kettle::Dev.debug_error(e, __method__, backtrace: false) # Optional dependency: fall back to CLI @backend = :cli rescue StandardError => e raise Kettle::Dev::Error, "Failed to open git repository: #{e.}" end end |
Instance Method Details
#blame_porcelain(path) ⇒ String
Return the raw ‘git blame –porcelain` output for a single tracked file.
Both backends shell out directly because the ‘git` gem does not provide a stable porcelain-blame interface. Callers that need only the output string (e.g. CopyrightCollector) should stub this method in specs.
178 179 180 181 182 183 184 |
# File 'lib/kettle/dev/git_adapter.rb', line 178 def blame_porcelain(path) out, status = Open3.capture2("git", "blame", "--porcelain", path.to_s) status.success? ? out : "" rescue StandardError => e Kettle::Dev.debug_error(e, __method__) "" end |
#capture(args) ⇒ Array<(String, Boolean)>
Execute a git command and capture its stdout and success flag. This is a generic escape hatch used by higher-level code for read-only queries that aren’t covered by the explicit adapter API. Tests can stub this method to avoid shelling out.
45 46 47 48 49 50 51 |
# File 'lib/kettle/dev/git_adapter.rb', line 45 def capture(args) out, status = Open3.capture2("git", *args) [out.strip, status.success?] rescue StandardError => e Kettle::Dev.debug_error(e, __method__) ["", false] end |
#checkout(branch) ⇒ Boolean
Checkout the given branch
246 247 248 249 250 251 252 253 254 255 256 |
# File 'lib/kettle/dev/git_adapter.rb', line 246 def checkout(branch) if @backend == :gem @git.checkout(branch) true else system("git", "checkout", branch.to_s) end rescue StandardError => e Kettle::Dev.debug_error(e, __method__) false end |
#clean? ⇒ Boolean
Determine whether the working tree is clean (no unstaged, staged, or untracked changes).
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/kettle/dev/git_adapter.rb', line 20 def clean? if @backend == :gem begin status = @git.status # git gem's Status responds to changed, added, deleted, untracked, etc. status.changed.empty? && status.added.empty? && status.deleted.empty? && status.untracked.empty? rescue StandardError => e Kettle::Dev.debug_error(e, __method__) false end else out, st = Open3.capture2("git", "status", "--porcelain") st.success? && out.strip.empty? end rescue StandardError => e Kettle::Dev.debug_error(e, __method__) false end |
#current_branch ⇒ String?
Returns current branch name, or nil on error.
138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/kettle/dev/git_adapter.rb', line 138 def current_branch if @backend == :gem @git.current_branch else out, status = Open3.capture2("git", "rev-parse", "--abbrev-ref", "HEAD") status.success? ? out.strip : nil end rescue StandardError => e Kettle::Dev.debug_error(e, __method__) nil end |
#fetch(remote, ref = nil) ⇒ Boolean
Fetch a ref from a remote (or everything if ref is nil)
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 |
# File 'lib/kettle/dev/git_adapter.rb', line 278 def fetch(remote, ref = nil) if @backend == :gem if ref @git.fetch(remote, ref) else @git.fetch(remote) end true elsif ref system("git", "fetch", remote.to_s, ref.to_s) else system("git", "fetch", remote.to_s) end rescue StandardError => e Kettle::Dev.debug_error(e, __method__) false end |
#ls_files ⇒ Array<String>
Return the list of files currently tracked by git.
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
# File 'lib/kettle/dev/git_adapter.rb', line 153 def ls_files if @backend == :gem begin @git.ls_files.keys rescue StandardError => e Kettle::Dev.debug_error(e, __method__) [] end else out, status = Open3.capture2("git", "ls-files") status.success? ? out.split(/\r?\n/).reject(&:empty?) : [] end rescue StandardError => e Kettle::Dev.debug_error(e, __method__) [] end |
#pull(remote, branch) ⇒ Boolean
Pull from a remote/branch
262 263 264 265 266 267 268 269 270 271 272 |
# File 'lib/kettle/dev/git_adapter.rb', line 262 def pull(remote, branch) if @backend == :gem @git.pull(remote, branch) true else system("git", "pull", remote.to_s, branch.to_s) end rescue StandardError => e Kettle::Dev.debug_error(e, __method__) false end |
#push(remote, branch, force: false) ⇒ Boolean
Push a branch to a remote.
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/kettle/dev/git_adapter.rb', line 83 def push(remote, branch, force: false) if @backend == :gem begin if remote @git.push(remote, branch, force: force) else # Default remote according to repo config @git.push(nil, branch, force: force) end true rescue StandardError => e Kettle::Dev.debug_error(e, __method__) false end else args = ["git", "push"] args << "--force" if force if remote args << remote.to_s << branch.to_s end system(*args) end end |
#push_tags(remote) ⇒ Boolean
Push all tags to a remote. Notes:
-
The ruby-git gem does not provide a stable API for pushing all tags across versions, so we intentionally shell out to ‘git push –tags` for both backends. Tests should stub this method in higher-level code to avoid mutating any repositories.
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/kettle/dev/git_adapter.rb', line 118 def (remote) if @backend == :gem # The ruby-git gem does not expose a dedicated API for "--tags" consistently across versions. # Use a shell fallback even when the gem backend is active. Tests should stub this method. if remote && !remote.to_s.empty? system("git", "push", remote.to_s, "--tags") else system("git", "push", "--tags") end elsif remote && !remote.to_s.empty? system("git", "push", remote.to_s, "--tags") else system("git", "push", "--tags") end rescue StandardError => e Kettle::Dev.debug_error(e, __method__) false end |
#remote_url(name) ⇒ String?
230 231 232 233 234 235 236 237 238 239 240 241 |
# File 'lib/kettle/dev/git_adapter.rb', line 230 def remote_url(name) if @backend == :gem r = @git.remotes.find { |x| x.name == name } r&.url else out, status = Open3.capture2("git", "config", "--get", "remote.#{name}.url") status.success? ? out.strip : nil end rescue StandardError => e Kettle::Dev.debug_error(e, __method__) nil end |
#remotes ⇒ Array<String>
Returns list of remote names.
187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/kettle/dev/git_adapter.rb', line 187 def remotes if @backend == :gem @git.remotes.map(&:name) else out, status = Open3.capture2("git", "remote") status.success? ? out.split(/\r?\n/).map(&:strip).reject(&:empty?) : [] end rescue StandardError => e Kettle::Dev.debug_error(e, __method__) [] end |
#remotes_with_urls ⇒ Hash{String=>String}
Returns remote name => fetch URL.
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 |
# File 'lib/kettle/dev/git_adapter.rb', line 200 def remotes_with_urls if @backend == :gem @git.remotes.each_with_object({}) do |r, h| begin h[r.name] = r.url rescue StandardError => e Kettle::Dev.debug_error(e, __method__) # ignore end end else out, status = Open3.capture2("git", "remote", "-v") return {} unless status.success? urls = {} out.each_line do |line| # Example: origin https://github.com/me/repo.git (fetch) if line =~ /^(\S+)\s+(\S+)\s+\(fetch\)/ urls[Regexp.last_match(1)] = Regexp.last_match(2) end end urls end rescue StandardError => e Kettle::Dev.debug_error(e, __method__) {} end |