ace-git 0.23.1 → 0.25.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 +21 -1
- data/CHANGELOG.md +67 -0
- data/docs/getting-started.md +4 -4
- data/docs/usage.md +53 -38
- 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/pr_reference.rb +76 -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/pr.rb +159 -84
- data/lib/ace/git/cli/commands/status.rb +16 -6
- data/lib/ace/git/cli.rb +8 -3
- data/lib/ace/git/errors.rb +82 -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/pull_request_lifecycle.rb +172 -0
- data/lib/ace/git/organisms/repo_status_loader.rb +72 -136
- data/lib/ace/git/providers/base.rb +221 -0
- data/lib/ace/git/providers/evidence.rb +96 -0
- data/lib/ace/git/providers.rb +121 -0
- data/lib/ace/git/resolved_server.rb +20 -0
- data/lib/ace/git/server_registry.rb +202 -0
- data/lib/ace/git/version.rb +1 -1
- data/lib/ace/git.rb +15 -14
- metadata +19 -13
- data/lib/ace/git/atoms/pr_identifier_parser.rb +0 -88
- 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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Git
|
|
5
|
+
# Normalized evidence types produced by every provider implementation.
|
|
6
|
+
#
|
|
7
|
+
# Provider packages translate their CLI responses into exactly these
|
|
8
|
+
# shapes, so consumers can stay forge-neutral. All fields except the
|
|
9
|
+
# identifying ones may be nil when a provider cannot supply them.
|
|
10
|
+
#
|
|
11
|
+
# Pull request state values: :open, :merged, :closed. Issue state values:
|
|
12
|
+
# :open, :closed.
|
|
13
|
+
ProviderPullRequest = Data.define(
|
|
14
|
+
:server_name, :number, :title, :state, :head_ref, :base_ref,
|
|
15
|
+
:head_sha, :author, :url, :draft, :merged_at,
|
|
16
|
+
:head_repository_url, :base_repository_url, :merge_commit_sha
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
ProviderIssue = Data.define(:server_name, :number, :title, :state, :author, :url, :labels)
|
|
20
|
+
|
|
21
|
+
# Normalized check/CI evidence for one check run.
|
|
22
|
+
ProviderCheck = Data.define(:server_name, :name, :state, :conclusion, :url)
|
|
23
|
+
|
|
24
|
+
# Normalized repository evidence.
|
|
25
|
+
ProviderRepository = Data.define(:server_name, :full_name, :default_branch, :url)
|
|
26
|
+
|
|
27
|
+
# Exact pull request source/base identity used for idempotent create
|
|
28
|
+
# reconciliation, expected-head proof, and cleanup consent binding.
|
|
29
|
+
#
|
|
30
|
+
# `head_sha` is the exact head commit the identity was captured at; every
|
|
31
|
+
# mutation decision that depends on head state must compare against it.
|
|
32
|
+
ProviderPullRequestIdentity = Data.define(
|
|
33
|
+
:head_repository_url, :head_ref, :base_repository_url, :base_ref, :head_sha
|
|
34
|
+
) do
|
|
35
|
+
# @return [Hash] plain, JSON-serializable representation
|
|
36
|
+
def to_h
|
|
37
|
+
{
|
|
38
|
+
head_repository_url: head_repository_url,
|
|
39
|
+
head_ref: head_ref,
|
|
40
|
+
base_repository_url: base_repository_url,
|
|
41
|
+
base_ref: base_ref,
|
|
42
|
+
head_sha: head_sha
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Normalized result of one provider lifecycle mutation (create, update,
|
|
48
|
+
# ready, merge). Receipts are immutable evidence: the operation performed,
|
|
49
|
+
# the pull request state observed afterwards, and — for creates only —
|
|
50
|
+
# whether the request created a new PR or reconciled to an existing one
|
|
51
|
+
# (`:created` / `:existing`; nil for non-create operations).
|
|
52
|
+
ProviderMutationReceipt = Data.define(:server_name, :operation, :pull_request, :idempotency) do
|
|
53
|
+
# @return [Hash] plain, JSON-serializable representation
|
|
54
|
+
def to_h
|
|
55
|
+
{
|
|
56
|
+
server_name: server_name,
|
|
57
|
+
operation: operation,
|
|
58
|
+
idempotency: idempotency,
|
|
59
|
+
pull_request: pull_request&.to_h
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Cleanup proof status vocabulary. Only :merged is confirmed remote merge
|
|
65
|
+
# proof; every other status retains the candidate. Provider transport
|
|
66
|
+
# failures (:offline, :authentication_error, :malformed) are distinct from
|
|
67
|
+
# object states and never relabel a failed lookup as merged.
|
|
68
|
+
CLEANUP_PROOF_STATUSES = %i[
|
|
69
|
+
no_pr open closed_unmerged merged offline authentication_error malformed
|
|
70
|
+
].freeze
|
|
71
|
+
|
|
72
|
+
# Normalized provider proof about the pull request behind a cleanup
|
|
73
|
+
# candidate (worktree, local ref, or remote ref). `status` is one of
|
|
74
|
+
# {CLEANUP_PROOF_STATUSES}; pr/head fields are nil unless a PR was found.
|
|
75
|
+
ProviderCleanupProof = Data.define(
|
|
76
|
+
:server_name, :status, :pr_number, :pr_url, :head_sha, :merge_commit_sha
|
|
77
|
+
) do
|
|
78
|
+
# @return [Boolean] true only for confirmed remote merge proof
|
|
79
|
+
def merged?
|
|
80
|
+
status == :merged
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @return [Hash] plain, JSON-serializable representation
|
|
84
|
+
def to_h
|
|
85
|
+
{
|
|
86
|
+
server_name: server_name,
|
|
87
|
+
status: status,
|
|
88
|
+
pr_number: pr_number,
|
|
89
|
+
pr_url: pr_url,
|
|
90
|
+
head_sha: head_sha,
|
|
91
|
+
merge_commit_sha: merge_commit_sha
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "providers/base"
|
|
5
|
+
require_relative "providers/evidence"
|
|
6
|
+
|
|
7
|
+
module Ace
|
|
8
|
+
module Git
|
|
9
|
+
# Registry of provider implementations owned by provider packages.
|
|
10
|
+
#
|
|
11
|
+
# The core never executes provider CLIs. Provider packages register their
|
|
12
|
+
# contract implementation here when loaded; downstream consumers then
|
|
13
|
+
# obtain a provider via {Providers.for}.
|
|
14
|
+
#
|
|
15
|
+
# @example Registering (inside a provider package)
|
|
16
|
+
# Ace::Git::Providers.register(:acme, Ace::Git::Acme::Provider)
|
|
17
|
+
#
|
|
18
|
+
# @example Resolving (in consumers)
|
|
19
|
+
# server = Ace::Git::ServerRegistry.resolve_default
|
|
20
|
+
# provider = Ace::Git::Providers.for(server)
|
|
21
|
+
# pr = provider.pull_request(number: 25)
|
|
22
|
+
module Providers
|
|
23
|
+
@registry = {}
|
|
24
|
+
@mutex = Mutex.new
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# Register a provider implementation for a provider type.
|
|
28
|
+
#
|
|
29
|
+
# @param type [Symbol, String] provider type (e.g. :acme)
|
|
30
|
+
# @param provider_class [Class] class implementing Providers::Base;
|
|
31
|
+
# must respond to .new
|
|
32
|
+
# @raise [ArgumentError] when type or provider_class is invalid
|
|
33
|
+
def register(type, provider_class)
|
|
34
|
+
key = normalize_type(type)
|
|
35
|
+
unless provider_class.respond_to?(:new)
|
|
36
|
+
raise ArgumentError, "Provider for #{key.inspect} must be a class (got #{provider_class.inspect})"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
@mutex.synchronize { @registry[key] = provider_class }
|
|
40
|
+
provider_class
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Build the provider implementation for a resolved server.
|
|
44
|
+
#
|
|
45
|
+
# Provider packages register themselves when required. On a miss, the
|
|
46
|
+
# provider libraries listed in the `git.providers` configuration are
|
|
47
|
+
# required once (mono-repo default: both shipped provider gems) and
|
|
48
|
+
# the registry is re-checked; a still-missing provider is a classified
|
|
49
|
+
# failure, never a fallback.
|
|
50
|
+
#
|
|
51
|
+
# @param server [ResolvedServer] exactly-resolved server identity
|
|
52
|
+
# @param timeout [Integer, nil] provider operation timeout in seconds
|
|
53
|
+
# @param runner [Proc, nil] optional command runner injection for tests;
|
|
54
|
+
# see Providers::Base
|
|
55
|
+
# @return [Providers::Base] provider instance bound to the server
|
|
56
|
+
# @raise [UnknownProviderError] when no provider is registered for the
|
|
57
|
+
# server's provider type
|
|
58
|
+
def for(server, timeout: nil, runner: nil)
|
|
59
|
+
key = normalize_type(server.provider)
|
|
60
|
+
provider_class = @mutex.synchronize { @registry[key] }
|
|
61
|
+
unless provider_class
|
|
62
|
+
require_configured_providers
|
|
63
|
+
provider_class = @mutex.synchronize { @registry[key] }
|
|
64
|
+
end
|
|
65
|
+
unless provider_class
|
|
66
|
+
raise UnknownProviderError,
|
|
67
|
+
"No provider registered for type #{key.inspect} (server '#{server.name}'); " \
|
|
68
|
+
"install the provider package that owns #{key.inspect} and list it in git.providers"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
provider_class.new(server: server, timeout: timeout, runner: runner)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @param type [Symbol, String] provider type
|
|
75
|
+
# @return [Boolean] true when a provider is registered for the type
|
|
76
|
+
def registered?(type)
|
|
77
|
+
key = normalize_type(type)
|
|
78
|
+
@mutex.synchronize { @registry.key?(key) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @return [Array<Symbol>] registered provider types, sorted
|
|
82
|
+
def types
|
|
83
|
+
@mutex.synchronize { @registry.keys.sort }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Clear all registrations (test seam).
|
|
87
|
+
def reset!
|
|
88
|
+
@mutex.synchronize do
|
|
89
|
+
@registry.clear
|
|
90
|
+
@required_configured = {}
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def normalize_type(type)
|
|
97
|
+
normalized = type.to_s.strip.downcase
|
|
98
|
+
raise ArgumentError, "Provider type must be a non-empty symbol or string" if normalized.empty?
|
|
99
|
+
|
|
100
|
+
normalized.to_sym
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Require the provider libraries configured under `git.providers`
|
|
104
|
+
# (e.g. "github" -> ace/git/github). Load failures are ignored here;
|
|
105
|
+
# a missing provider surfaces as UnknownProviderError at resolution.
|
|
106
|
+
def require_configured_providers
|
|
107
|
+
@required_configured ||= {}
|
|
108
|
+
Array(Ace::Git.config["providers"]).each do |name|
|
|
109
|
+
library = name.to_s.strip.downcase
|
|
110
|
+
next if library.empty? || @required_configured[library]
|
|
111
|
+
|
|
112
|
+
require "ace/git/#{library}"
|
|
113
|
+
@required_configured[library] = true
|
|
114
|
+
rescue LoadError
|
|
115
|
+
@required_configured[library] = true
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Git
|
|
5
|
+
# An exactly-resolved forge server identity.
|
|
6
|
+
#
|
|
7
|
+
# Produced by ServerRegistry (by explicit name, by default resolution, or by
|
|
8
|
+
# remote URL matching). Carries the configured server name, the provider
|
|
9
|
+
# type that owns its behavior, and the configured base URL.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# ResolvedServer.new(name: "forge-lab", provider: :acme, url: "https://forge.example.com")
|
|
13
|
+
ResolvedServer = Data.define(:name, :provider, :url) do
|
|
14
|
+
# @return [Hash] Plain hash representation
|
|
15
|
+
def to_h
|
|
16
|
+
{name: name, provider: provider, url: url}
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "atoms/command_executor"
|
|
4
|
+
require_relative "atoms/server_url"
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
require_relative "resolved_server"
|
|
7
|
+
|
|
8
|
+
module Ace
|
|
9
|
+
module Git
|
|
10
|
+
# Resolves forge server identity from configuration, deterministically.
|
|
11
|
+
#
|
|
12
|
+
# Configuration lives under the `git.servers` key (see
|
|
13
|
+
# `.ace-defaults/git/config.yml`). Each entry is a uniquely named server:
|
|
14
|
+
#
|
|
15
|
+
# ```yaml
|
|
16
|
+
# servers:
|
|
17
|
+
# - name: forge-lab
|
|
18
|
+
# provider: acme
|
|
19
|
+
# url: https://forge.example.com/owner/repo
|
|
20
|
+
# default: true
|
|
21
|
+
# - name: forge-mirror
|
|
22
|
+
# provider: other
|
|
23
|
+
# url: https://other.example.com/owner/repo
|
|
24
|
+
# ```
|
|
25
|
+
#
|
|
26
|
+
# At most one server may be marked `default: true`. Resolution never falls
|
|
27
|
+
# back silently: every failure raises a classified error from the failure
|
|
28
|
+
# taxonomy.
|
|
29
|
+
class ServerRegistry
|
|
30
|
+
# Internal configured entry: resolved identity plus its default flag.
|
|
31
|
+
Entry = Data.define(:server, :default)
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
# All configured servers, in configuration order.
|
|
35
|
+
#
|
|
36
|
+
# @return [Array<ResolvedServer>]
|
|
37
|
+
# @raise [DuplicateServerNameError] when two servers share a name
|
|
38
|
+
# @raise [ConfigError] when an entry is malformed
|
|
39
|
+
def servers
|
|
40
|
+
entries.map(&:server)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Resolve a server by its explicit, configured name.
|
|
44
|
+
#
|
|
45
|
+
# @param name [String, Symbol] configured server name
|
|
46
|
+
# @return [ResolvedServer]
|
|
47
|
+
# @raise [UnknownServerNameError] when no server has that name
|
|
48
|
+
def resolve(name)
|
|
49
|
+
wanted = name.to_s
|
|
50
|
+
entries.map(&:server).find { |server| server.name == wanted } ||
|
|
51
|
+
raise(UnknownServerNameError, "No server named '#{wanted}' is configured " \
|
|
52
|
+
"(configured: #{servers.map(&:name).join(", ")}); add it to git.servers")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Resolve the explicitly configured default server.
|
|
56
|
+
#
|
|
57
|
+
# @return [ResolvedServer]
|
|
58
|
+
# @raise [NoDefaultServerConfiguredError] when zero servers are default
|
|
59
|
+
# @raise [MultipleDefaultServersError] when more than one is default
|
|
60
|
+
def resolve_default
|
|
61
|
+
defaults = entries.select(&:default)
|
|
62
|
+
if defaults.empty?
|
|
63
|
+
raise NoDefaultServerConfiguredError,
|
|
64
|
+
"No default server configured; mark exactly one entry in git.servers with default: true"
|
|
65
|
+
end
|
|
66
|
+
if defaults.size > 1
|
|
67
|
+
raise MultipleDefaultServersError,
|
|
68
|
+
"Multiple servers marked as default: #{defaults.map { |e| e.server.name }.join(", ")}; " \
|
|
69
|
+
"keep exactly one"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
defaults.first.server
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Resolve server identity from a git remote URL, matching configured
|
|
76
|
+
# servers deterministically (no hostname assumptions).
|
|
77
|
+
#
|
|
78
|
+
# @param remote_name [String, nil] git remote name (default: configured
|
|
79
|
+
# `remote`, then "origin")
|
|
80
|
+
# @return [ResolvedServer] the single matching configured server
|
|
81
|
+
# @raise [AmbiguousRemoteError] when the remote matches no configured
|
|
82
|
+
# server, or more than one
|
|
83
|
+
def resolve_remote(remote_name = nil)
|
|
84
|
+
remote_name ||= Ace::Git.config["remote"] || "origin"
|
|
85
|
+
remote_url = read_remote_url(remote_name)
|
|
86
|
+
matches = entries.map(&:server).select { |server| Atoms::ServerUrl.match?(server.url, remote_url) }
|
|
87
|
+
|
|
88
|
+
if matches.empty?
|
|
89
|
+
raise AmbiguousRemoteError,
|
|
90
|
+
"Remote '#{remote_name}' (#{remote_url || "missing"}) matches no configured server " \
|
|
91
|
+
"(configured: #{servers.map(&:name).join(", ")})"
|
|
92
|
+
end
|
|
93
|
+
if matches.size > 1
|
|
94
|
+
raise AmbiguousRemoteError,
|
|
95
|
+
"Remote '#{remote_name}' (#{remote_url}) matches multiple configured servers: " \
|
|
96
|
+
"#{matches.map(&:name).join(", ")}; make server URLs distinct"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
matches.first
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Resolve exactly one server for an operation from CLI-style selection.
|
|
103
|
+
#
|
|
104
|
+
# @param server_name [String, Symbol, nil] explicit configured server name
|
|
105
|
+
# @param use_default [Boolean] resolve the configured default server
|
|
106
|
+
# @param remote_name [String, nil] remote used when nothing is selected
|
|
107
|
+
# @return [ResolvedServer]
|
|
108
|
+
# @raise [ConfigError] when explicit selection and default conflict
|
|
109
|
+
# @raise [UnknownServerNameError, NoDefaultServerConfiguredError,
|
|
110
|
+
# MultipleDefaultServersError, AmbiguousRemoteError] per resolution
|
|
111
|
+
def resolve_for(server_name: nil, use_default: false, remote_name: nil)
|
|
112
|
+
if server_name && use_default
|
|
113
|
+
raise ConfigError,
|
|
114
|
+
"Server selection is ambiguous: an explicit server ('#{server_name}') and " \
|
|
115
|
+
"the default-server request are mutually exclusive"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
if server_name
|
|
119
|
+
resolve(server_name)
|
|
120
|
+
elsif use_default
|
|
121
|
+
resolve_default
|
|
122
|
+
else
|
|
123
|
+
resolve_remote(remote_name)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Configured servers whose URL matches the repository URL exactly.
|
|
128
|
+
#
|
|
129
|
+
# @param repository_url [String] repository URL in any supported shape
|
|
130
|
+
# @return [Array<ResolvedServer>] matching servers (may be empty)
|
|
131
|
+
def matching_servers(repository_url)
|
|
132
|
+
entries.map(&:server).select { |server| Atoms::ServerUrl.match?(server.url, repository_url) }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Configured servers whose repository path equals `owner/repo`
|
|
136
|
+
# (any host). Never assumes a forge hostname.
|
|
137
|
+
#
|
|
138
|
+
# @param owner_repo [String] "owner/repo"
|
|
139
|
+
# @return [Array<ResolvedServer>] matching servers (may be empty)
|
|
140
|
+
def servers_for_owner_repo(owner_repo)
|
|
141
|
+
wanted = owner_repo.to_s.downcase
|
|
142
|
+
entries.map(&:server).select do |server|
|
|
143
|
+
path = Atoms::ServerUrl.normalize(server.url).to_s.split("/", 2)[1]
|
|
144
|
+
path == wanted
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
# Validated configured entries, in configuration order.
|
|
151
|
+
def entries
|
|
152
|
+
raw = Ace::Git.config["servers"]
|
|
153
|
+
return [] if raw.nil?
|
|
154
|
+
|
|
155
|
+
raise ConfigError, "servers config must be a list" unless raw.is_a?(Array)
|
|
156
|
+
|
|
157
|
+
list = raw.map { |entry| build_entry(entry) }
|
|
158
|
+
assert_unique_names!(list)
|
|
159
|
+
list
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Read a remote URL with local git only (no network, no forge CLI).
|
|
163
|
+
def read_remote_url(remote_name)
|
|
164
|
+
result = Atoms::CommandExecutor.execute("git", "remote", "get-url", remote_name.to_s)
|
|
165
|
+
result[:success] ? result[:output].to_s.strip : nil
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Build one internal entry from a config entry, validating structure.
|
|
169
|
+
def build_entry(entry)
|
|
170
|
+
unless entry.is_a?(Hash)
|
|
171
|
+
raise ConfigError, "Each git.servers entry must be a mapping with name, provider, url"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
normalized = entry.transform_keys(&:to_s)
|
|
175
|
+
%w[name provider url].each do |key|
|
|
176
|
+
next unless normalized[key].nil? || normalized[key].to_s.empty?
|
|
177
|
+
|
|
178
|
+
raise ConfigError, "git.servers entry is missing '#{key}': #{entry.inspect}"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
Entry.new(
|
|
182
|
+
server: ResolvedServer.new(
|
|
183
|
+
name: normalized["name"].to_s,
|
|
184
|
+
provider: normalized["provider"].to_s.to_sym,
|
|
185
|
+
url: normalized["url"].to_s
|
|
186
|
+
),
|
|
187
|
+
default: normalized["default"] == true || normalized["default"].to_s == "true"
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def assert_unique_names!(list)
|
|
192
|
+
names = list.map { |entry| entry.server.name }
|
|
193
|
+
duplicates = names.tally.select { |_name, count| count > 1 }.keys
|
|
194
|
+
return if duplicates.empty?
|
|
195
|
+
|
|
196
|
+
raise DuplicateServerNameError,
|
|
197
|
+
"Duplicate server names in git.servers: #{duplicates.join(", ")}; server names must be unique"
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
data/lib/ace/git/version.rb
CHANGED
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 providers].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,9 @@ 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_reference"
|
|
199
|
+
require_relative "git/atoms/pr_identifier"
|
|
201
200
|
require_relative "git/atoms/pattern_filter"
|
|
202
201
|
require_relative "git/atoms/diff_parser"
|
|
203
202
|
require_relative "git/atoms/diff_numstat_parser"
|
|
@@ -205,7 +204,6 @@ require_relative "git/atoms/file_grouper"
|
|
|
205
204
|
require_relative "git/atoms/grouped_stats_formatter"
|
|
206
205
|
require_relative "git/atoms/date_resolver"
|
|
207
206
|
require_relative "git/atoms/task_pattern_extractor"
|
|
208
|
-
require_relative "git/atoms/pr_identifier_parser"
|
|
209
207
|
require_relative "git/atoms/repository_state_detector"
|
|
210
208
|
require_relative "git/atoms/repository_checker"
|
|
211
209
|
require_relative "git/atoms/git_scope_filter"
|
|
@@ -218,14 +216,17 @@ require_relative "git/molecules/diff_generator"
|
|
|
218
216
|
require_relative "git/molecules/config_loader"
|
|
219
217
|
require_relative "git/molecules/diff_filter"
|
|
220
218
|
require_relative "git/molecules/branch_reader"
|
|
221
|
-
require_relative "git/molecules/pr_metadata_fetcher"
|
|
222
219
|
require_relative "git/molecules/recent_commits_fetcher"
|
|
223
220
|
require_relative "git/molecules/git_status_fetcher"
|
|
224
|
-
require_relative "git/molecules/gh_cli_executor"
|
|
225
|
-
require_relative "git/molecules/github_issue_sync"
|
|
226
221
|
|
|
227
222
|
require_relative "git/organisms/diff_orchestrator"
|
|
228
223
|
require_relative "git/organisms/repo_status_loader"
|
|
224
|
+
require_relative "git/organisms/pull_request_lifecycle"
|
|
225
|
+
require_relative "git/resolved_server"
|
|
226
|
+
require_relative "git/server_registry"
|
|
227
|
+
require_relative "git/providers/base"
|
|
228
|
+
require_relative "git/providers/evidence"
|
|
229
|
+
require_relative "git/providers"
|
|
229
230
|
|
|
230
231
|
require_relative "git/models/diff_result"
|
|
231
232
|
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.25.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-29 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,11 @@ 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
|
+
- lib/ace/git/atoms/pr_reference.rb
|
|
153
154
|
- lib/ace/git/atoms/repository_checker.rb
|
|
154
155
|
- lib/ace/git/atoms/repository_state_detector.rb
|
|
156
|
+
- lib/ace/git/atoms/server_url.rb
|
|
155
157
|
- lib/ace/git/atoms/stale_lock_cleaner.rb
|
|
156
158
|
- lib/ace/git/atoms/status_formatter.rb
|
|
157
159
|
- lib/ace/git/atoms/task_pattern_extractor.rb
|
|
@@ -161,6 +163,7 @@ files:
|
|
|
161
163
|
- lib/ace/git/cli/commands/diff.rb
|
|
162
164
|
- lib/ace/git/cli/commands/pr.rb
|
|
163
165
|
- lib/ace/git/cli/commands/status.rb
|
|
166
|
+
- lib/ace/git/errors.rb
|
|
164
167
|
- lib/ace/git/models/diff_config.rb
|
|
165
168
|
- lib/ace/git/models/diff_result.rb
|
|
166
169
|
- lib/ace/git/models/repo_status.rb
|
|
@@ -168,22 +171,25 @@ files:
|
|
|
168
171
|
- lib/ace/git/molecules/config_loader.rb
|
|
169
172
|
- lib/ace/git/molecules/diff_filter.rb
|
|
170
173
|
- lib/ace/git/molecules/diff_generator.rb
|
|
171
|
-
- lib/ace/git/molecules/gh_cli_executor.rb
|
|
172
174
|
- 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
175
|
- lib/ace/git/molecules/recent_commits_fetcher.rb
|
|
176
176
|
- lib/ace/git/organisms/diff_orchestrator.rb
|
|
177
|
+
- lib/ace/git/organisms/pull_request_lifecycle.rb
|
|
177
178
|
- lib/ace/git/organisms/repo_status_loader.rb
|
|
179
|
+
- lib/ace/git/providers.rb
|
|
180
|
+
- lib/ace/git/providers/base.rb
|
|
181
|
+
- lib/ace/git/providers/evidence.rb
|
|
182
|
+
- lib/ace/git/resolved_server.rb
|
|
183
|
+
- lib/ace/git/server_registry.rb
|
|
178
184
|
- lib/ace/git/version.rb
|
|
179
|
-
homepage: https://
|
|
185
|
+
homepage: https://forgejo.tail6c0887.ts.net/cs3b/ace
|
|
180
186
|
licenses:
|
|
181
187
|
- MIT
|
|
182
188
|
metadata:
|
|
183
189
|
allowed_push_host: https://rubygems.org
|
|
184
|
-
homepage_uri: https://
|
|
185
|
-
source_code_uri: https://
|
|
186
|
-
changelog_uri: https://
|
|
190
|
+
homepage_uri: https://forgejo.tail6c0887.ts.net/cs3b/ace
|
|
191
|
+
source_code_uri: https://forgejo.tail6c0887.ts.net/cs3b/ace/src/branch/main/ace-git
|
|
192
|
+
changelog_uri: https://forgejo.tail6c0887.ts.net/cs3b/ace/src/branch/main/ace-git/CHANGELOG.md
|
|
187
193
|
rdoc_options: []
|
|
188
194
|
require_paths:
|
|
189
195
|
- lib
|
|
@@ -200,5 +206,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
200
206
|
requirements: []
|
|
201
207
|
rubygems_version: 3.6.9
|
|
202
208
|
specification_version: 4
|
|
203
|
-
summary: Git workflows and context commands for developers and AI agents
|
|
209
|
+
summary: Forge-neutral Git workflows and context commands for developers and AI agents
|
|
204
210
|
test_files: []
|