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.
- checksums.yaml +4 -4
- data/.ace-defaults/git/config.yml +16 -1
- data/CHANGELOG.md +22 -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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 746038569079877012656b703209b794e8a055718955f23f8d4b613ec8fb6628
|
|
4
|
+
data.tar.gz: 841b2ac7dc6eed9348e36a46f6abef8e18c8f726e9f26a705d96a3548209ee27
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b10072d2af5d314c0c00fefc7883ab6d33de5bad5388b549a50db5cc1134b92196d1462c4c598c9aeaa3a79114eb44be2d29693aa3a95db74646bd4786dd6131
|
|
7
|
+
data.tar.gz: 510071cb8c141cde868bcc0fb79d55e62edbf749c5b82dfbb987488cfa20fcd236fc7c2c5c963db3891655bcbef8bcf9223634298a5abeb0236c4518f9ecf6be
|
|
@@ -10,7 +10,22 @@ git:
|
|
|
10
10
|
|
|
11
11
|
# Timeout settings (in seconds)
|
|
12
12
|
timeout: 30 # Local git operations (diff, status, log)
|
|
13
|
-
network_timeout: 60 # Network operations (
|
|
13
|
+
network_timeout: 60 # Network operations (provider CLIs, remote operations)
|
|
14
|
+
|
|
15
|
+
# Forge server registry (provider-neutral)
|
|
16
|
+
# Configure each forge server once with a unique name; mark at most one as
|
|
17
|
+
# default. Resolve by explicit name, by default, or by matching a git remote
|
|
18
|
+
# URL. The `provider` value selects the registered provider package that owns
|
|
19
|
+
# that server's behavior.
|
|
20
|
+
servers: []
|
|
21
|
+
# servers:
|
|
22
|
+
# - name: forgejo-lab
|
|
23
|
+
# provider: forgejo
|
|
24
|
+
# url: https://forgejo.example.com/owner/repo
|
|
25
|
+
# default: true
|
|
26
|
+
# - name: github-public
|
|
27
|
+
# provider: github
|
|
28
|
+
# url: https://github.example.com/owner/repo
|
|
14
29
|
|
|
15
30
|
# Status command settings
|
|
16
31
|
status:
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.24.0] - 2026-09-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Forge-neutral provider foundation: `Ace::Git::ServerRegistry` (explicit named servers, exactly-one-default resolution, deterministic remote-URL matching with no hostname assumptions), `Ace::Git::ResolvedServer`, the `Ace::Git::Providers` registry with the `Providers::Base` contract, normalized evidence types (`ProviderPullRequest`, `ProviderIssue`, `ProviderCheck`, `ProviderRepository`), and the neutral `Atoms::PrIdentifier` / `Atoms::ServerUrl` atoms.
|
|
15
|
+
- Classified failure taxonomy in `git/errors.rb`: `NoDefaultServerConfiguredError`, `MultipleDefaultServersError`, `DuplicateServerNameError`, `UnknownProviderError`, `UnknownServerNameError`, `AmbiguousRemoteError`, `ProviderCliMissingError`, `ProviderAuthenticationError`, `ProviderUnreachableError`, `ProviderMalformedOutputError`, `ProviderObjectNotFoundError`.
|
|
16
|
+
- `git.servers` configuration section supporting multiple uniquely named servers with at most one explicit default.
|
|
17
|
+
- Boundary proof: local Git operations succeed with provider CLIs scrubbed from PATH, and a test pins the core lib to zero forge CLI references.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Status PR enrichment routes through the provider contract (default server + registered provider); with no server or provider configured, status stays purely local and PR sections are skipped.
|
|
22
|
+
- Forge server URLs in gemspec metadata point at the canonical delivery forge instead of hardcoded GitHub URLs.
|
|
23
|
+
|
|
24
|
+
### Removed
|
|
25
|
+
|
|
26
|
+
- All forge behavior from the core (pre-1.0 replacement, no shims or aliases): `GhCliExecutor`, `PrMetadataFetcher`, `GithubIssueSync`, the `ace-git pr` CLI command, the GitHub-specific `PrIdentifierParser`, and the `GhNotInstalledError` / `GhAuthenticationError` / `PrNotFoundError` error classes. GitHub behavior moved to the new `ace-git-github` gem; Forgejo behavior is provided by the new `ace-git-forgejo` gem.
|
|
27
|
+
|
|
28
|
+
## [0.23.1] - 2026-09-02
|
|
29
|
+
|
|
30
|
+
### Technical
|
|
31
|
+
- Included in the coordinated all-package patch release preparation for ACE monorepo review.
|
|
10
32
|
## [0.23.0] - 2026-08-13
|
|
11
33
|
### Added
|
|
12
34
|
- Expose PR head and merge commit metadata
|
data/docs/getting-started.md
CHANGED
|
@@ -17,7 +17,8 @@ guided workflow.
|
|
|
17
17
|
- Ruby 3.2+
|
|
18
18
|
- Git 2.23+
|
|
19
19
|
- `ace-git` installed
|
|
20
|
-
- Optional:
|
|
20
|
+
- Optional: a provider package (`ace-git-github` or `ace-git-forgejo`) plus a configured
|
|
21
|
+
default forge server for PR-aware status enrichment
|
|
21
22
|
|
|
22
23
|
## Installation
|
|
23
24
|
|
|
@@ -76,7 +77,6 @@ Use this for preferences, not for redefining the workflow logic.
|
|
|
76
77
|
| Show repo context | `ace-git status` |
|
|
77
78
|
| Show a quick diff summary | `ace-git diff --format summary` |
|
|
78
79
|
| Show branch tracking info | `ace-git branch` |
|
|
79
|
-
| Show PR metadata | `ace-git pr` |
|
|
80
80
|
| Show package version | `ace-git version` |
|
|
81
81
|
|
|
82
82
|
## Run Package Tests
|
|
@@ -90,7 +90,7 @@ Use the restarted package test model:
|
|
|
90
90
|
|
|
91
91
|
## Next Steps
|
|
92
92
|
|
|
93
|
-
- Load `wfi://github/pr/create` to open a structured PR workflow
|
|
94
|
-
- Load `wfi://github/pr/update` to refresh an existing PR description
|
|
95
93
|
- Load `wfi://git/reorganize-commits` to clean up branch history before review
|
|
94
|
+
- Install a provider package (`ace-git-github` / `ace-git-forgejo`) and configure
|
|
95
|
+
`git.servers` for PR-aware status enrichment
|
|
96
96
|
- Run `ace-git --help` to browse command-level examples
|
data/docs/usage.md
CHANGED
|
@@ -23,12 +23,11 @@ gem install ace-git
|
|
|
23
23
|
|
|
24
24
|
## Command Overview
|
|
25
25
|
|
|
26
|
-
`ace-git` ships
|
|
26
|
+
`ace-git` ships four commands:
|
|
27
27
|
|
|
28
28
|
- `diff` for filtered or formatted git diffs
|
|
29
29
|
- `status` for repository context and PR activity
|
|
30
30
|
- `branch` for current branch and tracking state
|
|
31
|
-
- `pr` for PR metadata lookup
|
|
32
31
|
- `version` for the installed package version
|
|
33
32
|
|
|
34
33
|
`ace-git` with no arguments shows help. Git range shorthand such as `HEAD~5..HEAD` routes to `diff`.
|
|
@@ -155,43 +154,6 @@ ace-git branch --format json
|
|
|
155
154
|
**Options:**
|
|
156
155
|
- `--format, -f` - Output format: `text` (default), `json`
|
|
157
156
|
|
|
158
|
-
### `ace-git pr [NUMBER]`
|
|
159
|
-
|
|
160
|
-
Fetch and display PR metadata using GitHub CLI.
|
|
161
|
-
|
|
162
|
-
```bash
|
|
163
|
-
# Auto-detect PR from current branch
|
|
164
|
-
ace-git pr
|
|
165
|
-
|
|
166
|
-
# Specific PR number
|
|
167
|
-
ace-git pr 123
|
|
168
|
-
|
|
169
|
-
# Cross-repository PR
|
|
170
|
-
ace-git pr owner/repo#456
|
|
171
|
-
|
|
172
|
-
# JSON output
|
|
173
|
-
ace-git pr --format json
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
**Options:**
|
|
177
|
-
- `--format, -f` - Output format: `markdown` (default), `json`
|
|
178
|
-
- `--with-diff` - Include PR diff in output
|
|
179
|
-
|
|
180
|
-
**PR Number Formats:**
|
|
181
|
-
- Simple number: `123`
|
|
182
|
-
- Qualified: `owner/repo#456`
|
|
183
|
-
- GitHub URL: `https://github.com/owner/repo/pull/789`
|
|
184
|
-
|
|
185
|
-
**Requirements:** GitHub CLI (`gh`) must be installed and authenticated.
|
|
186
|
-
|
|
187
|
-
```bash
|
|
188
|
-
# Install gh
|
|
189
|
-
brew install gh
|
|
190
|
-
|
|
191
|
-
# Authenticate
|
|
192
|
-
gh auth login
|
|
193
|
-
```
|
|
194
|
-
|
|
195
157
|
### `ace-git version`
|
|
196
158
|
|
|
197
159
|
Print the installed `ace-git` version.
|
|
@@ -267,11 +229,18 @@ Note: `test/**/*` and `spec/**/*` are NOT excluded by default - test changes are
|
|
|
267
229
|
- **ace-bundle** - Load ACE workflow instructions directly
|
|
268
230
|
- **ace-nav** - Discover workflow and template protocol paths
|
|
269
231
|
|
|
270
|
-
##
|
|
232
|
+
## Provider Packages
|
|
271
233
|
|
|
272
|
-
`ace-git`
|
|
234
|
+
`ace-git` is forge-neutral. It defines the server registry (`Ace::Git::ServerRegistry`),
|
|
235
|
+
the provider contract (`Ace::Git::Providers::Base`), and normalized evidence types;
|
|
236
|
+
provider packages own all forge CLI behavior:
|
|
273
237
|
|
|
274
|
-
-
|
|
275
|
-
|
|
238
|
+
- **ace-git-github** - GitHub behavior via the `gh` CLI (PR lookups, diffs, issue
|
|
239
|
+
synchronization). Registers under the `github` provider type.
|
|
240
|
+
- **ace-git-forgejo** - Forgejo behavior via the `fj` CLI. Registers under the
|
|
241
|
+
`forgejo` provider type.
|
|
276
242
|
|
|
277
|
-
|
|
243
|
+
Configure servers in `.ace/git.yml` (or project config) under `git.servers`, then
|
|
244
|
+
resolve them by name, by default, or from a git remote URL. Every failure path is
|
|
245
|
+
classified (missing CLI, unauthenticated, unreachable, malformed output, object
|
|
246
|
+
not found) with no silent fallbacks.
|
|
@@ -106,7 +106,7 @@ module Ace
|
|
|
106
106
|
# Using Timeout to prevent hanging on network issues or stuck git operations
|
|
107
107
|
Timeout.timeout(timeout) do
|
|
108
108
|
# Using Open3.capture3 to avoid shell injection
|
|
109
|
-
# Arguments are passed directly as array elements,
|
|
109
|
+
# Arguments are passed directly as array elements, never via a shell
|
|
110
110
|
# If env is provided, prepend it to the command (Open3 convention)
|
|
111
111
|
args = env ? [env, *command_parts] : command_parts
|
|
112
112
|
stdout, stderr, status = Open3.capture3(*args)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Git
|
|
5
|
+
module Atoms
|
|
6
|
+
# Parse provider-neutral pull request identifiers into structured form.
|
|
7
|
+
#
|
|
8
|
+
# Supports the forge-neutral forms:
|
|
9
|
+
# - Simple number: "123"
|
|
10
|
+
# - Qualified reference: "owner/repo#456"
|
|
11
|
+
#
|
|
12
|
+
# Provider-specific identifier formats (for example full web URLs) belong
|
|
13
|
+
# to the provider packages, not to the core.
|
|
14
|
+
module PrIdentifier
|
|
15
|
+
# Parsed identifier result
|
|
16
|
+
ParseResult = Data.define(:number, :repo)
|
|
17
|
+
|
|
18
|
+
# Maximum identifier length to bound regex work
|
|
19
|
+
MAX_IDENTIFIER_LENGTH = 256
|
|
20
|
+
|
|
21
|
+
# Parse a pull request identifier string.
|
|
22
|
+
#
|
|
23
|
+
# Returns nil for nil/empty input (no PR specified), raises ArgumentError
|
|
24
|
+
# for invalid formats. This lets callers distinguish "no PR" (nil input)
|
|
25
|
+
# from "invalid PR" (bad format).
|
|
26
|
+
#
|
|
27
|
+
# @param input [String, Integer] pull request identifier
|
|
28
|
+
# @return [ParseResult, nil] parsed identifier, or nil for nil/empty input
|
|
29
|
+
# @raise [ArgumentError] if identifier format is invalid
|
|
30
|
+
#
|
|
31
|
+
# @example Simple number
|
|
32
|
+
# parse(123) # => ParseResult(number: "123", repo: nil)
|
|
33
|
+
#
|
|
34
|
+
# @example Qualified reference
|
|
35
|
+
# parse("owner/repo#456") # => ParseResult(number: "456", repo: "owner/repo")
|
|
36
|
+
#
|
|
37
|
+
# @example Nil/empty input
|
|
38
|
+
# parse(nil) # => nil
|
|
39
|
+
def self.parse(input)
|
|
40
|
+
return nil if input.nil?
|
|
41
|
+
|
|
42
|
+
input_str = input.to_s.strip
|
|
43
|
+
return nil if input_str.empty?
|
|
44
|
+
|
|
45
|
+
if input_str.length > MAX_IDENTIFIER_LENGTH
|
|
46
|
+
raise ArgumentError, "PR identifier too long (max #{MAX_IDENTIFIER_LENGTH} characters)"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
case input_str
|
|
50
|
+
when /\A(\d+)\z/
|
|
51
|
+
number = ::Regexp.last_match(1)
|
|
52
|
+
raise ArgumentError, "Invalid PR identifier format: #{input_str}" if number.to_i.zero?
|
|
53
|
+
|
|
54
|
+
# Canonical form (no leading zeros) for consistent downstream use
|
|
55
|
+
ParseResult.new(number: number.to_i.to_s, repo: nil)
|
|
56
|
+
when /\A(?<repo>[a-zA-Z0-9_\-.]+\/[a-zA-Z0-9_\-.]+)#(?<number>\d+)\z/
|
|
57
|
+
match = ::Regexp.last_match
|
|
58
|
+
ParseResult.new(number: match[:number], repo: match[:repo])
|
|
59
|
+
else
|
|
60
|
+
raise ArgumentError, "Invalid PR identifier format: #{input_str}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Git
|
|
5
|
+
module Atoms
|
|
6
|
+
# Normalize forge server URLs and remote URLs to a canonical comparable
|
|
7
|
+
# form. Purely textual: no network access, no hostname allow-lists, and
|
|
8
|
+
# no assumptions about any particular forge host.
|
|
9
|
+
#
|
|
10
|
+
# Supported input shapes:
|
|
11
|
+
# - "https://forge.example.com/owner/repo.git"
|
|
12
|
+
# - "http://forge.internal:3000/owner/repo"
|
|
13
|
+
# - "git@forge.example.com:owner/repo.git" (scp-style)
|
|
14
|
+
# - "ssh://git@forge.example.com:2222/owner/repo.git"
|
|
15
|
+
# - "forge.example.com/owner/repo"
|
|
16
|
+
#
|
|
17
|
+
# Canonical form: lowercase "host[:port]/path" with any credentials,
|
|
18
|
+
# scheme, trailing slash, and ".git" suffix removed. A colon segment is
|
|
19
|
+
# treated as a port (not an scp separator) only when it is all digits and
|
|
20
|
+
# followed by a slash; everything else is scp-style "host:path".
|
|
21
|
+
module ServerUrl
|
|
22
|
+
# Normalize a server or remote URL to the canonical comparable form.
|
|
23
|
+
#
|
|
24
|
+
# @param url [String] server or remote URL
|
|
25
|
+
# @return [String] canonical "host[:port]/path" form
|
|
26
|
+
def self.normalize(url)
|
|
27
|
+
text = url.to_s.strip
|
|
28
|
+
text = text.sub(%r{\A[a-z][a-z0-9+.\-]*://}i, "")
|
|
29
|
+
text = text.sub(%r{\A[^/@]+@}, "")
|
|
30
|
+
|
|
31
|
+
if (port_match = text.match(%r{\A([^/:]+):(\d+)(/.+)\z}))
|
|
32
|
+
text = "#{port_match[1]}:#{port_match[2]}#{port_match[3]}"
|
|
33
|
+
elsif (scp_match = text.match(%r{\A([^/:]+):(.+)\z}))
|
|
34
|
+
text = "#{scp_match[1]}/#{scp_match[2]}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
text = text.sub(%r{/?\.git/?\z}i, "")
|
|
38
|
+
text.chomp("/").downcase
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Compare a configured server URL against a remote URL for identity.
|
|
42
|
+
#
|
|
43
|
+
# @param server_url [String] configured server base URL
|
|
44
|
+
# @param remote_url [String] git remote URL
|
|
45
|
+
# @return [Boolean] true when both normalize to the same form
|
|
46
|
+
def self.match?(server_url, remote_url)
|
|
47
|
+
normalized_server = normalize(server_url)
|
|
48
|
+
normalized_remote = normalize(remote_url)
|
|
49
|
+
return false if normalized_server.empty? || normalized_remote.empty?
|
|
50
|
+
|
|
51
|
+
normalized_server == normalized_remote
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -75,40 +75,37 @@ module Ace
|
|
|
75
75
|
lines
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
# Format current PR section (
|
|
79
|
-
# @param
|
|
78
|
+
# Format current PR section (spotlit for current branch)
|
|
79
|
+
# @param pr [ProviderPullRequest, nil] normalized pull request evidence
|
|
80
80
|
# @return [Array<String>] Lines of markdown
|
|
81
|
-
def format_current_pr_section(
|
|
81
|
+
def format_current_pr_section(pr)
|
|
82
82
|
lines = []
|
|
83
83
|
lines << ""
|
|
84
84
|
lines << "## Current PR"
|
|
85
85
|
lines << ""
|
|
86
86
|
|
|
87
|
-
# Main line: #85 [
|
|
88
|
-
main_line = "##{
|
|
89
|
-
main_line += " [#{
|
|
90
|
-
main_line += " #{
|
|
87
|
+
# Main line: #85 [open] Title
|
|
88
|
+
main_line = "##{pr.number}"
|
|
89
|
+
main_line += " [#{pr.state}]" if pr.state
|
|
90
|
+
main_line += " #{pr.title}" if pr.title
|
|
91
91
|
lines << main_line
|
|
92
92
|
|
|
93
93
|
# Details line: Target: main | Author: @username | Draft/Not draft
|
|
94
94
|
details = []
|
|
95
|
-
details << "Target: #{
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
details << "Author: @#{author}"
|
|
99
|
-
end
|
|
100
|
-
details << (pr_metadata["isDraft"] ? "Draft" : "Not draft") if pr_metadata.key?("isDraft")
|
|
95
|
+
details << "Target: #{pr.base_ref}" if pr.base_ref
|
|
96
|
+
details << "Author: @#{pr.author}" if pr.author
|
|
97
|
+
details << (pr.draft ? "Draft" : "Not draft") unless pr.draft.nil?
|
|
101
98
|
lines << " #{details.join(" | ")}" unless details.empty?
|
|
102
99
|
|
|
103
100
|
# URL line
|
|
104
|
-
lines << " #{
|
|
101
|
+
lines << " #{pr.url}" if pr.url
|
|
105
102
|
|
|
106
103
|
lines
|
|
107
104
|
end
|
|
108
105
|
|
|
109
106
|
# Format PR activity section
|
|
110
|
-
# @param pr_activity [Hash, nil]
|
|
111
|
-
#
|
|
107
|
+
# @param pr_activity [Hash, nil] Activity with :merged and :open arrays of
|
|
108
|
+
# normalized ProviderPullRequest evidence
|
|
112
109
|
# @return [Array<String>] Lines of markdown
|
|
113
110
|
def format_pr_activity_section(pr_activity)
|
|
114
111
|
lines = []
|
|
@@ -119,17 +116,15 @@ module Ace
|
|
|
119
116
|
# Handle nil pr_activity for defensive programming
|
|
120
117
|
return lines << "No recent PR activity" if pr_activity.nil?
|
|
121
118
|
|
|
122
|
-
# pr_activity uses symbol keys (from RepoStatusLoader)
|
|
123
|
-
# PR data within uses string keys (from JSON parsing)
|
|
124
119
|
merged = pr_activity[:merged] || []
|
|
125
120
|
open_prs = pr_activity[:open] || []
|
|
126
121
|
|
|
127
122
|
unless merged.empty?
|
|
128
123
|
lines << "Merged:"
|
|
129
124
|
merged.each do |pr|
|
|
130
|
-
title = pr
|
|
131
|
-
merged_ago = format_merged_time_compact(pr
|
|
132
|
-
lines << " ##{pr
|
|
125
|
+
title = pr.title || "(no title)"
|
|
126
|
+
merged_ago = format_merged_time_compact(pr.merged_at)
|
|
127
|
+
lines << " ##{pr.number} #{title}#{merged_ago}"
|
|
133
128
|
end
|
|
134
129
|
end
|
|
135
130
|
|
|
@@ -137,9 +132,9 @@ module Ace
|
|
|
137
132
|
lines << "" unless merged.empty? # Add spacing between Merged and Open
|
|
138
133
|
lines << "Open:"
|
|
139
134
|
open_prs.each do |pr|
|
|
140
|
-
title = pr
|
|
141
|
-
author = format_author(pr
|
|
142
|
-
lines << " ##{pr
|
|
135
|
+
title = pr.title || "(no title)"
|
|
136
|
+
author = format_author(pr.author)
|
|
137
|
+
lines << " ##{pr.number} #{title}#{author}"
|
|
143
138
|
end
|
|
144
139
|
end
|
|
145
140
|
|
|
@@ -163,13 +158,13 @@ module Ace
|
|
|
163
158
|
end
|
|
164
159
|
|
|
165
160
|
# Format author info
|
|
166
|
-
# @param author [
|
|
161
|
+
# @param author [String, nil] Author identifier
|
|
167
162
|
# @return [String] Formatted string like " (@username)"
|
|
168
163
|
def format_author(author)
|
|
169
164
|
return "" if author.nil?
|
|
170
165
|
|
|
171
|
-
login = author.
|
|
172
|
-
return "" if login.
|
|
166
|
+
login = author.to_s
|
|
167
|
+
return "" if login.empty?
|
|
173
168
|
|
|
174
169
|
" (@#{login})"
|
|
175
170
|
end
|
|
@@ -59,24 +59,34 @@ module Ace
|
|
|
59
59
|
# Include diff if requested
|
|
60
60
|
if options[:with_diff] && status.has_pr?
|
|
61
61
|
begin
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if diff_result[:success]
|
|
62
|
+
provider = active_provider
|
|
63
|
+
diff = provider&.pull_request_diff(number: status.pr_metadata.number.to_s)
|
|
64
|
+
if diff
|
|
66
65
|
puts ""
|
|
67
66
|
puts "## PR Diff"
|
|
68
67
|
puts ""
|
|
69
68
|
puts "```diff"
|
|
70
|
-
puts
|
|
69
|
+
puts diff
|
|
71
70
|
puts "```"
|
|
72
71
|
end
|
|
73
72
|
rescue Ace::Git::Error
|
|
74
|
-
#
|
|
73
|
+
# Classified provider failure: skip the diff section
|
|
75
74
|
end
|
|
76
75
|
end
|
|
77
76
|
rescue Ace::Git::Error => e
|
|
78
77
|
raise Ace::Support::Cli::Error.new(e.message)
|
|
79
78
|
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# Resolve the default server's registered provider for optional
|
|
83
|
+
# enrichment; nil when no server or provider is configured.
|
|
84
|
+
def active_provider
|
|
85
|
+
server = Ace::Git::ServerRegistry.resolve_default
|
|
86
|
+
Ace::Git::Providers.for(server)
|
|
87
|
+
rescue Ace::Git::Error
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
80
90
|
end
|
|
81
91
|
end
|
|
82
92
|
end
|
data/lib/ace/git/cli.rb
CHANGED
|
@@ -6,7 +6,6 @@ require_relative "../git"
|
|
|
6
6
|
require_relative "cli/commands/diff"
|
|
7
7
|
require_relative "cli/commands/status"
|
|
8
8
|
require_relative "cli/commands/branch"
|
|
9
|
-
require_relative "cli/commands/pr"
|
|
10
9
|
|
|
11
10
|
module Ace
|
|
12
11
|
module Git
|
|
@@ -51,8 +50,7 @@ module Ace
|
|
|
51
50
|
REGISTERED_COMMANDS = [
|
|
52
51
|
["diff", "Show filtered git diff output"],
|
|
53
52
|
["status", "Show repository status and PR context"],
|
|
54
|
-
["branch", "Show current branch information"]
|
|
55
|
-
["pr", "Show pull request information"]
|
|
53
|
+
["branch", "Show current branch information"]
|
|
56
54
|
].freeze
|
|
57
55
|
|
|
58
56
|
HELP_EXAMPLES = [
|
|
@@ -64,7 +62,6 @@ module Ace
|
|
|
64
62
|
register "diff", Commands::Diff.new
|
|
65
63
|
register "status", Commands::Status.new
|
|
66
64
|
register "branch", Commands::Branch.new
|
|
67
|
-
register "pr", Commands::Pr.new
|
|
68
65
|
|
|
69
66
|
version_cmd = Ace::Support::Cli::VersionCommand.build(
|
|
70
67
|
gem_name: "ace-git",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Git
|
|
5
|
+
# Error handling pattern:
|
|
6
|
+
# - Atoms: Pure functions, raise exceptions for invalid inputs
|
|
7
|
+
# - Molecules: May return error hashes for "expected" errors (e.g., not in git repo)
|
|
8
|
+
# or raise exceptions for unexpected failures
|
|
9
|
+
# - Organisms: Orchestrate molecules, propagate or wrap exceptions
|
|
10
|
+
# - Commands: Catch exceptions and return exit codes (0=success, 1=error)
|
|
11
|
+
#
|
|
12
|
+
# All custom exceptions inherit from Ace::Git::Error for consistent catching.
|
|
13
|
+
class Error < StandardError; end
|
|
14
|
+
class GitError < Error; end
|
|
15
|
+
class ConfigError < Error; end
|
|
16
|
+
class TimeoutError < Error; end
|
|
17
|
+
|
|
18
|
+
# ---- Forge server resolution failures ----
|
|
19
|
+
|
|
20
|
+
# Default requested but no default server is configured.
|
|
21
|
+
class NoDefaultServerConfiguredError < Error; end
|
|
22
|
+
|
|
23
|
+
# More than one server is marked as default in configuration.
|
|
24
|
+
class MultipleDefaultServersError < Error; end
|
|
25
|
+
|
|
26
|
+
# Multiple servers are configured with the same identifier.
|
|
27
|
+
class DuplicateServerNameError < Error; end
|
|
28
|
+
|
|
29
|
+
# A server references a provider type that no provider package registered.
|
|
30
|
+
class UnknownProviderError < Error; end
|
|
31
|
+
|
|
32
|
+
# The requested server name is not present in configuration.
|
|
33
|
+
class UnknownServerNameError < Error; end
|
|
34
|
+
|
|
35
|
+
# A remote URL matches multiple configured servers, or none, so the server
|
|
36
|
+
# identity cannot be determined without caller input.
|
|
37
|
+
class AmbiguousRemoteError < Error; end
|
|
38
|
+
|
|
39
|
+
# ---- Provider interaction failures ----
|
|
40
|
+
|
|
41
|
+
# The provider CLI binary is not installed in PATH.
|
|
42
|
+
class ProviderCliMissingError < Error; end
|
|
43
|
+
|
|
44
|
+
# The provider CLI is unauthenticated or its credential lease expired.
|
|
45
|
+
class ProviderAuthenticationError < Error; end
|
|
46
|
+
|
|
47
|
+
# The remote forge endpoint is offline or unreachable.
|
|
48
|
+
class ProviderUnreachableError < Error; end
|
|
49
|
+
|
|
50
|
+
# The provider CLI returned invalid JSON or an unexpected schema.
|
|
51
|
+
class ProviderMalformedOutputError < Error; end
|
|
52
|
+
|
|
53
|
+
# The requested pull request, issue, branch, or commit does not exist.
|
|
54
|
+
class ProviderObjectNotFoundError < Error; end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -91,7 +91,7 @@ module Ace
|
|
|
91
91
|
elsif exclude_renames?
|
|
92
92
|
flags << "--diff-filter=ACDMTUXB"
|
|
93
93
|
elsif exclude_moves?
|
|
94
|
-
# Moves appear as
|
|
94
|
+
# Moves appear as renames with total similarity; disable such exact renames
|
|
95
95
|
flags << "-M0" # Disable rename detection (moves are renames at 100% similarity)
|
|
96
96
|
end
|
|
97
97
|
flags
|
|
@@ -164,7 +164,7 @@ module Ace
|
|
|
164
164
|
hash.each_key do |key|
|
|
165
165
|
key_str = key.to_s
|
|
166
166
|
next if KNOWN_KEYS.include?(key_str)
|
|
167
|
-
# Skip nested sections that may be
|
|
167
|
+
# Skip nested sections that may be propagated
|
|
168
168
|
next if %w[diff rebase pr squash default_branch remote verbose].include?(key_str)
|
|
169
169
|
|
|
170
170
|
warn "[ace-git] Unknown config key '#{key_str}' in DiffConfig - did you mean one of: #{KNOWN_KEYS.join(", ")}?"
|
|
@@ -35,17 +35,14 @@ module Ace
|
|
|
35
35
|
# @param executor [Module] Command executor
|
|
36
36
|
# @return [Hash] { ahead: Integer, behind: Integer }
|
|
37
37
|
def tracking_status(executor: Atoms::CommandExecutor)
|
|
38
|
-
|
|
38
|
+
ahead = count_commits("@{upstream}..HEAD", executor: executor)
|
|
39
|
+
behind = count_commits("HEAD..@{upstream}", executor: executor)
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
if ahead.nil? || behind.nil?
|
|
41
42
|
return {ahead: 0, behind: 0, error: "No tracking branch or not in git repo"}
|
|
42
43
|
end
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
{
|
|
46
|
-
ahead: parts[1].to_i,
|
|
47
|
-
behind: parts[0].to_i
|
|
48
|
-
}
|
|
45
|
+
{ahead: ahead, behind: behind}
|
|
49
46
|
end
|
|
50
47
|
|
|
51
48
|
# Get full branch information
|
|
@@ -85,6 +82,15 @@ module Ace
|
|
|
85
82
|
"#{behind} behind"
|
|
86
83
|
end
|
|
87
84
|
end
|
|
85
|
+
|
|
86
|
+
# Count commits in a rev-list range; nil when the range is invalid
|
|
87
|
+
# (e.g. no upstream configured)
|
|
88
|
+
def count_commits(range, executor: Atoms::CommandExecutor)
|
|
89
|
+
result = executor.execute("git", "rev-list", "--count", range)
|
|
90
|
+
return nil unless result[:success]
|
|
91
|
+
|
|
92
|
+
result[:output].strip.to_i
|
|
93
|
+
end
|
|
88
94
|
end
|
|
89
95
|
end
|
|
90
96
|
end
|
|
@@ -13,7 +13,7 @@ module Ace
|
|
|
13
13
|
class << self
|
|
14
14
|
# Load configuration using ace-config cascade with deep merge
|
|
15
15
|
# Priority: instance_config merged over global config (which is already defaults + user)
|
|
16
|
-
# @param instance_config [Hash] Instance-level configuration (
|
|
16
|
+
# @param instance_config [Hash] Instance-level configuration (top priority)
|
|
17
17
|
# @return [Models::DiffConfig] Merged configuration
|
|
18
18
|
def load(instance_config = {})
|
|
19
19
|
# Get global config from ace-git (already merged defaults + user per ADR-022)
|