Class: Kettle::Dev::GitAdapter
- Inherits:
-
Object
- Object
- Kettle::Dev::GitAdapter
- Defined in:
- lib/kettle/dev/git_adapter.rb,
sig/kettle/dev.rbs
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
gitexecutable 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
-
#add_all ⇒ Boolean
Stage all changes in the repository.
-
#add_paths(paths) ⇒ Boolean
Stage selected paths.
-
#add_repository_paths(paths) ⇒ Boolean
Stage paths reported by Git commands such as
git diff --name-only. -
#blame_porcelain(path) ⇒ String
Return the raw
git blame --porcelainoutput 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).
-
#commit_all(message, env: {}) ⇒ Boolean
Commit all staged/tracked changes with a message.
-
#commit_amend_no_edit(env: {}) ⇒ Boolean
Amend the current commit without changing its message.
-
#commit_staged(message, env: {}) ⇒ Boolean
Commit only the changes already staged by the caller.
-
#current_branch ⇒ String?
Current branch name, or nil on error.
-
#diff_head_quiet?(path) ⇒ Boolean
Return whether a tracked path has no changes compared with HEAD.
-
#diff_quiet?(path) ⇒ Boolean
Return whether a tracked path has no unstaged changes.
-
#fetch(remote, ref = nil) ⇒ Boolean
Fetch a ref from a remote (or everything if ref is nil).
-
#initialize(root = Dir.pwd) ⇒ GitAdapter
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.
-
#reset_soft(ref) ⇒ Boolean
Soft reset to a ref.
-
#tag_annotated(tag, message) ⇒ Boolean
Create an annotated tag.
Constructor Details
#initialize(root = Dir.pwd) ⇒ GitAdapter
Create a new adapter rooted at the current working directory.
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/kettle/dev/git_adapter.rb', line 177 def initialize(root = Dir.pwd) @root = File.(root.to_s) # 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(@root) end rescue LoadError => e Kettle::Dev.debug_error(e, __method__, backtrace: false) # Optional dependency: fall back to CLI @backend = :cli rescue => e raise Kettle::Dev::Error, "Failed to open git repository: #{e.}" end |
Instance Method Details
#add_all ⇒ Boolean
Stage all changes in the repository.
56 57 58 59 60 61 |
# File 'lib/kettle/dev/git_adapter.rb', line 56 def add_all git_system("add", "-A") rescue => e Kettle::Dev.debug_error(e, __method__) false end |
#add_paths(paths) ⇒ Boolean
Stage selected paths.
67 68 69 70 71 72 |
# File 'lib/kettle/dev/git_adapter.rb', line 67 def add_paths(paths) git_system("add", "--", *Array(paths).map(&:to_s)) rescue => e Kettle::Dev.debug_error(e, __method__) false end |
#add_repository_paths(paths) ⇒ Boolean
Stage paths reported by Git commands such as git diff --name-only.
Git reports these paths relative to the repository root, even when this
adapter operates from a monorepo subdirectory. :(top) keeps the
pathspec rooted at that repository root in either topology.
81 82 83 84 85 86 87 |
# File 'lib/kettle/dev/git_adapter.rb', line 81 def add_repository_paths(paths) pathspecs = Array(paths).map { |path| ":(top)#{path}" } git_system("add", "--", *pathspecs) rescue => e Kettle::Dev.debug_error(e, __method__) false end |
#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.
299 300 301 302 303 304 305 |
# File 'lib/kettle/dev/git_adapter.rb', line 299 def blame_porcelain(path) out, status = git_capture2("blame", "--porcelain", path.to_s) status.success? ? out : "" rescue => 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 = git_capture2(*args) [out.strip, status.success?] rescue => e Kettle::Dev.debug_error(e, __method__) ["", false] end |
#checkout(branch) ⇒ Boolean
Checkout the given branch
367 368 369 370 371 372 373 374 375 376 377 |
# File 'lib/kettle/dev/git_adapter.rb', line 367 def checkout(branch) if @backend == :gem @git.checkout(branch) true else git_system("checkout", branch.to_s) end rescue => 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 => e Kettle::Dev.debug_error(e, __method__) false end else out, st = git_capture2("status", "--porcelain") st.success? && out.strip.empty? end rescue => e Kettle::Dev.debug_error(e, __method__) false end |
#commit_all(message, env: {}) ⇒ Boolean
Commit all staged/tracked changes with a message.
94 95 96 97 98 99 |
# File 'lib/kettle/dev/git_adapter.rb', line 94 def commit_all(, env: {}) git_system("commit", "-am", .to_s, env: env) rescue => e Kettle::Dev.debug_error(e, __method__) false end |
#commit_amend_no_edit(env: {}) ⇒ Boolean
Amend the current commit without changing its message.
121 122 123 124 125 126 |
# File 'lib/kettle/dev/git_adapter.rb', line 121 def commit_amend_no_edit(env: {}) git_system("commit", "--amend", "--no-edit", env: env) rescue => e Kettle::Dev.debug_error(e, __method__) false end |
#commit_staged(message, env: {}) ⇒ Boolean
Commit only the changes already staged by the caller.
This is intentionally different from commit_all: bundle maintenance
must not sweep unrelated release-preparation changes into its own
atomic commit.
110 111 112 113 114 115 |
# File 'lib/kettle/dev/git_adapter.rb', line 110 def commit_staged(, env: {}) git_system("commit", "-m", .to_s, env: env) rescue => e Kettle::Dev.debug_error(e, __method__) false end |
#current_branch ⇒ String?
Returns current branch name, or nil on error.
259 260 261 262 263 264 265 266 267 268 269 |
# File 'lib/kettle/dev/git_adapter.rb', line 259 def current_branch if @backend == :gem @git.current_branch else out, status = git_capture2("rev-parse", "--abbrev-ref", "HEAD") status.success? ? out.strip : nil end rescue => e Kettle::Dev.debug_error(e, __method__) nil end |
#diff_head_quiet?(path) ⇒ Boolean
Return whether a tracked path has no changes compared with HEAD.
144 145 146 147 148 149 150 |
# File 'lib/kettle/dev/git_adapter.rb', line 144 def diff_head_quiet?(path) _out, status = git_capture2("diff", "--quiet", "HEAD", "--", path.to_s) status.success? rescue => e Kettle::Dev.debug_error(e, __method__) false end |
#diff_quiet?(path) ⇒ Boolean
Return whether a tracked path has no unstaged changes.
132 133 134 135 136 137 138 |
# File 'lib/kettle/dev/git_adapter.rb', line 132 def diff_quiet?(path) _out, status = git_capture2("diff", "--quiet", "--", path.to_s) status.success? rescue => e Kettle::Dev.debug_error(e, __method__) false end |
#fetch(remote, ref = nil) ⇒ Boolean
Fetch a ref from a remote (or everything if ref is nil)
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 |
# File 'lib/kettle/dev/git_adapter.rb', line 399 def fetch(remote, ref = nil) if @backend == :gem if ref @git.fetch(remote, ref) else @git.fetch(remote) end true elsif ref git_system("fetch", remote.to_s, ref.to_s) else git_system("fetch", remote.to_s) end rescue => e Kettle::Dev.debug_error(e, __method__) false end |
#ls_files ⇒ Array<String>
Return the list of files currently tracked by git.
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 |
# File 'lib/kettle/dev/git_adapter.rb', line 274 def ls_files if @backend == :gem begin @git.ls_files.keys rescue => e Kettle::Dev.debug_error(e, __method__) [] end else out, status = git_capture2("ls-files") status.success? ? out.split(/\r?\n/).reject(&:empty?) : [] end rescue => e Kettle::Dev.debug_error(e, __method__) [] end |
#pull(remote, branch) ⇒ Boolean
Pull from a remote/branch
383 384 385 386 387 388 389 390 391 392 393 |
# File 'lib/kettle/dev/git_adapter.rb', line 383 def pull(remote, branch) if @backend == :gem @git.pull(remote, branch) true else git_system("pull", remote.to_s, branch.to_s) end rescue => e Kettle::Dev.debug_error(e, __method__) false end |
#push(remote, branch, force: false) ⇒ Boolean
Push a branch to a remote.
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 204 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 => 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 --tagsfor both backends. Tests should stub this method in higher-level code to avoid mutating any repositories.
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 |
# File 'lib/kettle/dev/git_adapter.rb', line 239 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? git_system("push", remote.to_s, "--tags") else git_system("push", "--tags") end elsif remote && !remote.to_s.empty? git_system("push", remote.to_s, "--tags") else git_system("push", "--tags") end rescue => e Kettle::Dev.debug_error(e, __method__) false end |
#remote_url(name) ⇒ String?
351 352 353 354 355 356 357 358 359 360 361 362 |
# File 'lib/kettle/dev/git_adapter.rb', line 351 def remote_url(name) if @backend == :gem r = @git.remotes.find { |x| x.name == name } r&.url else out, status = git_capture2("config", "--get", "remote.#{name}.url") status.success? ? out.strip : nil end rescue => e Kettle::Dev.debug_error(e, __method__) nil end |
#remotes ⇒ Array<String>
Returns list of remote names.
308 309 310 311 312 313 314 315 316 317 318 |
# File 'lib/kettle/dev/git_adapter.rb', line 308 def remotes if @backend == :gem @git.remotes.map(&:name) else out, status = git_capture2("remote") status.success? ? out.split(/\r?\n/).map(&:strip).reject(&:empty?) : [] end rescue => e Kettle::Dev.debug_error(e, __method__) [] end |
#remotes_with_urls ⇒ Hash{String=>String}
Returns remote name => fetch URL.
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 |
# File 'lib/kettle/dev/git_adapter.rb', line 321 def remotes_with_urls if @backend == :gem @git.remotes.each_with_object({}) do |r, h| begin h[r.name] = r.url rescue => e Kettle::Dev.debug_error(e, __method__) # ignore end end else out, status = git_capture2("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 => e Kettle::Dev.debug_error(e, __method__) {} end |
#reset_soft(ref) ⇒ Boolean
Soft reset to a ref.
168 169 170 171 172 173 |
# File 'lib/kettle/dev/git_adapter.rb', line 168 def reset_soft(ref) git_system("reset", "--soft", ref.to_s) rescue => e Kettle::Dev.debug_error(e, __method__) false end |
#tag_annotated(tag, message) ⇒ Boolean
Create an annotated tag.
157 158 159 160 161 162 |
# File 'lib/kettle/dev/git_adapter.rb', line 157 def tag_annotated(tag, ) git_system("tag", "-a", tag.to_s, "-m", .to_s) rescue => e Kettle::Dev.debug_error(e, __method__) false end |