ace-git-forgejo 0.2.0 → 0.4.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 +46 -0
- data/README.md +35 -2
- data/lib/ace/git/forgejo/cli_executor.rb +112 -31
- data/lib/ace/git/forgejo/http_client.rb +111 -0
- data/lib/ace/git/forgejo/parsers.rb +23 -4
- data/lib/ace/git/forgejo/provider.rb +451 -56
- data/lib/ace/git/forgejo/repository_binding.rb +206 -0
- data/lib/ace/git/forgejo/version.rb +1 -1
- data/lib/ace/git/forgejo.rb +2 -0
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e1e1e605a8808db94b0b68ad90d445149f8fce01df65f7ddad401f59a0fce255
|
|
4
|
+
data.tar.gz: 312685879a60d54363626b6f7dc98d2954b0575120aeb0dde0e128826252fba6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 85a2d43f44c7d07d7c60c236f9a5c81c350233fca510c4a6503181f42ab2052a50e31094380e1a2ac752a55bb3ecdaf6923376a439197c459c1a71a66ed21364
|
|
7
|
+
data.tar.gz: ddfee8db45e1545dfb7592e77396255a71d90e083c455a1507ee412da71482fee2a499c098c47ed59cbcc0b6abe46952cea8faa5b151d97b2521106c5e2f6bc8
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-10-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Repository-bound Forgejo PR comment and review retrieval and comment create/update through the shared provider contract.
|
|
14
|
+
- Correlation-based reconciliation after uncertain posts and explicit unsupported thread resolution.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## [0.3.0] - 2026-09-29
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- Selected-repository binding for every `fj` repository command: the
|
|
21
|
+
resolved server's host/owner/repository is validated once (malformed
|
|
22
|
+
selections raise `ConfigError` before any subprocess) and stamped onto
|
|
23
|
+
every call via `-H` plus qualified `owner/repo#N` ids or `-r owner/repo`,
|
|
24
|
+
so a named server selection can never be silently retargeted by cwd,
|
|
25
|
+
remotes, default login, or another checkout.
|
|
26
|
+
- Capability table grounded in observed forgejo-cli v0.6.0 help/output
|
|
27
|
+
(upstream release binary, plus real-server read-only probes); operations
|
|
28
|
+
without an observed argv form and installed versions outside the observed
|
|
29
|
+
set refuse with `ProviderUnsupportedCapabilityError` before launch.
|
|
30
|
+
- Returned-identity validation: PR/issue numbers and repository view
|
|
31
|
+
full-name/URL must agree with the selection or evidence fails closed
|
|
32
|
+
with `ProviderIdentityMismatchError`.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- All `fj` subprocess launches now go through a two-path executor boundary:
|
|
36
|
+
an allowlisted control path (`fj version`, `fj auth list`) and the
|
|
37
|
+
repository boundary. Authentication compares `fj auth list` lines exactly
|
|
38
|
+
against the selected authority (substring hosts no longer count) and
|
|
39
|
+
scans both output streams (observed v0.6.0 prints "No logins." on stderr
|
|
40
|
+
with exit 0).
|
|
41
|
+
- Unauthenticated-API failures are classified as authentication errors
|
|
42
|
+
(observed Lab text "Only signed in user is allowed to call APIs.").
|
|
43
|
+
- `merge_commit_sha` stays empty: observed fj v0.6.0 exposes no
|
|
44
|
+
authoritative merge-commit field for a selected PR, so cleanup evidence
|
|
45
|
+
never claims one.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
- Selected-host fidelity: `-H` now carries the selected `scheme://authority`
|
|
49
|
+
(fj assumes HTTPS for a bare host, so an http authority was silently
|
|
50
|
+
upgraded), non-http(s) server schemes are rejected as configuration
|
|
51
|
+
errors before any subprocess, and the provider refuses to launch
|
|
52
|
+
repository commands when the readable fj keys file aliases the selected
|
|
53
|
+
host to a different endpoint (read-only check; absent/unreadable keys
|
|
54
|
+
file is allowed, never modified).
|
|
55
|
+
|
|
10
56
|
## [0.2.0] - 2026-09-28
|
|
11
57
|
|
|
12
58
|
### 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
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "openssl"
|
|
7
|
+
require "uri"
|
|
8
|
+
require_relative "repository_binding"
|
|
9
|
+
|
|
10
|
+
module Ace
|
|
11
|
+
module Git
|
|
12
|
+
module Forgejo
|
|
13
|
+
# Authenticated, repository-bound JSON transport for Forgejo API
|
|
14
|
+
# capabilities whose fj v0.6 output omits stable object IDs.
|
|
15
|
+
class HttpClient
|
|
16
|
+
def initialize(server:, timeout:, runner: nil)
|
|
17
|
+
@target = RepositoryBinding::Target.resolve(server.url)
|
|
18
|
+
@timeout = timeout
|
|
19
|
+
@runner = runner
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def paginate(suffix)
|
|
23
|
+
page = 1
|
|
24
|
+
entries = []
|
|
25
|
+
seen_pages = {}
|
|
26
|
+
loop do
|
|
27
|
+
separator = suffix.include?("?") ? "&" : "?"
|
|
28
|
+
batch = request(:get, "#{suffix}#{separator}page=#{page}&limit=50")
|
|
29
|
+
unless batch.is_a?(Array)
|
|
30
|
+
raise Ace::Git::ProviderMalformedOutputError, "Forgejo #{suffix} did not return a list"
|
|
31
|
+
end
|
|
32
|
+
entries.concat(batch)
|
|
33
|
+
# A server may cap pages below the requested limit, so a short
|
|
34
|
+
# page is not proof of the end; stop only on an empty page and
|
|
35
|
+
# guard against servers repeating the same page forever.
|
|
36
|
+
break if batch.empty?
|
|
37
|
+
digest = Digest::SHA256.hexdigest(batch.to_s)
|
|
38
|
+
raise Ace::Git::ProviderMalformedOutputError,
|
|
39
|
+
"Forgejo #{suffix} pagination repeated page #{page}" if seen_pages[digest]
|
|
40
|
+
|
|
41
|
+
seen_pages[digest] = true
|
|
42
|
+
page += 1
|
|
43
|
+
end
|
|
44
|
+
entries
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def request(method, suffix, body: nil)
|
|
48
|
+
RepositoryBinding.reject_selected_host_redirect!(@target.host_url)
|
|
49
|
+
path = "/api/v1/repos/#{@target.repo}/#{suffix}"
|
|
50
|
+
if @runner
|
|
51
|
+
result = @runner.call(args: ["forgejo-http", method.to_s.upcase, "#{@target.host_url}#{path}", body],
|
|
52
|
+
timeout: @timeout, env: {})
|
|
53
|
+
status = result[:status] || (result[:success] ? 200 : 503)
|
|
54
|
+
payload = result[:stdout].to_s
|
|
55
|
+
else
|
|
56
|
+
uri = URI.parse("#{@target.host_url}#{path}")
|
|
57
|
+
klass = {get: Net::HTTP::Get, post: Net::HTTP::Post, patch: Net::HTTP::Patch}.fetch(method)
|
|
58
|
+
req = klass.new(uri)
|
|
59
|
+
req["Authorization"] = "token #{access_token!}"
|
|
60
|
+
req["Accept"] = "application/json"
|
|
61
|
+
req["Content-Type"] = "application/json" if body
|
|
62
|
+
req.body = JSON.generate(body) if body
|
|
63
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
|
|
64
|
+
open_timeout: @timeout, read_timeout: @timeout) { |http| http.request(req) }
|
|
65
|
+
status = response.code.to_i
|
|
66
|
+
payload = response.body.to_s
|
|
67
|
+
end
|
|
68
|
+
classify_status!(status, method, path)
|
|
69
|
+
return nil if payload.empty?
|
|
70
|
+
|
|
71
|
+
JSON.parse(payload)
|
|
72
|
+
rescue JSON::ParserError => e
|
|
73
|
+
raise Ace::Git::ProviderMalformedOutputError, "Malformed Forgejo response: #{e.message}"
|
|
74
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, IOError, SocketError, OpenSSL::SSL::SSLError, SystemCallError => e
|
|
75
|
+
error = (method == :get) ? Ace::Git::ProviderUnreachableError : Ace::Git::ProviderUnknownOutcomeError
|
|
76
|
+
raise error, "Forgejo #{method} #{path} outcome unknown: #{e.class}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def access_token!
|
|
82
|
+
path = RepositoryBinding.default_keys_path
|
|
83
|
+
data = path && JSON.parse(File.read(path))
|
|
84
|
+
# fj stores credentials under the bare host (or host:port)
|
|
85
|
+
# authority, never the scheme-prefixed URL.
|
|
86
|
+
login = data.is_a?(Hash) && data.fetch("hosts", {})[@target.authority]
|
|
87
|
+
token = login.is_a?(Hash) && login["token"]
|
|
88
|
+
return token if token.is_a?(String) && !token.empty?
|
|
89
|
+
|
|
90
|
+
raise Ace::Git::ProviderAuthenticationError,
|
|
91
|
+
"No fj token for selected host #{@target.authority}; authenticate with fj first"
|
|
92
|
+
rescue Errno::ENOENT, Errno::EACCES, JSON::ParserError
|
|
93
|
+
raise Ace::Git::ProviderAuthenticationError,
|
|
94
|
+
"Cannot read fj authentication for selected host #{@target.authority}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def classify_status!(status, method, path)
|
|
98
|
+
return if (200..299).cover?(status.to_i)
|
|
99
|
+
|
|
100
|
+
error = case status.to_i
|
|
101
|
+
when 401, 403 then Ace::Git::ProviderAuthenticationError
|
|
102
|
+
when 404 then Ace::Git::ProviderObjectNotFoundError
|
|
103
|
+
when 409, 422 then Ace::Git::ProviderIdentityMismatchError
|
|
104
|
+
else (method == :get) ? Ace::Git::ProviderUnreachableError : Ace::Git::ProviderUnknownOutcomeError
|
|
105
|
+
end
|
|
106
|
+
raise error, "Forgejo #{method} #{path} returned HTTP #{status}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -116,18 +116,37 @@ module Ace
|
|
|
116
116
|
#
|
|
117
117
|
# @return [Array<Hash>] {name:, state:, sha:}
|
|
118
118
|
def self.parse_actions_tasks(text)
|
|
119
|
-
|
|
119
|
+
tasks = []
|
|
120
|
+
declared_count = nil
|
|
121
|
+
text.to_s.lines.map { |line| clean(line) }.reject(&:empty?).each do |line|
|
|
122
|
+
# `fj` emits a leading "<n> tasks" count header before task lines.
|
|
123
|
+
if (header = line.match(/\A(\d+) tasks?\z/i))
|
|
124
|
+
declared_count = header[1].to_i
|
|
125
|
+
next
|
|
126
|
+
end
|
|
127
|
+
|
|
120
128
|
match = line.match(
|
|
121
|
-
/\A#(?<task>\d+)\s+\((?<sha>[0-9a-f]+)\)\s+(?<state>\w+)\s+(?<name>.+?)\s+[\dhms.]+\s+\((
|
|
129
|
+
/\A#(?<task>\d+)\s+\((?<sha>[0-9a-f]+)\)\s+(?<state>\w+)\s+(?<name>.+?)\s+[\dhms.]+\s+\((?<event>[^)]+)\)/
|
|
122
130
|
)
|
|
123
|
-
|
|
131
|
+
# Any other nonempty line that cannot be parsed must not silently
|
|
132
|
+
# shrink check evidence; fail closed instead.
|
|
133
|
+
raise Ace::Git::ProviderMalformedOutputError,
|
|
134
|
+
"Unrecognized `fj actions tasks` output line: #{line[0, 80]}" unless match
|
|
124
135
|
|
|
125
|
-
{
|
|
136
|
+
tasks << {
|
|
126
137
|
name: match[:name],
|
|
127
138
|
state: match[:state].downcase.to_sym,
|
|
128
139
|
sha: match[:sha]
|
|
129
140
|
}
|
|
130
141
|
end
|
|
142
|
+
# A declared count that disagrees with the parsed rows means
|
|
143
|
+
# truncated output; fail closed instead of accepting partial
|
|
144
|
+
# check evidence.
|
|
145
|
+
if declared_count && declared_count != tasks.length
|
|
146
|
+
raise Ace::Git::ProviderMalformedOutputError,
|
|
147
|
+
"Incomplete `fj actions tasks` output: declared #{declared_count}, parsed #{tasks.length}"
|
|
148
|
+
end
|
|
149
|
+
tasks
|
|
131
150
|
end
|
|
132
151
|
|
|
133
152
|
# Parse `fj repo view` output.
|