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.
@@ -0,0 +1,206 @@
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
+ # macOS app-support uses reverse-DNS bundle directories.
189
+ candidates << File.join(home, "Library", "Application Support", "forgejo-cli.forgejo-cli", "keys.json")
190
+ candidates << File.join(home, ".local", "share", "Cyborus", "forgejo-cli", "keys.json")
191
+ candidates << File.join(home, "Library", "Application Support", "Cyborus.forgejo-cli", "keys.json")
192
+ candidates << File.join(home, "Library", "Application Support", "Cyborus", "forgejo-cli", "keys.json")
193
+ end
194
+ candidates.find { |path| File.exist?(path) }
195
+ end
196
+
197
+ private
198
+
199
+ def strip_scheme(url)
200
+ url.to_s.sub(%r{\A[a-z][a-z0-9+.\-]*://}i, "").chomp("/")
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
@@ -3,7 +3,7 @@
3
3
  module Ace
4
4
  module Git
5
5
  module Forgejo
6
- VERSION = "0.2.0"
6
+ VERSION = "0.4.0"
7
7
  end
8
8
  end
9
9
  end
@@ -3,9 +3,11 @@
3
3
  require "ace/git"
4
4
 
5
5
  require_relative "forgejo/version"
6
+ require_relative "forgejo/repository_binding"
6
7
  require_relative "forgejo/cli_executor"
7
8
  require_relative "forgejo/pr_identifier"
8
9
  require_relative "forgejo/parsers"
10
+ require_relative "forgejo/http_client"
9
11
  require_relative "forgejo/provider"
10
12
 
11
13
  module Ace
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.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-09-29 00:00:00.000000000 Z
10
+ date: 2026-10-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ace-git
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '0.24'
18
+ version: '0.26'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '0.24'
25
+ version: '0.26'
26
26
  description: 'Implements the shared ace-git provider contract for Forgejo: all `fj`
27
27
  CLI invocation, output parsing, authentication verification, and classified failure
28
28
  handling live behind one normalized evidence boundary. Ad-hoc curl fallbacks are
@@ -39,9 +39,11 @@ files:
39
39
  - Rakefile
40
40
  - lib/ace/git/forgejo.rb
41
41
  - lib/ace/git/forgejo/cli_executor.rb
42
+ - lib/ace/git/forgejo/http_client.rb
42
43
  - lib/ace/git/forgejo/parsers.rb
43
44
  - lib/ace/git/forgejo/pr_identifier.rb
44
45
  - lib/ace/git/forgejo/provider.rb
46
+ - lib/ace/git/forgejo/repository_binding.rb
45
47
  - lib/ace/git/forgejo/version.rb
46
48
  homepage: https://forgejo.tail6c0887.ts.net/cs3b/ace
47
49
  licenses: