ace-git 0.23.0 → 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.
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
- class Error < StandardError; end
17
- class GitError < Error; end
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 (gh CLI, remote 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.23.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-08-13 00:00:00.000000000 Z
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, pull requests, and commit reorganization, with
98
- smart diff output and Git 2.23+ guardrails.
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/pr_identifier_parser.rb
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://github.com/cs3b/ace/tree/main/ace-git
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://github.com/cs3b/ace/tree/main/ace-git
185
- source_code_uri: https://github.com/cs3b/ace/tree/main/ace-git/
186
- changelog_uri: https://github.com/cs3b/ace/blob/main/ace-git/CHANGELOG.md
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