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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '089e377ec56099fb872544bfc754f6f95f00ecaf157715ecad93e726e9c8960f'
|
|
4
|
+
data.tar.gz: 35bb8b556655c0ef1804fb2fe26039dd182115e558127254c49e17da9ce3f1ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c156b7e8b123f15f9d305d27933d68de72d640e16a803a9d7b5ce2cce12c1c7cf537bb7e6ae31e7c8ca044999869160ccf835281fdcc51e3bee76589cd43363f
|
|
7
|
+
data.tar.gz: 4eaeac69d71391c491718c68e68e1599a0a3e1991eae4a25d3e4e2a922959ef13e940c7d863eae01478a78ae9f81940a016fcac46da27ee7bbdf427f64527a03
|
|
@@ -10,7 +10,27 @@ 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
|
|
29
|
+
|
|
30
|
+
# Provider packages required on first use (library names under ace/git/*).
|
|
31
|
+
# Both shipped provider gems are loaded by default; remove entries for
|
|
32
|
+
# providers you do not use.
|
|
33
|
+
providers: [github, forgejo]
|
|
14
34
|
|
|
15
35
|
# Status command settings
|
|
16
36
|
status:
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.25.0] - 2026-09-28
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Forge-neutral `ace-git pr` lifecycle CLI: `show`, `create`, `update`, `ready`, `merge`
|
|
14
|
+
with `--server` / `--default-server` selection and `--format json`.
|
|
15
|
+
- Provider contract for PR lifecycle mutations (exact base/head identity,
|
|
16
|
+
idempotent create reconciliation, expected-head enforcement, unknown-outcome
|
|
17
|
+
classification) on `Ace::Git::Providers::Base`.
|
|
18
|
+
- Normalized evidence types: PR provenance fields (`head_repository_url`,
|
|
19
|
+
`base_repository_url`, `merge_commit_sha`), `ProviderPullRequestIdentity`,
|
|
20
|
+
`ProviderMutationReceipt`, `ProviderCleanupProof` with the cleanup proof
|
|
21
|
+
status vocabulary.
|
|
22
|
+
- Classified lifecycle failures: identity mismatch, conflicting exact matches,
|
|
23
|
+
expected-head conflict, unsupported capability, unknown mutation outcome.
|
|
24
|
+
- Server selection helpers `ServerRegistry.resolve_for`, `matching_servers`,
|
|
25
|
+
and `servers_for_owner_repo`; `PrReference` atom for `NUMBER`,
|
|
26
|
+
`owner/repo#NUMBER`, and URL identifiers.
|
|
27
|
+
- Config-driven provider loading via `git.providers` (default: both shipped
|
|
28
|
+
provider gems).
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- PR identifiers now validate repository identity against the resolved server;
|
|
32
|
+
mismatches fail before any mutation. `owner/repo#NUMBER` requires exactly one
|
|
33
|
+
configured matching server and never assumes github.com.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- Forge-neutral `ace-git pr` lifecycle CLI: `show`, `create`, `update`, `ready`, `merge`
|
|
38
|
+
with `--server` / `--default-server` selection and `--format json`.
|
|
39
|
+
- Provider contract for PR lifecycle mutations (exact base/head identity,
|
|
40
|
+
idempotent create reconciliation, expected-head enforcement, unknown-outcome
|
|
41
|
+
classification) on `Ace::Git::Providers::Base`.
|
|
42
|
+
- Normalized evidence types: PR provenance fields (`head_repository_url`,
|
|
43
|
+
`base_repository_url`, `merge_commit_sha`), `ProviderPullRequestIdentity`,
|
|
44
|
+
`ProviderMutationReceipt`, `ProviderCleanupProof` with the cleanup proof
|
|
45
|
+
status vocabulary.
|
|
46
|
+
- Classified lifecycle failures: identity mismatch, conflicting exact matches,
|
|
47
|
+
expected-head conflict, unsupported capability, unknown mutation outcome.
|
|
48
|
+
- Server selection helpers `ServerRegistry.resolve_for`, `matching_servers`,
|
|
49
|
+
and `servers_for_owner_repo`; `PrReference` atom for `NUMBER`,
|
|
50
|
+
`owner/repo#NUMBER`, and URL identifiers.
|
|
51
|
+
- Config-driven provider loading via `git.providers` (default: both shipped
|
|
52
|
+
provider gems).
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
- PR identifiers now validate repository identity against the resolved server;
|
|
56
|
+
mismatches fail before any mutation. `owner/repo#NUMBER` requires exactly one
|
|
57
|
+
configured matching server and never assumes github.com.
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
## [0.24.0] - 2026-09-21
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
- 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.
|
|
65
|
+
- Classified failure taxonomy in `git/errors.rb`: `NoDefaultServerConfiguredError`, `MultipleDefaultServersError`, `DuplicateServerNameError`, `UnknownProviderError`, `UnknownServerNameError`, `AmbiguousRemoteError`, `ProviderCliMissingError`, `ProviderAuthenticationError`, `ProviderUnreachableError`, `ProviderMalformedOutputError`, `ProviderObjectNotFoundError`.
|
|
66
|
+
- `git.servers` configuration section supporting multiple uniquely named servers with at most one explicit default.
|
|
67
|
+
- Boundary proof: local Git operations succeed with provider CLIs scrubbed from PATH, and a test pins the core lib to zero forge CLI references.
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
|
|
71
|
+
- 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.
|
|
72
|
+
- Forge server URLs in gemspec metadata point at the canonical delivery forge instead of hardcoded GitHub URLs.
|
|
73
|
+
|
|
74
|
+
### Removed
|
|
75
|
+
|
|
76
|
+
- 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.
|
|
10
77
|
|
|
11
78
|
## [0.23.1] - 2026-09-02
|
|
12
79
|
|
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,12 @@ gem install ace-git
|
|
|
23
23
|
|
|
24
24
|
## Command Overview
|
|
25
25
|
|
|
26
|
-
`ace-git` ships
|
|
26
|
+
`ace-git` ships the following 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
|
|
31
|
+
- `pr` for the forge-neutral pull request lifecycle (`show`, `create`, `update`, `ready`, `merge`)
|
|
32
32
|
- `version` for the installed package version
|
|
33
33
|
|
|
34
34
|
`ace-git` with no arguments shows help. Git range shorthand such as `HEAD~5..HEAD` routes to `diff`.
|
|
@@ -155,42 +155,50 @@ ace-git branch --format json
|
|
|
155
155
|
**Options:**
|
|
156
156
|
- `--format, -f` - Output format: `text` (default), `json`
|
|
157
157
|
|
|
158
|
-
### `ace-git pr
|
|
158
|
+
### `ace-git pr`
|
|
159
159
|
|
|
160
|
-
|
|
160
|
+
Forge-neutral pull request lifecycle over the configured forge servers
|
|
161
|
+
(`git.servers`). Every subcommand accepts `--server NAME` / `--default-server`
|
|
162
|
+
(mutually exclusive; with neither, the configured repository remote is
|
|
163
|
+
resolved) and `--format json`. Server, provider, and the exact head SHA are
|
|
164
|
+
part of every result; failures are classified and exit nonzero.
|
|
161
165
|
|
|
162
166
|
```bash
|
|
163
|
-
#
|
|
164
|
-
ace-git pr
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
#
|
|
170
|
-
ace-git pr
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
167
|
+
# Show normalized PR evidence (number, owner/repo#number, or URL)
|
|
168
|
+
ace-git pr show 25
|
|
169
|
+
ace-git pr show cs3b/ace#25 --format json
|
|
170
|
+
ace-git pr show https://forge.example.com/cs3b/ace/pull/25 --server forgejo-lab
|
|
171
|
+
|
|
172
|
+
# Create (or reconcile to) a draft PR for a pushed branch.
|
|
173
|
+
# The pushed head SHA is proven against the PR; draft is the default.
|
|
174
|
+
ace-git pr create --head feature --base main \
|
|
175
|
+
--expected-head "$(git rev-parse HEAD)" \
|
|
176
|
+
--title "Add feature" --body-file /tmp/body.md --format json
|
|
177
|
+
|
|
178
|
+
# Fork PRs name the source repository explicitly; the base repository is
|
|
179
|
+
# always the selected configured server. No fork inference happens.
|
|
180
|
+
ace-git pr create --head feature --head-repo https://forge.example.com/fork/ace \
|
|
181
|
+
--base main --expected-head SHA --title "Fork PR" --draft
|
|
182
|
+
|
|
183
|
+
# Update title/body after head verification
|
|
184
|
+
ace-git pr update 25 --expected-head SHA --title "New title" --body-file /tmp/body.md
|
|
185
|
+
|
|
186
|
+
# Mark a draft ready (provider capability; unsupported providers fail classified)
|
|
187
|
+
ace-git pr ready 25 --expected-head SHA
|
|
188
|
+
|
|
189
|
+
# Merge with provider-side expected-head enforcement; no default method
|
|
190
|
+
ace-git pr merge 25 --expected-head SHA --method squash
|
|
174
191
|
```
|
|
175
192
|
|
|
176
|
-
**
|
|
177
|
-
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
-
|
|
183
|
-
|
|
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
|
-
```
|
|
193
|
+
**Behavior guarantees:**
|
|
194
|
+
- Create is idempotent: one exact open base/head match returns the existing
|
|
195
|
+
PR (`idempotency: existing`); multiple matches are a conflict.
|
|
196
|
+
- `merge` requires the provider to enforce `--expected-head` atomically;
|
|
197
|
+
providers without that capability return an unsupported-capability failure
|
|
198
|
+
instead of racing.
|
|
199
|
+
- A create request whose outcome is unknown (transport failure after send)
|
|
200
|
+
reports an unknown outcome with the exact identity to reconcile; it never
|
|
201
|
+
retries automatically.
|
|
194
202
|
|
|
195
203
|
### `ace-git version`
|
|
196
204
|
|
|
@@ -267,11 +275,18 @@ Note: `test/**/*` and `spec/**/*` are NOT excluded by default - test changes are
|
|
|
267
275
|
- **ace-bundle** - Load ACE workflow instructions directly
|
|
268
276
|
- **ace-nav** - Discover workflow and template protocol paths
|
|
269
277
|
|
|
270
|
-
##
|
|
278
|
+
## Provider Packages
|
|
271
279
|
|
|
272
|
-
`ace-git`
|
|
280
|
+
`ace-git` is forge-neutral. It defines the server registry (`Ace::Git::ServerRegistry`),
|
|
281
|
+
the provider contract (`Ace::Git::Providers::Base`), and normalized evidence types;
|
|
282
|
+
provider packages own all forge CLI behavior:
|
|
273
283
|
|
|
274
|
-
-
|
|
275
|
-
|
|
284
|
+
- **ace-git-github** - GitHub behavior via the `gh` CLI (PR lookups, diffs, issue
|
|
285
|
+
synchronization). Registers under the `github` provider type.
|
|
286
|
+
- **ace-git-forgejo** - Forgejo behavior via the `fj` CLI. Registers under the
|
|
287
|
+
`forgejo` provider type.
|
|
276
288
|
|
|
277
|
-
|
|
289
|
+
Configure servers in `.ace/git.yml` (or project config) under `git.servers`, then
|
|
290
|
+
resolve them by name, by default, or from a git remote URL. Every failure path is
|
|
291
|
+
classified (missing CLI, unauthenticated, unreachable, malformed output, object
|
|
292
|
+
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,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "server_url"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Git
|
|
7
|
+
module Atoms
|
|
8
|
+
# Parse a user-supplied pull request identifier into its exact components.
|
|
9
|
+
#
|
|
10
|
+
# Three accepted shapes:
|
|
11
|
+
# - bare number: "25"
|
|
12
|
+
# - owner/repo#number: "cs3b/ace#25"
|
|
13
|
+
# - full URL: "https://forge.example.com/cs3b/ace/pull/25"
|
|
14
|
+
# "https://forge.example.com/cs3b/ace/pulls/25"
|
|
15
|
+
#
|
|
16
|
+
# A URL's repository part is canonicalized through {ServerUrl.normalize}
|
|
17
|
+
# so identity matching never assumes a hostname. Parsing is purely
|
|
18
|
+
# textual; it never contacts a forge and never infers github.com.
|
|
19
|
+
module PrReference
|
|
20
|
+
# Parsed reference: `number` always present; exactly one of
|
|
21
|
+
# `repository_url` (canonical URL form) or `owner_repo` is set for
|
|
22
|
+
# explicit-repository shapes, both nil for a bare number.
|
|
23
|
+
Reference = Data.define(:number, :repository_url, :owner_repo) do
|
|
24
|
+
# @return [Boolean] true when the reference names a repository
|
|
25
|
+
def repository_explicit?
|
|
26
|
+
!repository_url.nil? || !owner_repo.nil?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [Hash] plain representation
|
|
30
|
+
def to_h
|
|
31
|
+
{number: number, repository_url: repository_url, owner_repo: owner_repo}
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
BARE_NUMBER_PATTERN = /\A\d+\z/
|
|
36
|
+
OWNER_REPO_PATTERN = /\A(?<owner>[\w.\-]+)\/(?<repo>[\w.\-]+)#(?<number>\d+)\z/
|
|
37
|
+
URL_PATTERN = %r{
|
|
38
|
+
\Ahttps?://[^/\s]+/(?<path>.+?)/pulls?/(?<number>\d+)(?:[/?#].*)?\z
|
|
39
|
+
}ix
|
|
40
|
+
|
|
41
|
+
class << self
|
|
42
|
+
# Parse a pull request identifier.
|
|
43
|
+
#
|
|
44
|
+
# @param input [String, Integer] identifier in any accepted shape
|
|
45
|
+
# @return [Reference, nil] parsed reference, or nil when unrecognized
|
|
46
|
+
def parse(input)
|
|
47
|
+
text = input.to_s.strip
|
|
48
|
+
return nil if text.empty?
|
|
49
|
+
|
|
50
|
+
if (match = text.match(BARE_NUMBER_PATTERN))
|
|
51
|
+
return Reference.new(number: match[0].to_i, repository_url: nil, owner_repo: nil)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if (match = text.match(OWNER_REPO_PATTERN))
|
|
55
|
+
return Reference.new(
|
|
56
|
+
number: match[:number].to_i,
|
|
57
|
+
repository_url: nil,
|
|
58
|
+
owner_repo: "#{match[:owner]}/#{match[:repo]}"
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
if (match = text.match(URL_PATTERN))
|
|
63
|
+
return Reference.new(
|
|
64
|
+
number: match[:number].to_i,
|
|
65
|
+
repository_url: ServerUrl.normalize("#{text.split("/")[0..2].join("/")}/#{match[:path]}"),
|
|
66
|
+
owner_repo: nil
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
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
|