Module: Ace::Git::Atoms::CommandExecutor

Defined in:
lib/ace/git/atoms/command_executor.rb

Overview

Pure functions for executing git commands safely Migrated from ace-git-diff

Lock Retry Behavior:

  • Automatically retries git commands that encounter .git/index.lock errors

  • Uses progressive delays: 1s, 2s, 3s, 4s (total 10s across 4 retries)

  • On each retry, attempts to clean orphaned locks (dead PID) or stale locks (>10s)

  • Configurable via lock_retry section in .ace/git/config.yml

  • Only git commands are retried; non-git commands fail immediately

This retry logic prevents “Unable to create .git/index.lock” errors that commonly occur in multi-worktree environments or when operations are interrupted (Ctrl+C, crashes, timeouts).

Class Method Summary collapse

Class Method Details

.changed_files(range = nil) ⇒ Array<String>

Get list of changed files

Parameters:

  • range (String) (defaults to: nil)

    Git range to check

Returns:

  • (Array<String>)

    List of changed file paths



209
210
211
212
213
214
215
216
217
218
# File 'lib/ace/git/atoms/command_executor.rb', line 209

def changed_files(range = nil)
  range = "origin/main...HEAD" if range.nil? && ref_exists?("origin/main")
  args = ["git", "diff", "--name-only"]
  args << range if range && !range.empty?

  result = execute(*args)
  return [] unless result[:success]

  result[:output].split("\n").map(&:strip).reject(&:empty?)
end

.current_branchString?

Get current branch name or commit SHA if detached

Returns:

  • (String, nil)

    Current branch name, commit SHA (if detached), or nil on error



180
181
182
183
184
185
186
187
188
189
190
# File 'lib/ace/git/atoms/command_executor.rb', line 180

def current_branch
  result = execute("git", "rev-parse", "--abbrev-ref", "HEAD")
  return nil unless result[:success]

  branch = result[:output].strip
  return branch unless branch == "HEAD"

  # Detached HEAD - return commit SHA instead
  sha_result = execute("git", "rev-parse", "HEAD")
  sha_result[:success] ? sha_result[:output].strip : nil
end

.execute(*command_parts, timeout: Ace::Git.git_timeout, env: nil) ⇒ Hash

Execute a command safely using array arguments to prevent command injection

Parameters:

  • command_parts (Array<String>)

    Command parts to execute

  • timeout (Integer) (defaults to: Ace::Git.git_timeout)

    Timeout in seconds (default from config)

  • env (Hash) (defaults to: nil)

    Optional environment variables to set for the command

Returns:

  • (Hash)

    Result with output, error, and success status



29
30
31
32
33
34
35
36
37
38
39
# File 'lib/ace/git/atoms/command_executor.rb', line 29

def execute(*command_parts, timeout: Ace::Git.git_timeout, env: nil)
  # Check if lock retry is enabled (default: true)
  lock_retry_config = Ace::Git.config["lock_retry"]
  lock_retry_enabled = lock_retry_config.nil? || lock_retry_config["enabled"] != false

  if lock_retry_enabled && !command_parts.empty? && command_parts.first == "git"
    execute_with_lock_retry(command_parts, timeout: timeout, env: env, config: lock_retry_config)
  else
    execute_once(command_parts, timeout: timeout, env: env)
  end
end

.git_diff(*args, raise_on_error: false) ⇒ String

Execute git diff command

Parameters:

  • args (Array<String>)

    Arguments to pass to git diff

  • raise_on_error (Boolean) (defaults to: false)

    If true, raises GitError on failure

Returns:

  • (String)

    Diff output (empty string if no changes, raises on error if raise_on_error)



146
147
148
149
150
151
152
153
154
155
# File 'lib/ace/git/atoms/command_executor.rb', line 146

def git_diff(*args, raise_on_error: false)
  result = execute("git", "diff", *args)
  if result[:success]
    result[:output]
  elsif raise_on_error
    raise Ace::Git::GitError, "git diff failed: #{result[:error]}"
  else
    ""
  end
end

.has_staged_changes?Boolean

Check if there are staged changes

Returns:

  • (Boolean)

    True if there are staged changes



228
229
230
# File 'lib/ace/git/atoms/command_executor.rb', line 228

def has_staged_changes?
  !staged_diff.strip.empty?
end

.has_unstaged_changes?Boolean

Check if there are unstaged changes

Returns:

  • (Boolean)

    True if there are unstaged changes



222
223
224
# File 'lib/ace/git/atoms/command_executor.rb', line 222

def has_unstaged_changes?
  !working_diff.strip.empty?
end

.has_untracked_changes?Boolean

Check if there are untracked changes

Returns:

  • (Boolean)

    True if there are untracked files



234
235
236
237
# File 'lib/ace/git/atoms/command_executor.rb', line 234

def has_untracked_changes?
  result = execute("git", "ls-files", "--others", "--exclude-standard")
  result[:success] && !result[:output].strip.empty?
end

.in_git_repo?Boolean

Check if we’re in a git repository

Returns:

  • (Boolean)

    True if in a git repository



173
174
175
176
# File 'lib/ace/git/atoms/command_executor.rb', line 173

def in_git_repo?
  result = execute("git", "rev-parse", "--git-dir")
  result[:success]
end

.ref_exists?(ref) ⇒ Boolean

Check whether a git reference exists in the repository.

Parameters:

  • ref (String)

    Git ref to validate

Returns:

  • (Boolean)

    True if ref resolves, false otherwise



243
244
245
246
247
248
# File 'lib/ace/git/atoms/command_executor.rb', line 243

def ref_exists?(ref)
  return false if ref.nil? || ref.strip.empty?

  result = execute("git", "rev-parse", "--verify", "#{ref}^{}")
  result[:success]
end

.repo_rootString?

Get repository root path

Returns:

  • (String, nil)

    Repository root path or nil on error



194
195
196
197
# File 'lib/ace/git/atoms/command_executor.rb', line 194

def repo_root
  result = execute_once(["git", "rev-parse", "--show-toplevel"], timeout: Ace::Git.git_timeout, env: nil)
  result[:success] ? result[:output].strip : nil
end

.staged_diffString

Get staged changes

Returns:

  • (String)

    Diff of staged changes



159
160
161
162
# File 'lib/ace/git/atoms/command_executor.rb', line 159

def staged_diff
  result = execute("git", "diff", "--cached")
  result[:success] ? result[:output] : ""
end

.tracking_branchString?

Get remote tracking branch

Returns:

  • (String, nil)

    Remote tracking branch or nil



201
202
203
204
# File 'lib/ace/git/atoms/command_executor.rb', line 201

def tracking_branch
  result = execute("git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}")
  result[:success] ? result[:output].strip : nil
end

.working_diffString

Get working directory changes

Returns:

  • (String)

    Diff of working directory changes



166
167
168
169
# File 'lib/ace/git/atoms/command_executor.rb', line 166

def working_diff
  result = execute("git", "diff")
  result[:success] ? result[:output] : ""
end