ace-git-forgejo 0.2.0 → 0.3.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/CHANGELOG.md +39 -0
- data/README.md +35 -2
- data/lib/ace/git/forgejo/cli_executor.rb +112 -31
- data/lib/ace/git/forgejo/provider.rb +145 -55
- data/lib/ace/git/forgejo/repository_binding.rb +203 -0
- data/lib/ace/git/forgejo/version.rb +1 -1
- data/lib/ace/git/forgejo.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c8a35836d33fbf6fc14e52f4c27ef396244d1f809bc919ceca6ac0202db5e583
|
|
4
|
+
data.tar.gz: 548b2f7734d7f98ef2594e9630ddeb499ae9ead7f8c4297498ebe8726217c4a2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c19a8e1f44853f9c173e51118a42f1de9fd6ab04d77c01371c68f16d867f654b938f4cf5b252d8f7d4300c4bdcca826c53ed7dafddc6e616051fab8292aa457d
|
|
7
|
+
data.tar.gz: b312fbb4a37d5e871e55148f40e90951ebc2692c474d87e0052adb6beb0647fb24347b3599b914c7a4c7a93f9ca4726f201bd67301f4ba84ded528db979168da
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-09-29
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Selected-repository binding for every `fj` repository command: the
|
|
14
|
+
resolved server's host/owner/repository is validated once (malformed
|
|
15
|
+
selections raise `ConfigError` before any subprocess) and stamped onto
|
|
16
|
+
every call via `-H` plus qualified `owner/repo#N` ids or `-r owner/repo`,
|
|
17
|
+
so a named server selection can never be silently retargeted by cwd,
|
|
18
|
+
remotes, default login, or another checkout.
|
|
19
|
+
- Capability table grounded in observed forgejo-cli v0.6.0 help/output
|
|
20
|
+
(upstream release binary, plus real-server read-only probes); operations
|
|
21
|
+
without an observed argv form and installed versions outside the observed
|
|
22
|
+
set refuse with `ProviderUnsupportedCapabilityError` before launch.
|
|
23
|
+
- Returned-identity validation: PR/issue numbers and repository view
|
|
24
|
+
full-name/URL must agree with the selection or evidence fails closed
|
|
25
|
+
with `ProviderIdentityMismatchError`.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- All `fj` subprocess launches now go through a two-path executor boundary:
|
|
29
|
+
an allowlisted control path (`fj version`, `fj auth list`) and the
|
|
30
|
+
repository boundary. Authentication compares `fj auth list` lines exactly
|
|
31
|
+
against the selected authority (substring hosts no longer count) and
|
|
32
|
+
scans both output streams (observed v0.6.0 prints "No logins." on stderr
|
|
33
|
+
with exit 0).
|
|
34
|
+
- Unauthenticated-API failures are classified as authentication errors
|
|
35
|
+
(observed Lab text "Only signed in user is allowed to call APIs.").
|
|
36
|
+
- `merge_commit_sha` stays empty: observed fj v0.6.0 exposes no
|
|
37
|
+
authoritative merge-commit field for a selected PR, so cleanup evidence
|
|
38
|
+
never claims one.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
- Selected-host fidelity: `-H` now carries the selected `scheme://authority`
|
|
42
|
+
(fj assumes HTTPS for a bare host, so an http authority was silently
|
|
43
|
+
upgraded), non-http(s) server schemes are rejected as configuration
|
|
44
|
+
errors before any subprocess, and the provider refuses to launch
|
|
45
|
+
repository commands when the readable fj keys file aliases the selected
|
|
46
|
+
host to a different endpoint (read-only check; absent/unreadable keys
|
|
47
|
+
file is allowed, never modified).
|
|
48
|
+
|
|
10
49
|
## [0.2.0] - 2026-09-28
|
|
11
50
|
|
|
12
51
|
### Added
|
data/README.md
CHANGED
|
@@ -5,8 +5,20 @@ Forgejo provider for the forge-neutral [`ace-git`](../ace-git) core.
|
|
|
5
5
|
This package owns **all** Forgejo-specific behavior for ACE:
|
|
6
6
|
|
|
7
7
|
- `fj` CLI invocation (with timeout handling and structured output)
|
|
8
|
-
- `fj`
|
|
9
|
-
|
|
8
|
+
- **Selected-repository binding**: every repository-scoped `fj` call is
|
|
9
|
+
bound to the resolved server's exact host/owner/repository — `-H` plus
|
|
10
|
+
qualified `owner/repo#N` object ids or `-r owner/repo` — never cwd,
|
|
11
|
+
remotes, a default login, or user `fj` configuration. Malformed server
|
|
12
|
+
URLs fail as `ConfigError` before any subprocess; operations whose argv
|
|
13
|
+
form was never observed on a real `fj` refuse with
|
|
14
|
+
`ProviderUnsupportedCapabilityError` before launch, and the installed
|
|
15
|
+
`fj` version must be one of the observed versions (`v0.6.0`) to inherit
|
|
16
|
+
the capability table.
|
|
17
|
+
- `fj` minimal-style output parsing and normalization, with returned
|
|
18
|
+
identity validated against the selection (misrouted evidence raises
|
|
19
|
+
`ProviderIdentityMismatchError` instead of being relabelled)
|
|
20
|
+
- `fj` authentication verification (`fj auth list`, compared exactly
|
|
21
|
+
against the selected host authority)
|
|
10
22
|
- Forgejo-specific identifier formats (including `/pulls/N` web URLs)
|
|
11
23
|
|
|
12
24
|
It implements the provider contract defined by the core
|
|
@@ -21,3 +33,24 @@ Failures are classified with the shared taxonomy
|
|
|
21
33
|
`ProviderUnreachableError`, `ProviderMalformedOutputError`,
|
|
22
34
|
`ProviderObjectNotFoundError`). Ad-hoc curl fallbacks are strictly
|
|
23
35
|
forbidden; there is no silent fallback of any kind.
|
|
36
|
+
|
|
37
|
+
## Supported operations (observed on forgejo-cli v0.6.0)
|
|
38
|
+
|
|
39
|
+
PR view/diff/head-commits, PR search (all/open, client-side newest-first
|
|
40
|
+
window — `fj` has no `--limit`), PR create (same-repository head only),
|
|
41
|
+
PR edit title/body, issue view, actions tasks, repository view, version
|
|
42
|
+
and `auth list` probes. `fj` v0.6.0 exposes no draft-to-ready command, no
|
|
43
|
+
atomic expected-head merge, and no authoritative merge-commit field, so
|
|
44
|
+
`ready_pull_request`/`merge_pull_request` refuse as unsupported
|
|
45
|
+
capabilities and `merge_commit_sha` stays empty — worktree cleanup
|
|
46
|
+
conservatively retains Forgejo checkouts until `fj` exposes that field.
|
|
47
|
+
The capability evidence and its provenance live in the
|
|
48
|
+
`8wr.t.uj0` task folder (`evidence/fj-capabilities.md`).
|
|
49
|
+
|
|
50
|
+
Endpoint fidelity: `-H` carries the selected `scheme://authority` (fj
|
|
51
|
+
otherwise assumes HTTPS), and because fj v0.6.0 silently applies its
|
|
52
|
+
keys-file `aliases` map to `-H`, the provider read-only checks the
|
|
53
|
+
observed keys-file locations and refuses a repository command when an
|
|
54
|
+
alias redirects the selected host — never modifying user fj
|
|
55
|
+
configuration; an absent or unreadable keys file is allowed.
|
|
56
|
+
|
|
@@ -2,59 +2,129 @@
|
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
4
|
require "timeout"
|
|
5
|
+
require_relative "parsers"
|
|
6
|
+
require_relative "repository_binding"
|
|
5
7
|
|
|
6
8
|
module Ace
|
|
7
9
|
module Git
|
|
8
10
|
module Forgejo
|
|
9
11
|
# Executes the Forgejo CLI (`fj`) with timeout and structured output.
|
|
10
12
|
#
|
|
11
|
-
# Owns every `fj` subprocess invocation for ACE.
|
|
12
|
-
#
|
|
13
|
+
# Owns every `fj` subprocess invocation for ACE. Two launch paths exist:
|
|
14
|
+
#
|
|
15
|
+
# - {execute_control}: allowlisted login/version probes (`fj version`,
|
|
16
|
+
# `fj auth list`) that are host- or installation-scoped.
|
|
17
|
+
# - {execute_repository}: repository-scoped commands, which require the
|
|
18
|
+
# validated selected {RepositoryBinding::Target} and a named operation
|
|
19
|
+
# with an observed argv form. The executor stamps `-H <authority>`
|
|
20
|
+
# from the target and refuses unobserved operations before launch —
|
|
21
|
+
# repository selection never comes from cwd, remotes, a default
|
|
22
|
+
# login, or user `fj` configuration.
|
|
23
|
+
#
|
|
24
|
+
# A test runner may be injected instead of spawning real processes
|
|
25
|
+
# (see Providers::Base).
|
|
13
26
|
module CliExecutor
|
|
14
27
|
DEFAULT_TIMEOUT = 30
|
|
15
28
|
BINARY = "fj"
|
|
16
29
|
|
|
30
|
+
# Host/installation-scoped probes; never repository targeting.
|
|
31
|
+
CONTROL_ARGS = {
|
|
32
|
+
version: ["version"],
|
|
33
|
+
auth_list: ["auth", "list"]
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
17
36
|
class << self
|
|
18
|
-
# Execute an
|
|
37
|
+
# Execute an allowlisted control command with timeout.
|
|
19
38
|
#
|
|
20
|
-
# @param
|
|
21
|
-
# @param timeout [Integer, nil] seconds before the call times out
|
|
22
|
-
# @param runner [Proc, nil] injectable command runner for tests
|
|
39
|
+
# @param command [Symbol] key in {CONTROL_ARGS}
|
|
23
40
|
# @return [Hash] {success:, stdout:, stderr:, exit_code:}
|
|
24
41
|
# @raise [Ace::Git::ProviderCliMissingError] when `fj` is missing
|
|
25
42
|
# @raise [Ace::Git::ProviderUnreachableError] when the call times out
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
raise Ace::Git::ProviderUnreachableError,
|
|
31
|
-
"fj command timed out after #{timeout_seconds} seconds: #{args.join(" ")}"
|
|
43
|
+
# @raise [ArgumentError] when the command is not allowlisted
|
|
44
|
+
def execute_control(command, timeout: nil, runner: nil)
|
|
45
|
+
args = CONTROL_ARGS.fetch(command) do
|
|
46
|
+
raise ArgumentError, "Unknown fj control command #{command.inspect}"
|
|
32
47
|
end
|
|
48
|
+
execute(args, timeout: timeout, runner: runner)
|
|
49
|
+
end
|
|
33
50
|
|
|
34
|
-
|
|
51
|
+
# Execute one repository-scoped operation bound to the selected
|
|
52
|
+
# target. The operation must have an observed argv form; the host
|
|
53
|
+
# flag is stamped here so no caller can omit selection.
|
|
54
|
+
#
|
|
55
|
+
# @param target [RepositoryBinding::Target] validated selected repository
|
|
56
|
+
# @param operation [Symbol] repository operation (RepositoryBinding::FORMS)
|
|
57
|
+
# @param arguments [Array<Object>] per-operation arguments
|
|
58
|
+
# @return [Hash] {success:, stdout:, stderr:, exit_code:}
|
|
59
|
+
# @raise [Ace::Git::ConfigError] when the target is missing/invalid
|
|
60
|
+
# @raise [Ace::Git::ProviderUnsupportedCapabilityError] when the
|
|
61
|
+
# operation has no observed form
|
|
62
|
+
# @raise [Ace::Git::ProviderCliMissingError] when `fj` is missing
|
|
63
|
+
# @raise [Ace::Git::ProviderUnreachableError] when the call times out
|
|
64
|
+
def execute_repository(target:, operation:, arguments: [], timeout: nil, runner: nil)
|
|
65
|
+
unless target.is_a?(RepositoryBinding::Target)
|
|
66
|
+
raise Ace::Git::ConfigError,
|
|
67
|
+
"Repository commands require a validated selected Forgejo target"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
argv = RepositoryBinding.argv_for(operation, target, arguments)
|
|
71
|
+
# `-H` carries the full selected URL: fj otherwise assumes HTTPS,
|
|
72
|
+
# which would drop a configured http authority (observed v0.6.0).
|
|
73
|
+
execute(["-H", target.host_url] + argv, timeout: timeout, runner: runner)
|
|
35
74
|
end
|
|
36
75
|
|
|
37
76
|
# Probe whether the `fj` binary is installed and runnable.
|
|
38
77
|
# `fj` has no `--version` flag (it rejects it with "unexpected
|
|
39
78
|
# argument"); the supported presence probe is `fj version`.
|
|
40
79
|
def installed?(runner: nil)
|
|
41
|
-
result =
|
|
42
|
-
result
|
|
80
|
+
result = execute_control(:version, runner: runner)
|
|
81
|
+
result[:success]
|
|
43
82
|
rescue Ace::Git::ProviderCliMissingError
|
|
44
83
|
false
|
|
45
84
|
end
|
|
46
85
|
|
|
47
|
-
#
|
|
48
|
-
# `fj
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
86
|
+
# @return [String, nil] installed forgejo-cli version (e.g. "v0.6.0"),
|
|
87
|
+
# or nil when `fj version` output is unparseable
|
|
88
|
+
# @raise [Ace::Git::ProviderCliMissingError] when `fj` is missing
|
|
89
|
+
# or the version probe fails
|
|
90
|
+
def installed_version(runner: nil)
|
|
91
|
+
result = execute_control(:version, runner: runner)
|
|
92
|
+
unless result[:success]
|
|
93
|
+
raise Ace::Git::ProviderCliMissingError,
|
|
94
|
+
"Forgejo CLI (fj) is not installed or not runnable; install the Forgejo CLI and retry"
|
|
95
|
+
end
|
|
53
96
|
|
|
54
|
-
|
|
55
|
-
|
|
97
|
+
match = result[:stdout].to_s.match(/\bfj\s+(v?\d+\.\d+\.\d+)\b/)
|
|
98
|
+
match && match[1].sub(/\Av?/, "v")
|
|
99
|
+
end
|
|
56
100
|
|
|
57
|
-
|
|
101
|
+
# @raise [Ace::Git::ProviderUnsupportedCapabilityError] when the
|
|
102
|
+
# installed version was never observed; it must not inherit the
|
|
103
|
+
# capability table of an observed version
|
|
104
|
+
def check_version_supported!(runner: nil)
|
|
105
|
+
version = installed_version(runner: runner)
|
|
106
|
+
return version if RepositoryBinding::OBSERVED_VERSIONS.include?(version)
|
|
107
|
+
|
|
108
|
+
raise Ace::Git::ProviderUnsupportedCapabilityError,
|
|
109
|
+
"Installed forgejo-cli version #{version || "of unknown version"} was never " \
|
|
110
|
+
"observed; observed versions: #{RepositoryBinding::OBSERVED_VERSIONS.join(", ")}. " \
|
|
111
|
+
"Observe its real surface before repository operations are allowed " \
|
|
112
|
+
"(see the ace-git-forgejo repository binding evidence)"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Probe whether the resolved server authority has a configured `fj`
|
|
116
|
+
# login. `fj auth list` prints "No logins." (stderr, exit 0) when no
|
|
117
|
+
# instance is configured; configured hosts are listed one per line.
|
|
118
|
+
# Comparison is exact against the selected authority — never a
|
|
119
|
+
# substring of an unrelated line.
|
|
120
|
+
def authenticated?(authority, runner: nil)
|
|
121
|
+
result = execute_control(:auth_list, runner: runner)
|
|
122
|
+
return false unless result[:success]
|
|
123
|
+
|
|
124
|
+
lines = "#{result[:stdout]}\n#{result[:stderr]}".lines
|
|
125
|
+
lines.map { |line| Parsers.clean(line) }.any? do |line|
|
|
126
|
+
!line.empty? && line.casecmp(authority.to_s)&.zero?
|
|
127
|
+
end
|
|
58
128
|
rescue Ace::Git::ProviderCliMissingError
|
|
59
129
|
false
|
|
60
130
|
end
|
|
@@ -68,18 +138,31 @@ module Ace
|
|
|
68
138
|
end
|
|
69
139
|
|
|
70
140
|
# @raise [Ace::Git::ProviderAuthenticationError] when unauthenticated
|
|
71
|
-
def check_authenticated!(
|
|
72
|
-
return true if authenticated?(
|
|
141
|
+
def check_authenticated!(authority:, runner: nil)
|
|
142
|
+
return true if authenticated?(authority, runner: runner)
|
|
73
143
|
|
|
74
144
|
raise Ace::Git::ProviderAuthenticationError,
|
|
75
|
-
"Forgejo CLI (fj) has no usable login for #{
|
|
145
|
+
"Forgejo CLI (fj) has no usable login for #{authority || "the target host"}; " \
|
|
76
146
|
"check fj auth list"
|
|
77
147
|
end
|
|
78
148
|
|
|
79
149
|
# Run a full argument vector against `fj`.
|
|
80
150
|
#
|
|
81
|
-
# @return [Hash] result hash
|
|
82
|
-
def
|
|
151
|
+
# @return [Hash] result hash
|
|
152
|
+
def execute(args, timeout: nil, runner: nil)
|
|
153
|
+
timeout_seconds = timeout || Ace::Git.network_timeout || DEFAULT_TIMEOUT
|
|
154
|
+
result = run_command([BINARY] + args, timeout_seconds, runner)
|
|
155
|
+
if result == :timeout
|
|
156
|
+
raise Ace::Git::ProviderUnreachableError,
|
|
157
|
+
"fj command timed out after #{timeout_seconds} seconds: #{args.join(" ")}"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
result
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
def run_command(command, timeout_seconds, runner)
|
|
83
166
|
if runner
|
|
84
167
|
call_runner(runner, command, timeout_seconds)
|
|
85
168
|
else
|
|
@@ -87,8 +170,6 @@ module Ace
|
|
|
87
170
|
end
|
|
88
171
|
end
|
|
89
172
|
|
|
90
|
-
private
|
|
91
|
-
|
|
92
173
|
def call_runner(runner, command, timeout_seconds)
|
|
93
174
|
runner.call(args: command, timeout: timeout_seconds, env: {"LC_ALL" => "C"})
|
|
94
175
|
rescue StandardError => e
|
|
@@ -11,9 +11,14 @@ module Ace
|
|
|
11
11
|
# Forgejo provider implementation of the shared provider contract.
|
|
12
12
|
#
|
|
13
13
|
# Owns Forgejo terminology and translates `fj` responses into the
|
|
14
|
-
# normalized evidence types defined by the ace-git core. Every
|
|
15
|
-
#
|
|
16
|
-
#
|
|
14
|
+
# normalized evidence types defined by the ace-git core. Every
|
|
15
|
+
# repository-scoped call is bound to the selected server's exact
|
|
16
|
+
# host/owner/repository through the {CliExecutor} repository boundary:
|
|
17
|
+
# the selected identity is mandatory, unobserved operations refuse
|
|
18
|
+
# before launch, and returned evidence is checked for conflicting
|
|
19
|
+
# identity. Every failure is classified with the shared taxonomy;
|
|
20
|
+
# `fj` failures are never hidden behind ad-hoc curl calls or silent
|
|
21
|
+
# fallbacks.
|
|
17
22
|
class Provider < Ace::Git::Providers::Base
|
|
18
23
|
class << self
|
|
19
24
|
def display_name
|
|
@@ -31,14 +36,15 @@ module Ace
|
|
|
31
36
|
CliExecutor.check_installed!(runner: runner)
|
|
32
37
|
end
|
|
33
38
|
|
|
34
|
-
# @return [Boolean] true when the server host has a
|
|
39
|
+
# @return [Boolean] true when the selected server's host has a
|
|
40
|
+
# configured `fj` login
|
|
35
41
|
def authenticated?
|
|
36
|
-
CliExecutor.authenticated?(
|
|
42
|
+
CliExecutor.authenticated?(repository_target.authority, runner: runner)
|
|
37
43
|
end
|
|
38
44
|
|
|
39
45
|
# @raise [Ace::Git::ProviderAuthenticationError] when unauthenticated
|
|
40
46
|
def check_authenticated!
|
|
41
|
-
CliExecutor.check_authenticated!(
|
|
47
|
+
CliExecutor.check_authenticated!(authority: repository_target.authority, runner: runner)
|
|
42
48
|
end
|
|
43
49
|
|
|
44
50
|
# @return [ProviderPullRequest] normalized pull request evidence
|
|
@@ -51,13 +57,13 @@ module Ace
|
|
|
51
57
|
|
|
52
58
|
# @return [ProviderPullRequest, nil] evidence for the branch's PR
|
|
53
59
|
def pull_request_for_branch(branch:)
|
|
54
|
-
# `fj pr search` has no --limit flag; fetch all
|
|
55
|
-
|
|
60
|
+
# `fj pr search` has no --limit flag (observed v0.6.0); fetch all
|
|
61
|
+
# and order newest-first.
|
|
62
|
+
listing = fj(:pr_search, "all")
|
|
56
63
|
entries = Parsers.parse_search(listing).sort_by { |entry| -entry[:number] }
|
|
57
64
|
|
|
58
65
|
entries.each do |entry|
|
|
59
|
-
|
|
60
|
-
parsed = Parsers.parse_pr_view(view)
|
|
66
|
+
parsed = view_pr(entry[:number])
|
|
61
67
|
next unless parsed && parsed[:head_ref] == branch
|
|
62
68
|
|
|
63
69
|
parsed[:head_sha] = head_sha_of(entry[:number])
|
|
@@ -69,7 +75,7 @@ module Ace
|
|
|
69
75
|
|
|
70
76
|
# @return [String] unified diff text for the pull request
|
|
71
77
|
def pull_request_diff(number:)
|
|
72
|
-
fj(
|
|
78
|
+
fj(:pr_diff, request_number!(number))
|
|
73
79
|
end
|
|
74
80
|
|
|
75
81
|
# `fj pr search` has no --limit flag; cap client-side after sorting
|
|
@@ -77,7 +83,7 @@ module Ace
|
|
|
77
83
|
#
|
|
78
84
|
# @return [Array<ProviderPullRequest>] recent PRs, newest first
|
|
79
85
|
def recent_pull_requests(limit:)
|
|
80
|
-
listing = fj(
|
|
86
|
+
listing = fj(:pr_search, "all")
|
|
81
87
|
Parsers.parse_search(listing)
|
|
82
88
|
.map { |entry| normalize_search_entry(entry) }
|
|
83
89
|
.sort_by { |pr| -pr.number }
|
|
@@ -86,10 +92,11 @@ module Ace
|
|
|
86
92
|
|
|
87
93
|
# @return [ProviderIssue] normalized issue evidence
|
|
88
94
|
def issue(number:)
|
|
89
|
-
view = fj(
|
|
95
|
+
view = fj(:issue_view, request_number!(number))
|
|
90
96
|
parsed = Parsers.parse_issue_view(view) ||
|
|
91
97
|
raise(Ace::Git::ProviderMalformedOutputError,
|
|
92
98
|
"Unrecognized `fj issue view #{number}` output; update the Forgejo provider parser")
|
|
99
|
+
verify_returned_number!(parsed, number, "issue")
|
|
93
100
|
|
|
94
101
|
Ace::Git::ProviderIssue.new(
|
|
95
102
|
server_name: server.name,
|
|
@@ -104,7 +111,7 @@ module Ace
|
|
|
104
111
|
|
|
105
112
|
# @return [Array<ProviderCheck>] normalized check evidence for a ref
|
|
106
113
|
def checks(ref:)
|
|
107
|
-
tasks = fj(
|
|
114
|
+
tasks = fj(:actions_tasks)
|
|
108
115
|
Parsers.parse_actions_tasks(tasks)
|
|
109
116
|
.select { |task| task[:sha].start_with?(ref.to_s) || ref.to_s.start_with?(task[:sha]) }
|
|
110
117
|
.map do |task|
|
|
@@ -119,15 +126,19 @@ module Ace
|
|
|
119
126
|
end
|
|
120
127
|
|
|
121
128
|
# @return [ProviderRepository] normalized repository evidence
|
|
129
|
+
# @raise [ProviderIdentityMismatchError] when the CLI reports a
|
|
130
|
+
# repository other than the selected one
|
|
122
131
|
def repository
|
|
123
|
-
|
|
132
|
+
target = repository_target
|
|
133
|
+
view = fj(:repo_view)
|
|
124
134
|
parsed = Parsers.parse_repo_view(view)
|
|
135
|
+
verify_returned_repository!(parsed, target)
|
|
125
136
|
|
|
126
137
|
Ace::Git::ProviderRepository.new(
|
|
127
138
|
server_name: server.name,
|
|
128
139
|
full_name: parsed[:full_name],
|
|
129
140
|
default_branch: nil,
|
|
130
|
-
url: parsed[:url]
|
|
141
|
+
url: parsed[:url] || target.url
|
|
131
142
|
)
|
|
132
143
|
end
|
|
133
144
|
|
|
@@ -137,7 +148,7 @@ module Ace
|
|
|
137
148
|
# base/head identity, with head SHA provenance
|
|
138
149
|
def find_open_pull_requests(head_repository_url:, head_ref:, base_repository_url:, base_ref:)
|
|
139
150
|
require_base_on_server!(base_repository_url)
|
|
140
|
-
listing = fj(
|
|
151
|
+
listing = fj(:pr_search, "open")
|
|
141
152
|
Parsers.parse_search(listing).filter_map do |entry|
|
|
142
153
|
parsed = view_pr(entry[:number])
|
|
143
154
|
next unless parsed && parsed[:state] == :open
|
|
@@ -150,14 +161,14 @@ module Ace
|
|
|
150
161
|
|
|
151
162
|
# Create a PR via `fj pr create`, reconciling to an exact open match
|
|
152
163
|
# first and proving the resulting head against `expected_head`.
|
|
153
|
-
# `fj` cannot
|
|
154
|
-
# real draft state.
|
|
164
|
+
# `fj` cannot encode an explicit fork head; the receipt reports the
|
|
165
|
+
# provider's real draft state.
|
|
155
166
|
#
|
|
156
167
|
# @return [ProviderMutationReceipt] idempotency :created/:existing
|
|
157
168
|
def create_pull_request(head_ref:, head_repository_url:, base_ref:, expected_head:, title:, body: nil, draft: true)
|
|
158
|
-
# `fj pr create`
|
|
159
|
-
#
|
|
160
|
-
# lookups) beats creating the wrong PR.
|
|
169
|
+
# `fj pr create -r` binds the base repository, but no observed form
|
|
170
|
+
# encodes a cross-repository fork head; refusing before any command
|
|
171
|
+
# (even lookups) beats creating the wrong PR.
|
|
161
172
|
unless Ace::Git::Atoms::ServerUrl.match?(head_repository_url, server.url)
|
|
162
173
|
raise Ace::Git::ProviderUnsupportedCapabilityError,
|
|
163
174
|
"Forgejo CLI (fj) pr create cannot target head repository " \
|
|
@@ -178,9 +189,8 @@ module Ace
|
|
|
178
189
|
return receipt(:create, verify_expected_head!(matches.first, expected_head), :existing)
|
|
179
190
|
end
|
|
180
191
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
send_mutation(args, ambiguous: true, identity: identity_text(head_repository_url, head_ref, base_ref))
|
|
192
|
+
send_mutation(:pr_create, title, head_ref, base_ref, body,
|
|
193
|
+
ambiguous: true, identity: identity_text(head_repository_url, head_ref, base_ref))
|
|
184
194
|
|
|
185
195
|
created = reconcile_created(
|
|
186
196
|
head_repository_url: head_repository_url, head_ref: head_ref,
|
|
@@ -195,9 +205,10 @@ module Ace
|
|
|
195
205
|
#
|
|
196
206
|
# @return [ProviderMutationReceipt] operation :update
|
|
197
207
|
def update_pull_request(number:, expected_head:, title: nil, body: nil)
|
|
208
|
+
number = request_number!(number)
|
|
198
209
|
verify_expected_head!(fetch_pr(number), expected_head)
|
|
199
|
-
send_mutation(
|
|
200
|
-
send_mutation(
|
|
210
|
+
send_mutation(:pr_edit_title, number, title, ambiguous: false) if title
|
|
211
|
+
send_mutation(:pr_edit_body, number, body, ambiguous: false) if body
|
|
201
212
|
receipt(:update, fetch_pr(number), nil)
|
|
202
213
|
end
|
|
203
214
|
|
|
@@ -222,23 +233,39 @@ module Ace
|
|
|
222
233
|
|
|
223
234
|
private
|
|
224
235
|
|
|
225
|
-
#
|
|
226
|
-
|
|
227
|
-
|
|
236
|
+
# Validated selected repository identity. Built once; malformed
|
|
237
|
+
# selections fail as configuration errors before any subprocess.
|
|
238
|
+
def repository_target
|
|
239
|
+
@repository_target ||= RepositoryBinding::Target.resolve(server.url)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Refuse before any repository subprocess when the installed fj was
|
|
243
|
+
# never observed; a different version must not inherit capabilities.
|
|
244
|
+
def ensure_observed_version!
|
|
245
|
+
return if @version_checked
|
|
246
|
+
|
|
247
|
+
CliExecutor.check_version_supported!(runner: runner)
|
|
248
|
+
@version_checked = true
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Run one repository-scoped `fj` command bound to the selected
|
|
252
|
+
# target, classifying every failure with the taxonomy.
|
|
253
|
+
def fj(operation, *arguments)
|
|
254
|
+
result = execute_repository(operation, arguments)
|
|
228
255
|
return result[:stdout] if result[:success]
|
|
229
256
|
|
|
230
|
-
classify_failure(result[:stderr], context:
|
|
257
|
+
classify_failure(result[:stderr], context: operation_context(operation, arguments))
|
|
231
258
|
end
|
|
232
259
|
|
|
233
|
-
# Send one mutating
|
|
234
|
-
# any transport-level failure becomes an unknown outcome:
|
|
235
|
-
# may have mutated the forge, so callers must reconcile
|
|
236
|
-
# identity instead of retrying blindly.
|
|
237
|
-
def send_mutation(
|
|
238
|
-
result =
|
|
260
|
+
# Send one mutating repository operation. When `ambiguous` is true
|
|
261
|
+
# (create), any transport-level failure becomes an unknown outcome:
|
|
262
|
+
# the request may have mutated the forge, so callers must reconcile
|
|
263
|
+
# by exact identity instead of retrying blindly.
|
|
264
|
+
def send_mutation(operation, *arguments, ambiguous:, identity: nil)
|
|
265
|
+
result = execute_repository(operation, arguments)
|
|
239
266
|
return result if result[:success]
|
|
240
267
|
|
|
241
|
-
classify_failure(result[:stderr], context:
|
|
268
|
+
classify_failure(result[:stderr], context: operation_context(operation, arguments))
|
|
242
269
|
rescue Ace::Git::ProviderUnreachableError => e
|
|
243
270
|
raise e unless ambiguous
|
|
244
271
|
|
|
@@ -247,12 +274,36 @@ module Ace
|
|
|
247
274
|
"reconcile by exact identity before repeating: #{identity}"
|
|
248
275
|
end
|
|
249
276
|
|
|
277
|
+
def execute_repository(operation, arguments)
|
|
278
|
+
target = repository_target
|
|
279
|
+
ensure_observed_version!
|
|
280
|
+
ensure_not_redirecting!(target)
|
|
281
|
+
CliExecutor.execute_repository(
|
|
282
|
+
target: target, operation: operation,
|
|
283
|
+
arguments: arguments, timeout: timeout, runner: runner
|
|
284
|
+
)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Refuse before any subprocess when the readable fj configuration
|
|
288
|
+
# aliases the selected host to a different endpoint: `-H` cannot
|
|
289
|
+
# prevent that redirect, and returned-identity checks cannot see it.
|
|
290
|
+
def ensure_not_redirecting!(target)
|
|
291
|
+
return if @redirect_checked
|
|
292
|
+
|
|
293
|
+
RepositoryBinding.reject_selected_host_redirect!(target.host_url)
|
|
294
|
+
@redirect_checked = true
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def operation_context(operation, arguments)
|
|
298
|
+
"#{operation} #{arguments.compact.join(" ")}".strip
|
|
299
|
+
end
|
|
300
|
+
|
|
250
301
|
def classify_failure(message, context:)
|
|
251
302
|
message = message.to_s
|
|
252
303
|
case message
|
|
253
304
|
when /not found|does not exist|no pull request|no issue/i
|
|
254
305
|
raise Ace::Git::ProviderObjectNotFoundError, "Object not found (#{context}): #{message}"
|
|
255
|
-
when /access denied|unauthorized|token/i
|
|
306
|
+
when /access denied|unauthorized|token|only signed in|not logged in|log ?in required/i
|
|
256
307
|
raise Ace::Git::ProviderAuthenticationError, "Not authenticated with Forgejo: #{message}"
|
|
257
308
|
else
|
|
258
309
|
raise Ace::Git::ProviderUnreachableError, "Forgejo request failed (#{context}): #{message}"
|
|
@@ -270,12 +321,17 @@ module Ace
|
|
|
270
321
|
normalize_pr(parsed)
|
|
271
322
|
end
|
|
272
323
|
|
|
273
|
-
# View one PR and parse its stable minimal-style output.
|
|
324
|
+
# View one PR and parse its stable minimal-style output. The
|
|
325
|
+
# qualified `owner/repo#N` reference binds the read to the selected
|
|
326
|
+
# repository; a returned number that disagrees with the request is a
|
|
327
|
+
# routed mismatch and fails closed.
|
|
274
328
|
def view_pr(number)
|
|
275
|
-
view = fj(
|
|
276
|
-
Parsers.parse_pr_view(view) ||
|
|
329
|
+
view = fj(:pr_view, number)
|
|
330
|
+
parsed = Parsers.parse_pr_view(view) ||
|
|
277
331
|
raise(Ace::Git::ProviderMalformedOutputError,
|
|
278
332
|
"Unrecognized `fj pr view #{number}` output; update the Forgejo provider parser")
|
|
333
|
+
verify_returned_number!(parsed, number, "pull request")
|
|
334
|
+
parsed
|
|
279
335
|
end
|
|
280
336
|
|
|
281
337
|
# Re-fetch and verify the PR created by an accepted create request.
|
|
@@ -319,7 +375,7 @@ module Ace
|
|
|
319
375
|
end
|
|
320
376
|
|
|
321
377
|
def head_sha_of(number)
|
|
322
|
-
commits = fj(
|
|
378
|
+
commits = fj(:pr_commits, number)
|
|
323
379
|
Parsers.parse_head_sha(commits)
|
|
324
380
|
end
|
|
325
381
|
|
|
@@ -337,7 +393,10 @@ module Ace
|
|
|
337
393
|
draft: nil,
|
|
338
394
|
merged_at: nil,
|
|
339
395
|
head_repository_url: head_repository_url_of(parsed),
|
|
340
|
-
base_repository_url:
|
|
396
|
+
base_repository_url: repository_target.url,
|
|
397
|
+
# fj v0.6.0 exposes no authoritative merge-commit field for a
|
|
398
|
+
# selected PR (observed; see the task capability evidence), so
|
|
399
|
+
# merge evidence stays empty and cleanup retains the checkout.
|
|
341
400
|
merge_commit_sha: nil
|
|
342
401
|
)
|
|
343
402
|
end
|
|
@@ -347,14 +406,14 @@ module Ace
|
|
|
347
406
|
# base repository; canonical PRs use the configured repository.
|
|
348
407
|
def head_repository_url_of(parsed)
|
|
349
408
|
repo = parsed[:head_repository]
|
|
350
|
-
return
|
|
409
|
+
return repository_target.url if repo.nil? || repo.empty?
|
|
351
410
|
|
|
352
411
|
"#{server_host_root}/#{repo}"
|
|
353
412
|
end
|
|
354
413
|
|
|
355
414
|
def server_host_root
|
|
356
415
|
@server_host_root ||= begin
|
|
357
|
-
uri = URI.parse(
|
|
416
|
+
uri = URI.parse(repository_target.url)
|
|
358
417
|
"#{uri.scheme || "https"}://#{uri.host}#{":#{uri.port}" if uri.port && uri.port != uri.default_port}"
|
|
359
418
|
end
|
|
360
419
|
end
|
|
@@ -373,27 +432,58 @@ module Ace
|
|
|
373
432
|
draft: nil,
|
|
374
433
|
merged_at: nil,
|
|
375
434
|
head_repository_url: nil,
|
|
376
|
-
base_repository_url:
|
|
435
|
+
base_repository_url: repository_target.url,
|
|
377
436
|
merge_commit_sha: nil
|
|
378
437
|
)
|
|
379
438
|
end
|
|
380
439
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
440
|
+
# Reject non-numeric or non-positive PR/issue numbers as
|
|
441
|
+
# configuration failures before any subprocess.
|
|
442
|
+
def request_number!(number)
|
|
443
|
+
text = number.to_s
|
|
444
|
+
unless /\A\d+\z/.match?(text) && text.to_i.positive?
|
|
445
|
+
raise Ace::Git::ConfigError,
|
|
446
|
+
"Pull request and issue numbers must be positive integers, got #{number.inspect}"
|
|
387
447
|
end
|
|
448
|
+
|
|
449
|
+
text.to_i
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# PR and issue numbers are not globally unique: evidence whose
|
|
453
|
+
# number disagrees with the selected reference was not routed to the
|
|
454
|
+
# selected repository and must never be relabelled.
|
|
455
|
+
def verify_returned_number!(parsed, requested, kind)
|
|
456
|
+
return if parsed[:number] == requested.to_i
|
|
457
|
+
|
|
458
|
+
raise Ace::Git::ProviderIdentityMismatchError,
|
|
459
|
+
"Forgejo returned #{kind} ##{parsed[:number]} for selected #{kind} " \
|
|
460
|
+
"##{requested} on #{repository_target.repo}; refusing misrouted evidence"
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# `fj repo view` was bound to the selected owner/repository, so
|
|
464
|
+
# returned identity fields must agree with the selection.
|
|
465
|
+
def verify_returned_repository!(parsed, target)
|
|
466
|
+
full_name = parsed[:full_name].to_s.sub(%r{\.git\z}i, "")
|
|
467
|
+
unless !full_name.empty? && full_name.casecmp(target.repo).zero?
|
|
468
|
+
raise Ace::Git::ProviderIdentityMismatchError,
|
|
469
|
+
"Forgejo repo view returned #{full_name.inspect} for selected repository " \
|
|
470
|
+
"#{target.repo}; refusing misrouted evidence"
|
|
471
|
+
end
|
|
472
|
+
return if parsed[:url].nil? || parsed[:url].empty? ||
|
|
473
|
+
Ace::Git::Atoms::ServerUrl.match?(parsed[:url], target.url)
|
|
474
|
+
|
|
475
|
+
raise Ace::Git::ProviderIdentityMismatchError,
|
|
476
|
+
"Forgejo repo view returned URL #{parsed[:url].inspect} for selected repository " \
|
|
477
|
+
"#{target.url}; refusing misrouted evidence"
|
|
388
478
|
end
|
|
389
479
|
|
|
390
480
|
def pr_url(number)
|
|
391
|
-
base =
|
|
481
|
+
base = repository_target.url.to_s.chomp("/")
|
|
392
482
|
"#{base}/pulls/#{number}"
|
|
393
483
|
end
|
|
394
484
|
|
|
395
485
|
def issue_url(number)
|
|
396
|
-
base =
|
|
486
|
+
base = repository_target.url.to_s.chomp("/")
|
|
397
487
|
"#{base}/issues/#{number}"
|
|
398
488
|
end
|
|
399
489
|
end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module Git
|
|
8
|
+
module Forgejo
|
|
9
|
+
# Selected-server identity binding for `fj` repository commands.
|
|
10
|
+
#
|
|
11
|
+
# Forgejo CLI v0.6.0 exposes two explicit-target mechanisms and no
|
|
12
|
+
# others (see the task evidence file): `-H HOST` plus either
|
|
13
|
+
# `-r OWNER/REPO` (search, create, issue/actions repo selection) or a
|
|
14
|
+
# qualified `OWNER/REPO#N` object id (view/edit/status family). A
|
|
15
|
+
# repository command may only launch through {RepositoryBinding.argv_for},
|
|
16
|
+
# which exists for operations whose argv form was observed on a real
|
|
17
|
+
# binary; everything else refuses before any subprocess.
|
|
18
|
+
module RepositoryBinding
|
|
19
|
+
# forgejo-cli versions whose help/output this table was observed on.
|
|
20
|
+
# A different installed version must not inherit these capabilities;
|
|
21
|
+
# the executor refuses it until the table is re-observed and extended.
|
|
22
|
+
OBSERVED_VERSIONS = ["v0.6.0"].freeze
|
|
23
|
+
|
|
24
|
+
# The validated selected repository. Built once from the resolved
|
|
25
|
+
# server URL; malformed selections fail with ConfigError before any
|
|
26
|
+
# subprocess launch.
|
|
27
|
+
class Target
|
|
28
|
+
attr_reader :host, :authority, :host_url, :repo, :url
|
|
29
|
+
|
|
30
|
+
SUPPORTED_SCHEMES = ["http", "https"].freeze
|
|
31
|
+
|
|
32
|
+
# @param server_url [String] resolved server URL (https://host/owner/repo)
|
|
33
|
+
# @raise [Ace::Git::ConfigError] when host, scheme, or owner/repository is missing
|
|
34
|
+
def self.resolve(server_url)
|
|
35
|
+
uri = URI.parse(server_url.to_s)
|
|
36
|
+
raise_config_error!(server_url, "missing host") if uri.host.nil? || uri.host.empty?
|
|
37
|
+
unless SUPPORTED_SCHEMES.include?(uri.scheme.to_s.downcase)
|
|
38
|
+
raise_config_error!(server_url, "scheme must be http or https")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
authority = if uri.port && uri.port != uri.default_port
|
|
42
|
+
"#{uri.host}:#{uri.port}"
|
|
43
|
+
else
|
|
44
|
+
uri.host
|
|
45
|
+
end
|
|
46
|
+
path = uri.path.to_s.sub(%r{/+\z}, "").sub(/\.git\z/i, "")
|
|
47
|
+
segments = path.split("/").reject(&:empty?)
|
|
48
|
+
unless segments.length == 2
|
|
49
|
+
raise_config_error!(server_url, "path must be exactly owner/repository")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
repo = "#{segments[0]}/#{segments[1]}"
|
|
53
|
+
# `fj -H` accepts a full URL and otherwise assumes HTTPS (observed
|
|
54
|
+
# v0.6.0); passing scheme + authority preserves the configured
|
|
55
|
+
# endpoint exactly.
|
|
56
|
+
host_url = "#{uri.scheme.downcase}://#{authority}"
|
|
57
|
+
new(
|
|
58
|
+
host: uri.host, authority: authority, host_url: host_url,
|
|
59
|
+
repo: repo, url: "#{host_url}/#{repo}"
|
|
60
|
+
)
|
|
61
|
+
rescue URI::Error
|
|
62
|
+
raise_config_error!(server_url, "unparseable URL")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.raise_config_error!(server_url, reason)
|
|
66
|
+
raise Ace::Git::ConfigError,
|
|
67
|
+
"Selected Forgejo server URL #{server_url.inspect} is unusable (#{reason}); " \
|
|
68
|
+
"configure the server as scheme://host/owner/repository"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def initialize(host:, authority:, host_url:, repo:, url:)
|
|
72
|
+
@host = host
|
|
73
|
+
@authority = authority
|
|
74
|
+
@host_url = host_url
|
|
75
|
+
@repo = repo
|
|
76
|
+
@url = url
|
|
77
|
+
end
|
|
78
|
+
private_class_method :new
|
|
79
|
+
|
|
80
|
+
# Qualified object id form accepted by `fj pr/issue view|edit` (v0.6.0).
|
|
81
|
+
def qualified_ref(number)
|
|
82
|
+
"#{repo}##{number}"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Capability table: repository operations with an argv form observed
|
|
87
|
+
# on real forgejo-cli v0.6.0 (task evidence file). Forms receive the
|
|
88
|
+
# validated Target plus the provider's per-call arguments and return
|
|
89
|
+
# the argv after the binary name; the executor stamps `-H host`.
|
|
90
|
+
FORMS = {
|
|
91
|
+
pr_view: ->(target, number) do
|
|
92
|
+
["--style", "minimal", "pr", "view", target.qualified_ref(number)]
|
|
93
|
+
end,
|
|
94
|
+
pr_commits: ->(target, number) do
|
|
95
|
+
["--style", "minimal", "pr", "view", target.qualified_ref(number), "commits"]
|
|
96
|
+
end,
|
|
97
|
+
pr_diff: ->(target, number) do
|
|
98
|
+
["pr", "view", target.qualified_ref(number), "diff"]
|
|
99
|
+
end,
|
|
100
|
+
pr_search: ->(target, state) do
|
|
101
|
+
["--style", "minimal", "pr", "search", "--state", state, "-r", target.repo]
|
|
102
|
+
end,
|
|
103
|
+
pr_create: ->(target, title, head_ref, base_ref, body) do
|
|
104
|
+
argv = ["pr", "create", title, "--head", head_ref, "--base", base_ref]
|
|
105
|
+
argv += ["--body", body] if body
|
|
106
|
+
argv + ["-r", target.repo]
|
|
107
|
+
end,
|
|
108
|
+
pr_edit_title: ->(target, number, title) do
|
|
109
|
+
["pr", "edit", target.qualified_ref(number), "title", title]
|
|
110
|
+
end,
|
|
111
|
+
pr_edit_body: ->(target, number, body) do
|
|
112
|
+
["pr", "edit", target.qualified_ref(number), "body", body]
|
|
113
|
+
end,
|
|
114
|
+
issue_view: ->(target, number) do
|
|
115
|
+
["--style", "minimal", "issue", "view", target.qualified_ref(number)]
|
|
116
|
+
end,
|
|
117
|
+
actions_tasks: ->(target) do
|
|
118
|
+
["--style", "minimal", "actions", "tasks", "-r", target.repo]
|
|
119
|
+
end,
|
|
120
|
+
repo_view: ->(target) do
|
|
121
|
+
["--style", "minimal", "repo", "view", target.repo]
|
|
122
|
+
end
|
|
123
|
+
}.freeze
|
|
124
|
+
|
|
125
|
+
class << self
|
|
126
|
+
# Resolve one repository operation to its explicitly bound argv.
|
|
127
|
+
#
|
|
128
|
+
# @param operation [Symbol] key in {FORMS}
|
|
129
|
+
# @param target [Target] validated selected repository
|
|
130
|
+
# @param arguments [Array] per-operation arguments
|
|
131
|
+
# @raise [Ace::Git::ProviderUnsupportedCapabilityError] when the
|
|
132
|
+
# operation has no observed form
|
|
133
|
+
def argv_for(operation, target, arguments)
|
|
134
|
+
form = FORMS.fetch(operation) do
|
|
135
|
+
raise Ace::Git::ProviderUnsupportedCapabilityError,
|
|
136
|
+
"forgejo-cli #{OBSERVED_VERSIONS.join("/")} exposes no repository-bound " \
|
|
137
|
+
"argv form for #{operation.inspect}; refusing to run it against #{target.repo}"
|
|
138
|
+
end
|
|
139
|
+
form.call(target, *arguments)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Refuse to launch repository commands when the readable fj keys
|
|
143
|
+
# file contains an alias that redirects the selected host. fj
|
|
144
|
+
# v0.6.0 applies its `aliases` map to `-H` values (observed: an
|
|
145
|
+
# alias silently rewrites the endpoint), and no CLI command prints
|
|
146
|
+
# aliases, so ACE verifies the one configuration file it must
|
|
147
|
+
# agree with — read-only, never modified. An absent or unreadable
|
|
148
|
+
# keys file (other fj versions, moved storage) cannot be verified
|
|
149
|
+
# and is allowed; misdirected runtime requests still classify
|
|
150
|
+
# through the shared taxonomy.
|
|
151
|
+
#
|
|
152
|
+
# @param host_url [String] selected scheme://authority
|
|
153
|
+
# @param config_path [String, nil] explicit keys-file path (tests)
|
|
154
|
+
# @raise [Ace::Git::ConfigError] on a conflicting redirect
|
|
155
|
+
def reject_selected_host_redirect!(host_url, config_path: nil)
|
|
156
|
+
path = config_path || default_keys_path
|
|
157
|
+
return unless path && File.exist?(path)
|
|
158
|
+
|
|
159
|
+
data = JSON.parse(File.read(path))
|
|
160
|
+
aliases = data.is_a?(Hash) ? data["aliases"] : nil
|
|
161
|
+
return unless aliases.is_a?(Hash)
|
|
162
|
+
|
|
163
|
+
selected = strip_scheme(host_url)
|
|
164
|
+
redirect = aliases[selected] || aliases[host_url.to_s]
|
|
165
|
+
return if redirect.nil? || redirect.to_s.empty?
|
|
166
|
+
|
|
167
|
+
redirect_authority = strip_scheme(redirect)
|
|
168
|
+
return if redirect_authority.casecmp(selected).zero?
|
|
169
|
+
|
|
170
|
+
raise Ace::Git::ConfigError,
|
|
171
|
+
"fj configuration redirects selected host #{selected} to #{redirect} " \
|
|
172
|
+
"(#{path}); remove or fix the conflicting fj alias — ACE will not " \
|
|
173
|
+
"operate on a redirected endpoint"
|
|
174
|
+
rescue JSON::ParserError, IOError, SystemCallError
|
|
175
|
+
nil
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Observed forgejo-cli v0.6.0 keys-file locations (data_dir of the
|
|
179
|
+
# `directories` crate, plus the legacy Cyborus org path).
|
|
180
|
+
def default_keys_path
|
|
181
|
+
candidates = []
|
|
182
|
+
xdg = ENV["XDG_DATA_HOME"]
|
|
183
|
+
candidates << File.join(xdg, "forgejo-cli", "keys.json") if xdg && !xdg.empty?
|
|
184
|
+
home = Dir.home rescue nil
|
|
185
|
+
if home
|
|
186
|
+
candidates << File.join(home, ".local", "share", "forgejo-cli", "keys.json")
|
|
187
|
+
candidates << File.join(home, "Library", "Application Support", "forgejo-cli", "keys.json")
|
|
188
|
+
candidates << File.join(home, ".local", "share", "Cyborus", "forgejo-cli", "keys.json")
|
|
189
|
+
candidates << File.join(home, "Library", "Application Support", "Cyborus", "forgejo-cli", "keys.json")
|
|
190
|
+
end
|
|
191
|
+
candidates.find { |path| File.exist?(path) }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
private
|
|
195
|
+
|
|
196
|
+
def strip_scheme(url)
|
|
197
|
+
url.to_s.sub(%r{\A[a-z][a-z0-9+.\-]*://}i, "").chomp("/")
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
data/lib/ace/git/forgejo.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-git-forgejo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-10-01 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-git
|
|
@@ -42,6 +42,7 @@ files:
|
|
|
42
42
|
- lib/ace/git/forgejo/parsers.rb
|
|
43
43
|
- lib/ace/git/forgejo/pr_identifier.rb
|
|
44
44
|
- lib/ace/git/forgejo/provider.rb
|
|
45
|
+
- lib/ace/git/forgejo/repository_binding.rb
|
|
45
46
|
- lib/ace/git/forgejo/version.rb
|
|
46
47
|
homepage: https://forgejo.tail6c0887.ts.net/cs3b/ace
|
|
47
48
|
licenses:
|