ace-git 0.23.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ace-defaults/git/config.yml +16 -1
- data/CHANGELOG.md +17 -0
- data/docs/getting-started.md +4 -4
- data/docs/usage.md +13 -44
- data/lib/ace/git/atoms/command_executor.rb +1 -1
- data/lib/ace/git/atoms/pr_identifier.rb +66 -0
- data/lib/ace/git/atoms/server_url.rb +56 -0
- data/lib/ace/git/atoms/status_formatter.rb +22 -27
- data/lib/ace/git/cli/commands/status.rb +16 -6
- data/lib/ace/git/cli.rb +1 -4
- data/lib/ace/git/errors.rb +56 -0
- data/lib/ace/git/models/diff_config.rb +2 -2
- data/lib/ace/git/models/repo_status.rb +1 -1
- data/lib/ace/git/molecules/branch_reader.rb +13 -7
- data/lib/ace/git/molecules/config_loader.rb +1 -1
- data/lib/ace/git/organisms/repo_status_loader.rb +72 -136
- data/lib/ace/git/providers/base.rb +115 -0
- data/lib/ace/git/providers/evidence.rb +26 -0
- data/lib/ace/git/providers.rb +92 -0
- data/lib/ace/git/resolved_server.rb +20 -0
- data/lib/ace/git/server_registry.rb +156 -0
- data/lib/ace/git/version.rb +1 -1
- data/lib/ace/git.rb +13 -14
- metadata +17 -14
- data/lib/ace/git/atoms/pr_identifier_parser.rb +0 -88
- data/lib/ace/git/cli/commands/pr.rb +0 -119
- data/lib/ace/git/molecules/gh_cli_executor.rb +0 -77
- data/lib/ace/git/molecules/github_issue_sync.rb +0 -248
- data/lib/ace/git/molecules/pr_metadata_fetcher.rb +0 -288
data/lib/ace/git.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "ace/support/config"
|
|
4
4
|
require_relative "git/version"
|
|
5
|
+
require_relative "git/errors"
|
|
5
6
|
|
|
6
7
|
module Ace
|
|
7
8
|
module Git
|
|
@@ -12,14 +13,9 @@ module Ace
|
|
|
12
13
|
# - Organisms: Orchestrate molecules, propagate or wrap exceptions
|
|
13
14
|
# - Commands: Catch exceptions and return exit codes (0=success, 1=error)
|
|
14
15
|
#
|
|
15
|
-
# All custom exceptions inherit from Ace::Git::Error for consistent catching
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class ConfigError < Error; end
|
|
19
|
-
class GhNotInstalledError < Error; end
|
|
20
|
-
class GhAuthenticationError < Error; end
|
|
21
|
-
class PrNotFoundError < Error; end
|
|
22
|
-
class TimeoutError < Error; end
|
|
16
|
+
# All custom exceptions inherit from Ace::Git::Error for consistent catching;
|
|
17
|
+
# they are defined in git/errors.rb (local git failures plus the forge server
|
|
18
|
+
# resolution and provider interaction taxonomy).
|
|
23
19
|
|
|
24
20
|
# Mutex for thread-safe config initialization
|
|
25
21
|
@config_mutex = Mutex.new
|
|
@@ -124,7 +120,7 @@ module Ace
|
|
|
124
120
|
normalized = normalize_keys(git_section)
|
|
125
121
|
|
|
126
122
|
# Copy top-level settings
|
|
127
|
-
%w[default_branch remote verbose timeout network_timeout].each do |key|
|
|
123
|
+
%w[default_branch remote verbose timeout network_timeout servers].each do |key|
|
|
128
124
|
config[key] = normalized[key] if normalized.key?(key)
|
|
129
125
|
end
|
|
130
126
|
|
|
@@ -170,7 +166,7 @@ module Ace
|
|
|
170
166
|
config["timeout"] || 30
|
|
171
167
|
end
|
|
172
168
|
|
|
173
|
-
# Timeout for network operations (
|
|
169
|
+
# Timeout for network operations (provider CLIs, remote operations)
|
|
174
170
|
# @return [Integer] Timeout in seconds (default: 60)
|
|
175
171
|
def self.network_timeout
|
|
176
172
|
config["network_timeout"] || 60
|
|
@@ -198,6 +194,8 @@ end
|
|
|
198
194
|
|
|
199
195
|
# Require ATOM architecture components
|
|
200
196
|
require_relative "git/atoms/command_executor"
|
|
197
|
+
require_relative "git/atoms/server_url"
|
|
198
|
+
require_relative "git/atoms/pr_identifier"
|
|
201
199
|
require_relative "git/atoms/pattern_filter"
|
|
202
200
|
require_relative "git/atoms/diff_parser"
|
|
203
201
|
require_relative "git/atoms/diff_numstat_parser"
|
|
@@ -205,7 +203,6 @@ require_relative "git/atoms/file_grouper"
|
|
|
205
203
|
require_relative "git/atoms/grouped_stats_formatter"
|
|
206
204
|
require_relative "git/atoms/date_resolver"
|
|
207
205
|
require_relative "git/atoms/task_pattern_extractor"
|
|
208
|
-
require_relative "git/atoms/pr_identifier_parser"
|
|
209
206
|
require_relative "git/atoms/repository_state_detector"
|
|
210
207
|
require_relative "git/atoms/repository_checker"
|
|
211
208
|
require_relative "git/atoms/git_scope_filter"
|
|
@@ -218,14 +215,16 @@ require_relative "git/molecules/diff_generator"
|
|
|
218
215
|
require_relative "git/molecules/config_loader"
|
|
219
216
|
require_relative "git/molecules/diff_filter"
|
|
220
217
|
require_relative "git/molecules/branch_reader"
|
|
221
|
-
require_relative "git/molecules/pr_metadata_fetcher"
|
|
222
218
|
require_relative "git/molecules/recent_commits_fetcher"
|
|
223
219
|
require_relative "git/molecules/git_status_fetcher"
|
|
224
|
-
require_relative "git/molecules/gh_cli_executor"
|
|
225
|
-
require_relative "git/molecules/github_issue_sync"
|
|
226
220
|
|
|
227
221
|
require_relative "git/organisms/diff_orchestrator"
|
|
228
222
|
require_relative "git/organisms/repo_status_loader"
|
|
223
|
+
require_relative "git/resolved_server"
|
|
224
|
+
require_relative "git/server_registry"
|
|
225
|
+
require_relative "git/providers/base"
|
|
226
|
+
require_relative "git/providers/evidence"
|
|
227
|
+
require_relative "git/providers"
|
|
229
228
|
|
|
230
229
|
require_relative "git/models/diff_result"
|
|
231
230
|
require_relative "git/models/diff_config"
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-git
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.24.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-09-
|
|
10
|
+
date: 2026-09-23 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-support-config
|
|
@@ -94,8 +94,8 @@ dependencies:
|
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
95
|
version: '13.0'
|
|
96
96
|
description: ace-git gives developers and coding agents focused git context commands
|
|
97
|
-
and guided workflows for rebases
|
|
98
|
-
|
|
97
|
+
and guided workflows for rebases and commit reorganization, with smart diff output,
|
|
98
|
+
a forge server registry, and a provider contract backed by pluggable provider gems.
|
|
99
99
|
email:
|
|
100
100
|
- mc@cs3b.com
|
|
101
101
|
executables:
|
|
@@ -149,9 +149,10 @@ files:
|
|
|
149
149
|
- lib/ace/git/atoms/grouped_stats_formatter.rb
|
|
150
150
|
- lib/ace/git/atoms/lock_error_detector.rb
|
|
151
151
|
- lib/ace/git/atoms/pattern_filter.rb
|
|
152
|
-
- lib/ace/git/atoms/
|
|
152
|
+
- lib/ace/git/atoms/pr_identifier.rb
|
|
153
153
|
- lib/ace/git/atoms/repository_checker.rb
|
|
154
154
|
- lib/ace/git/atoms/repository_state_detector.rb
|
|
155
|
+
- lib/ace/git/atoms/server_url.rb
|
|
155
156
|
- lib/ace/git/atoms/stale_lock_cleaner.rb
|
|
156
157
|
- lib/ace/git/atoms/status_formatter.rb
|
|
157
158
|
- lib/ace/git/atoms/task_pattern_extractor.rb
|
|
@@ -159,8 +160,8 @@ files:
|
|
|
159
160
|
- lib/ace/git/cli.rb
|
|
160
161
|
- lib/ace/git/cli/commands/branch.rb
|
|
161
162
|
- lib/ace/git/cli/commands/diff.rb
|
|
162
|
-
- lib/ace/git/cli/commands/pr.rb
|
|
163
163
|
- lib/ace/git/cli/commands/status.rb
|
|
164
|
+
- lib/ace/git/errors.rb
|
|
164
165
|
- lib/ace/git/models/diff_config.rb
|
|
165
166
|
- lib/ace/git/models/diff_result.rb
|
|
166
167
|
- lib/ace/git/models/repo_status.rb
|
|
@@ -168,22 +169,24 @@ files:
|
|
|
168
169
|
- lib/ace/git/molecules/config_loader.rb
|
|
169
170
|
- lib/ace/git/molecules/diff_filter.rb
|
|
170
171
|
- lib/ace/git/molecules/diff_generator.rb
|
|
171
|
-
- lib/ace/git/molecules/gh_cli_executor.rb
|
|
172
172
|
- lib/ace/git/molecules/git_status_fetcher.rb
|
|
173
|
-
- lib/ace/git/molecules/github_issue_sync.rb
|
|
174
|
-
- lib/ace/git/molecules/pr_metadata_fetcher.rb
|
|
175
173
|
- lib/ace/git/molecules/recent_commits_fetcher.rb
|
|
176
174
|
- lib/ace/git/organisms/diff_orchestrator.rb
|
|
177
175
|
- lib/ace/git/organisms/repo_status_loader.rb
|
|
176
|
+
- lib/ace/git/providers.rb
|
|
177
|
+
- lib/ace/git/providers/base.rb
|
|
178
|
+
- lib/ace/git/providers/evidence.rb
|
|
179
|
+
- lib/ace/git/resolved_server.rb
|
|
180
|
+
- lib/ace/git/server_registry.rb
|
|
178
181
|
- lib/ace/git/version.rb
|
|
179
|
-
homepage: https://
|
|
182
|
+
homepage: https://forgejo.tail6c0887.ts.net/cs3b/ace
|
|
180
183
|
licenses:
|
|
181
184
|
- MIT
|
|
182
185
|
metadata:
|
|
183
186
|
allowed_push_host: https://rubygems.org
|
|
184
|
-
homepage_uri: https://
|
|
185
|
-
source_code_uri: https://
|
|
186
|
-
changelog_uri: https://
|
|
187
|
+
homepage_uri: https://forgejo.tail6c0887.ts.net/cs3b/ace
|
|
188
|
+
source_code_uri: https://forgejo.tail6c0887.ts.net/cs3b/ace/src/branch/main/ace-git
|
|
189
|
+
changelog_uri: https://forgejo.tail6c0887.ts.net/cs3b/ace/src/branch/main/ace-git/CHANGELOG.md
|
|
187
190
|
rdoc_options: []
|
|
188
191
|
require_paths:
|
|
189
192
|
- lib
|
|
@@ -200,5 +203,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
200
203
|
requirements: []
|
|
201
204
|
rubygems_version: 3.6.9
|
|
202
205
|
specification_version: 4
|
|
203
|
-
summary: Git workflows and context commands for developers and AI agents
|
|
206
|
+
summary: Forge-neutral Git workflows and context commands for developers and AI agents
|
|
204
207
|
test_files: []
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ace
|
|
4
|
-
module Git
|
|
5
|
-
module Atoms
|
|
6
|
-
# Parse PR identifiers into structured format
|
|
7
|
-
#
|
|
8
|
-
# Supports three formats:
|
|
9
|
-
# - Simple number: "123"
|
|
10
|
-
# - Qualified reference: "owner/repo#456"
|
|
11
|
-
# - GitHub URL: "https://github.com/owner/repo/pull/789"
|
|
12
|
-
#
|
|
13
|
-
# Consolidated from ace-bundle PrIdentifierParser
|
|
14
|
-
module PrIdentifierParser
|
|
15
|
-
# Parsed PR identifier result
|
|
16
|
-
ParseResult = Data.define(:number, :repo, :gh_format)
|
|
17
|
-
|
|
18
|
-
# Parse a PR identifier string
|
|
19
|
-
#
|
|
20
|
-
# Returns nil for nil/empty input (no PR specified), raises ArgumentError for
|
|
21
|
-
# invalid formats. This design allows callers to distinguish between "no PR"
|
|
22
|
-
# (nil input -> nil result) and "invalid PR" (bad format -> exception).
|
|
23
|
-
#
|
|
24
|
-
# @param input [String, Integer] PR identifier
|
|
25
|
-
# @return [ParseResult, nil] Parsed identifier with number, repo, and gh_format,
|
|
26
|
-
# or nil if input is nil/empty
|
|
27
|
-
# @raise [ArgumentError] if identifier format is invalid (non-empty but malformed)
|
|
28
|
-
#
|
|
29
|
-
# @example Simple number
|
|
30
|
-
# parse(123)
|
|
31
|
-
# # => ParseResult(number: "123", repo: nil, gh_format: "123")
|
|
32
|
-
#
|
|
33
|
-
# @example Qualified reference
|
|
34
|
-
# parse("owner/repo#456")
|
|
35
|
-
# # => ParseResult(number: "456", repo: "owner/repo", gh_format: "owner/repo#456")
|
|
36
|
-
#
|
|
37
|
-
# @example GitHub URL
|
|
38
|
-
# parse("https://github.com/owner/repo/pull/789")
|
|
39
|
-
# # => ParseResult(number: "789", repo: "owner/repo", gh_format: "owner/repo#789")
|
|
40
|
-
#
|
|
41
|
-
# @example Nil/empty input
|
|
42
|
-
# parse(nil) # => nil
|
|
43
|
-
# parse("") # => nil
|
|
44
|
-
# parse(" ") # => nil
|
|
45
|
-
# Maximum length for PR identifier to prevent ReDoS attacks
|
|
46
|
-
# GitHub URLs are typically under 200 chars, this provides generous margin
|
|
47
|
-
MAX_IDENTIFIER_LENGTH = 256
|
|
48
|
-
|
|
49
|
-
def self.parse(input)
|
|
50
|
-
return nil if input.nil?
|
|
51
|
-
|
|
52
|
-
input_str = input.to_s.strip
|
|
53
|
-
return nil if input_str.empty?
|
|
54
|
-
|
|
55
|
-
# Validate length to prevent ReDoS attacks on regex patterns
|
|
56
|
-
if input_str.length > MAX_IDENTIFIER_LENGTH
|
|
57
|
-
raise ArgumentError, "PR identifier too long (max #{MAX_IDENTIFIER_LENGTH} characters)"
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
case input_str
|
|
61
|
-
when /^(\d+)$/
|
|
62
|
-
# Simple PR number: "123"
|
|
63
|
-
number = ::Regexp.last_match(1)
|
|
64
|
-
# Reject zero: GitHub PR numbers are positive integers starting from 1
|
|
65
|
-
raise ArgumentError, "Invalid PR identifier format: #{input_str}" if number.to_i.zero?
|
|
66
|
-
# Normalize to canonical form (strip leading zeros) for consistent gh_format
|
|
67
|
-
canonical_number = number.to_i.to_s
|
|
68
|
-
ParseResult.new(number: canonical_number, repo: nil, gh_format: canonical_number)
|
|
69
|
-
|
|
70
|
-
when /^(?<repo>[a-zA-Z0-9_\-.]+\/[a-zA-Z0-9_\-.]+)#(?<number>\d+)$/
|
|
71
|
-
# Qualified reference: "owner/repo#456"
|
|
72
|
-
# GitHub owner/repo names: alphanumeric, hyphens, underscores, dots only
|
|
73
|
-
match = ::Regexp.last_match
|
|
74
|
-
ParseResult.new(number: match[:number], repo: match[:repo], gh_format: "#{match[:repo]}##{match[:number]}")
|
|
75
|
-
|
|
76
|
-
when %r{github\.com/(?<repo>[^/]+/[^/]+)/pull/(?<number>\d+)}
|
|
77
|
-
# GitHub URL: "https://github.com/owner/repo/pull/789"
|
|
78
|
-
match = ::Regexp.last_match
|
|
79
|
-
ParseResult.new(number: match[:number], repo: match[:repo], gh_format: "#{match[:repo]}##{match[:number]}")
|
|
80
|
-
|
|
81
|
-
else
|
|
82
|
-
raise ArgumentError, "Invalid PR identifier format: #{input_str}"
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "ace/support/cli"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Git
|
|
8
|
-
module CLI
|
|
9
|
-
module Commands
|
|
10
|
-
# ace-support-cli command for showing PR information
|
|
11
|
-
class Pr < Ace::Support::Cli::Command
|
|
12
|
-
include Ace::Support::Cli::Base
|
|
13
|
-
|
|
14
|
-
desc "Show PR information"
|
|
15
|
-
|
|
16
|
-
argument :number, required: false, desc: "PR number (auto-detected if not provided)"
|
|
17
|
-
|
|
18
|
-
option :format, type: :string, aliases: ["f"], default: "markdown",
|
|
19
|
-
desc: "Output format: markdown, json"
|
|
20
|
-
option :with_diff, type: :boolean, default: false,
|
|
21
|
-
desc: "Include PR diff in output"
|
|
22
|
-
|
|
23
|
-
# Standard options
|
|
24
|
-
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
25
|
-
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
26
|
-
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
27
|
-
|
|
28
|
-
def call(number: nil, **options)
|
|
29
|
-
# Check if gh is installed
|
|
30
|
-
unless Molecules::PrMetadataFetcher.gh_installed?
|
|
31
|
-
raise Ace::Support::Cli::Error.new("GitHub CLI (gh) not installed. Install with: brew install gh")
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Determine PR identifier
|
|
35
|
-
identifier = if number
|
|
36
|
-
number.to_s
|
|
37
|
-
else
|
|
38
|
-
# Try to find PR for current branch
|
|
39
|
-
found = Molecules::PrMetadataFetcher.find_pr_for_branch
|
|
40
|
-
unless found
|
|
41
|
-
raise Ace::Support::Cli::Error.new("No PR found for current branch. Specify a PR number.")
|
|
42
|
-
end
|
|
43
|
-
found
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Fetch PR data
|
|
47
|
-
result = if options[:with_diff]
|
|
48
|
-
Molecules::PrMetadataFetcher.fetch_pr(identifier)
|
|
49
|
-
else
|
|
50
|
-
Molecules::PrMetadataFetcher.fetch_metadata(identifier)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
unless result[:success]
|
|
54
|
-
raise Ace::Support::Cli::Error.new(result[:error])
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Output based on format
|
|
58
|
-
case options[:format]
|
|
59
|
-
when "json"
|
|
60
|
-
output_data = {metadata: result[:metadata]}
|
|
61
|
-
output_data[:diff] = result[:diff] if options[:with_diff]
|
|
62
|
-
puts JSON.pretty_generate(output_data)
|
|
63
|
-
else
|
|
64
|
-
output_markdown(result[:metadata], result[:diff], options)
|
|
65
|
-
end
|
|
66
|
-
rescue Ace::Git::Error => e
|
|
67
|
-
raise Ace::Support::Cli::Error.new(e.message)
|
|
68
|
-
rescue ArgumentError => e
|
|
69
|
-
raise Ace::Support::Cli::Error.new(e.message)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
private
|
|
73
|
-
|
|
74
|
-
def output_markdown(metadata, diff, options)
|
|
75
|
-
lines = []
|
|
76
|
-
|
|
77
|
-
# Header line: # PR #82: Title... [OPEN]
|
|
78
|
-
header = "# PR ##{metadata["number"]}"
|
|
79
|
-
header += ": #{metadata["title"]}" if metadata["title"]
|
|
80
|
-
header += " [#{metadata["state"]}]" if metadata["state"]
|
|
81
|
-
lines << header
|
|
82
|
-
|
|
83
|
-
# Branch line: Branch: source → target | Draft: No
|
|
84
|
-
branch_parts = []
|
|
85
|
-
if metadata["headRefName"] && metadata["baseRefName"]
|
|
86
|
-
branch_parts << "Branch: #{metadata["headRefName"]} → #{metadata["baseRefName"]}"
|
|
87
|
-
elsif metadata["headRefName"]
|
|
88
|
-
branch_parts << "Branch: #{metadata["headRefName"]}"
|
|
89
|
-
elsif metadata["baseRefName"]
|
|
90
|
-
branch_parts << "Target: #{metadata["baseRefName"]}"
|
|
91
|
-
end
|
|
92
|
-
branch_parts << "Draft: #{metadata["isDraft"] ? "Yes" : "No"}" if metadata.key?("isDraft")
|
|
93
|
-
lines << branch_parts.join(" | ") unless branch_parts.empty?
|
|
94
|
-
|
|
95
|
-
# Author line
|
|
96
|
-
if metadata["author"]
|
|
97
|
-
author = metadata["author"].is_a?(Hash) ? metadata["author"]["login"] : metadata["author"]
|
|
98
|
-
lines << "Author: #{author}"
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# URL line
|
|
102
|
-
lines << "URL: #{metadata["url"]}" if metadata["url"]
|
|
103
|
-
|
|
104
|
-
if options[:with_diff] && diff
|
|
105
|
-
lines << ""
|
|
106
|
-
lines << "## Diff"
|
|
107
|
-
lines << ""
|
|
108
|
-
lines << "```diff"
|
|
109
|
-
lines << diff
|
|
110
|
-
lines << "```"
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
puts lines.join("\n")
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
end
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "open3"
|
|
4
|
-
require "timeout"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Git
|
|
8
|
-
module Molecules
|
|
9
|
-
# Safely execute gh CLI commands with timeout and structured output.
|
|
10
|
-
class GhCliExecutor
|
|
11
|
-
DEFAULT_GH_TIMEOUT = 30
|
|
12
|
-
DEFAULT_SIMPLE_TIMEOUT = 10
|
|
13
|
-
|
|
14
|
-
def self.execute(subcommand, args = [], timeout: nil)
|
|
15
|
-
check_installed
|
|
16
|
-
timeout_seconds = timeout || Ace::Git.network_timeout || DEFAULT_GH_TIMEOUT
|
|
17
|
-
command = ["gh", subcommand] + args
|
|
18
|
-
run_command(command, timeout_seconds)
|
|
19
|
-
rescue Timeout::Error
|
|
20
|
-
raise Ace::Git::TimeoutError, "gh command timed out after #{timeout_seconds} seconds"
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def self.check_installed
|
|
24
|
-
result = execute_simple("--version")
|
|
25
|
-
raise Ace::Git::GhNotInstalledError unless result[:success]
|
|
26
|
-
|
|
27
|
-
true
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def self.check_authenticated
|
|
31
|
-
result = execute_simple("auth", ["status"])
|
|
32
|
-
raise Ace::Git::GhAuthenticationError unless result[:success]
|
|
33
|
-
|
|
34
|
-
{
|
|
35
|
-
authenticated: true,
|
|
36
|
-
username: extract_username(result[:stderr])
|
|
37
|
-
}
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def self.execute_simple(command, args = [], timeout_seconds = nil)
|
|
41
|
-
timeout_seconds ||= Ace::Git.network_timeout || DEFAULT_SIMPLE_TIMEOUT
|
|
42
|
-
cmd = ["gh", command] + args
|
|
43
|
-
run_command(cmd, timeout_seconds)
|
|
44
|
-
rescue Timeout::Error
|
|
45
|
-
{
|
|
46
|
-
success: false,
|
|
47
|
-
stdout: "",
|
|
48
|
-
stderr: "Command timed out",
|
|
49
|
-
exit_code: 1
|
|
50
|
-
}
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def self.extract_username(output)
|
|
54
|
-
match = output.match(/Logged in to .+ as (\S+)/)
|
|
55
|
-
match ? match[1] : nil
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def self.run_command(command, timeout_seconds)
|
|
59
|
-
stdout_str, stderr_str, status = Timeout.timeout(timeout_seconds) do
|
|
60
|
-
Open3.capture3(*command)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
{
|
|
64
|
-
success: status.success?,
|
|
65
|
-
stdout: stdout_str,
|
|
66
|
-
stderr: stderr_str,
|
|
67
|
-
exit_code: status.exitstatus
|
|
68
|
-
}
|
|
69
|
-
rescue Errno::ENOENT
|
|
70
|
-
raise Ace::Git::GhNotInstalledError
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
private_class_method :execute_simple, :extract_username, :run_command
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|