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
|
@@ -7,110 +7,185 @@ module Ace
|
|
|
7
7
|
module Git
|
|
8
8
|
module CLI
|
|
9
9
|
module Commands
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
# Forge-neutral pull request lifecycle commands.
|
|
11
|
+
#
|
|
12
|
+
# Every subcommand accepts `--server NAME` / `--default-server`
|
|
13
|
+
# (mutually exclusive; remote resolution when neither is given) and
|
|
14
|
+
# `--format json`. Success output carries the resolved server,
|
|
15
|
+
# provider, and exact PR identity; failures exit nonzero with the
|
|
16
|
+
# classified provider error and never claim success.
|
|
17
|
+
module Pr
|
|
18
|
+
# Shared plumbing for all pr subcommands.
|
|
19
|
+
class BaseCommand < Ace::Support::Cli::Command
|
|
20
|
+
include Ace::Support::Cli::Base
|
|
21
|
+
|
|
22
|
+
option :server, type: :string, desc: "Explicit configured forge server name"
|
|
23
|
+
option :default_server, type: :boolean, desc: "Use the configured default forge server"
|
|
24
|
+
option :remote, type: :string, desc: "Git remote used for server resolution when nothing is selected"
|
|
25
|
+
option :format, type: :string, default: "text", desc: "Output format: text, json"
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def lifecycle(options)
|
|
30
|
+
Organisms::PullRequestLifecycle.new(
|
|
31
|
+
server_name: options[:server],
|
|
32
|
+
use_default: options[:default_server] == true,
|
|
33
|
+
remote_name: options[:remote]
|
|
34
|
+
)
|
|
35
|
+
end
|
|
22
36
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
37
|
+
def render_evidence(options, header: nil, **fields)
|
|
38
|
+
if options[:format] == "json"
|
|
39
|
+
puts JSON.pretty_generate(fields)
|
|
40
|
+
else
|
|
41
|
+
puts header if header
|
|
42
|
+
fields.each do |label, value|
|
|
43
|
+
next if value.nil?
|
|
27
44
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
raise Ace::Support::Cli::Error.new("GitHub CLI (gh) not installed. Install with: brew install gh")
|
|
45
|
+
puts "#{label.to_s.tr("_", "-")}: #{format_value(value)}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
32
48
|
end
|
|
33
49
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
unless found
|
|
41
|
-
raise Ace::Support::Cli::Error.new("No PR found for current branch. Specify a PR number.")
|
|
50
|
+
def format_value(value)
|
|
51
|
+
case value
|
|
52
|
+
when Hash
|
|
53
|
+
value.compact.map { |key, nested| "#{key}=#{nested}" }.join(" ")
|
|
54
|
+
else
|
|
55
|
+
value.to_s
|
|
42
56
|
end
|
|
43
|
-
found
|
|
44
57
|
end
|
|
45
58
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
59
|
+
def emit_receipt(options, operation, receipt)
|
|
60
|
+
pr = receipt.pull_request
|
|
61
|
+
render_evidence(
|
|
62
|
+
options,
|
|
63
|
+
header: "#{operation} PR ##{pr.number} on #{receipt.server_name}",
|
|
64
|
+
server: receipt.server_name,
|
|
65
|
+
operation: receipt.operation,
|
|
66
|
+
idempotency: receipt.idempotency,
|
|
67
|
+
number: pr.number,
|
|
68
|
+
url: pr.url,
|
|
69
|
+
title: pr.title,
|
|
70
|
+
state: pr.state,
|
|
71
|
+
draft: pr.draft,
|
|
72
|
+
head: pr.head_sha,
|
|
73
|
+
head_ref: pr.head_ref,
|
|
74
|
+
head_repository: pr.head_repository_url,
|
|
75
|
+
base_ref: pr.base_ref,
|
|
76
|
+
base_repository: pr.base_repository_url
|
|
77
|
+
)
|
|
51
78
|
end
|
|
79
|
+
end
|
|
52
80
|
|
|
53
|
-
|
|
54
|
-
|
|
81
|
+
# `ace-git pr show ID`
|
|
82
|
+
class Show < BaseCommand
|
|
83
|
+
desc "Show normalized pull request evidence"
|
|
84
|
+
|
|
85
|
+
argument :identifier, required: true, desc: "PR number, owner/repo#number, or URL"
|
|
86
|
+
|
|
87
|
+
def call(identifier:, **options)
|
|
88
|
+
pr = lifecycle(options).show(identifier)
|
|
89
|
+
render_evidence(
|
|
90
|
+
options,
|
|
91
|
+
header: "PR ##{pr.number} on #{pr.server_name}",
|
|
92
|
+
server: pr.server_name,
|
|
93
|
+
number: pr.number,
|
|
94
|
+
url: pr.url,
|
|
95
|
+
title: pr.title,
|
|
96
|
+
state: pr.state,
|
|
97
|
+
draft: pr.draft,
|
|
98
|
+
head: pr.head_sha,
|
|
99
|
+
head_ref: pr.head_ref,
|
|
100
|
+
head_repository: pr.head_repository_url,
|
|
101
|
+
base_ref: pr.base_ref,
|
|
102
|
+
base_repository: pr.base_repository_url,
|
|
103
|
+
merge_commit: pr.merge_commit_sha
|
|
104
|
+
)
|
|
105
|
+
rescue Ace::Git::Error, ArgumentError => e
|
|
106
|
+
raise Ace::Support::Cli::Error.new(e.message)
|
|
55
107
|
end
|
|
108
|
+
end
|
|
56
109
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
110
|
+
# `ace-git pr create --head REF --head-repo URL --base REF --expected-head SHA --title TEXT [--body-file PATH] [--draft]`
|
|
111
|
+
class Create < BaseCommand
|
|
112
|
+
desc "Create (or reconcile to) a pull request; drafts are the default"
|
|
113
|
+
|
|
114
|
+
option :head, type: :string, required: true, desc: "Source branch/ref"
|
|
115
|
+
option :head_repo, type: :string, desc: "Source repository URL (fork); defaults to the base repository"
|
|
116
|
+
option :base, type: :string, required: true, desc: "Base branch/ref"
|
|
117
|
+
option :expected_head, type: :string, required: true, desc: "Exact SHA the pushed source ref must resolve to"
|
|
118
|
+
option :title, type: :string, required: true, desc: "Pull request title"
|
|
119
|
+
option :body_file, type: :string, desc: "Path to the pull request body text"
|
|
120
|
+
option :draft, type: :boolean, default: true, desc: "Create as draft when the provider supports it"
|
|
121
|
+
|
|
122
|
+
def call(head:, base:, expected_head:, title:, **options)
|
|
123
|
+
receipt = lifecycle(options).create(
|
|
124
|
+
head_ref: head,
|
|
125
|
+
head_repository_url: options[:head_repo],
|
|
126
|
+
base_ref: base,
|
|
127
|
+
expected_head: expected_head,
|
|
128
|
+
title: title,
|
|
129
|
+
body_file: options[:body_file],
|
|
130
|
+
draft: options[:draft] != false
|
|
131
|
+
)
|
|
132
|
+
emit_receipt(options, "created", receipt)
|
|
133
|
+
rescue Ace::Git::Error, ArgumentError => e
|
|
134
|
+
raise Ace::Support::Cli::Error.new(e.message)
|
|
65
135
|
end
|
|
66
|
-
rescue Ace::Git::Error => e
|
|
67
|
-
raise Ace::Support::Cli::Error.new(e.message)
|
|
68
|
-
rescue ArgumentError => e
|
|
69
|
-
raise Ace::Support::Cli::Error.new(e.message)
|
|
70
136
|
end
|
|
71
137
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
138
|
+
# `ace-git pr update ID --expected-head SHA [--title TEXT] [--body-file PATH]`
|
|
139
|
+
class Update < BaseCommand
|
|
140
|
+
desc "Update pull request title/body after head verification"
|
|
141
|
+
|
|
142
|
+
argument :identifier, required: true, desc: "PR number, owner/repo#number, or URL"
|
|
143
|
+
option :expected_head, type: :string, required: true, desc: "Exact SHA the PR head must still be"
|
|
144
|
+
option :title, type: :string, desc: "New title"
|
|
145
|
+
option :body_file, type: :string, desc: "Path to the new body text"
|
|
146
|
+
|
|
147
|
+
def call(identifier:, expected_head:, **options)
|
|
148
|
+
receipt = lifecycle(options).update(
|
|
149
|
+
identifier,
|
|
150
|
+
expected_head: expected_head,
|
|
151
|
+
title: options[:title],
|
|
152
|
+
body_file: options[:body_file]
|
|
153
|
+
)
|
|
154
|
+
emit_receipt(options, "updated", receipt)
|
|
155
|
+
rescue Ace::Git::Error, ArgumentError => e
|
|
156
|
+
raise Ace::Support::Cli::Error.new(e.message)
|
|
91
157
|
end
|
|
92
|
-
|
|
93
|
-
lines << branch_parts.join(" | ") unless branch_parts.empty?
|
|
158
|
+
end
|
|
94
159
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
lines << "Author: #{author}"
|
|
99
|
-
end
|
|
160
|
+
# `ace-git pr ready ID --expected-head SHA`
|
|
161
|
+
class Ready < BaseCommand
|
|
162
|
+
desc "Mark a draft pull request ready for review"
|
|
100
163
|
|
|
101
|
-
# URL
|
|
102
|
-
|
|
164
|
+
argument :identifier, required: true, desc: "PR number, owner/repo#number, or URL"
|
|
165
|
+
option :expected_head, type: :string, required: true, desc: "Exact SHA the PR head must still be"
|
|
103
166
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
lines << diff
|
|
110
|
-
lines << "```"
|
|
167
|
+
def call(identifier:, expected_head:, **options)
|
|
168
|
+
receipt = lifecycle(options).ready(identifier, expected_head: expected_head)
|
|
169
|
+
emit_receipt(options, "readied", receipt)
|
|
170
|
+
rescue Ace::Git::Error, ArgumentError => e
|
|
171
|
+
raise Ace::Support::Cli::Error.new(e.message)
|
|
111
172
|
end
|
|
173
|
+
end
|
|
112
174
|
|
|
113
|
-
|
|
175
|
+
# `ace-git pr merge ID --expected-head SHA --method squash|merge|rebase`
|
|
176
|
+
class Merge < BaseCommand
|
|
177
|
+
desc "Merge with provider-side expected-head enforcement; no default method"
|
|
178
|
+
|
|
179
|
+
argument :identifier, required: true, desc: "PR number, owner/repo#number, or URL"
|
|
180
|
+
option :expected_head, type: :string, required: true, desc: "Exact SHA enforced by the provider"
|
|
181
|
+
option :method, type: :string, required: true, desc: "Merge method: squash, merge, rebase"
|
|
182
|
+
|
|
183
|
+
def call(identifier:, expected_head:, method:, **options)
|
|
184
|
+
receipt = lifecycle(options).merge(identifier, expected_head: expected_head, method: method)
|
|
185
|
+
emit_receipt(options, "merged", receipt)
|
|
186
|
+
rescue Ace::Git::Error, ArgumentError => e
|
|
187
|
+
raise Ace::Support::Cli::Error.new(e.message)
|
|
188
|
+
end
|
|
114
189
|
end
|
|
115
190
|
end
|
|
116
191
|
end
|
|
@@ -59,24 +59,34 @@ module Ace
|
|
|
59
59
|
# Include diff if requested
|
|
60
60
|
if options[:with_diff] && status.has_pr?
|
|
61
61
|
begin
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if diff_result[:success]
|
|
62
|
+
provider = active_provider
|
|
63
|
+
diff = provider&.pull_request_diff(number: status.pr_metadata.number.to_s)
|
|
64
|
+
if diff
|
|
66
65
|
puts ""
|
|
67
66
|
puts "## PR Diff"
|
|
68
67
|
puts ""
|
|
69
68
|
puts "```diff"
|
|
70
|
-
puts
|
|
69
|
+
puts diff
|
|
71
70
|
puts "```"
|
|
72
71
|
end
|
|
73
72
|
rescue Ace::Git::Error
|
|
74
|
-
#
|
|
73
|
+
# Classified provider failure: skip the diff section
|
|
75
74
|
end
|
|
76
75
|
end
|
|
77
76
|
rescue Ace::Git::Error => e
|
|
78
77
|
raise Ace::Support::Cli::Error.new(e.message)
|
|
79
78
|
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# Resolve the default server's registered provider for optional
|
|
83
|
+
# enrichment; nil when no server or provider is configured.
|
|
84
|
+
def active_provider
|
|
85
|
+
server = Ace::Git::ServerRegistry.resolve_default
|
|
86
|
+
Ace::Git::Providers.for(server)
|
|
87
|
+
rescue Ace::Git::Error
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
80
90
|
end
|
|
81
91
|
end
|
|
82
92
|
end
|
data/lib/ace/git/cli.rb
CHANGED
|
@@ -52,19 +52,24 @@ module Ace
|
|
|
52
52
|
["diff", "Show filtered git diff output"],
|
|
53
53
|
["status", "Show repository status and PR context"],
|
|
54
54
|
["branch", "Show current branch information"],
|
|
55
|
-
["pr", "
|
|
55
|
+
["pr", "Forge-neutral pull request lifecycle: show, create, update, ready, merge"]
|
|
56
56
|
].freeze
|
|
57
57
|
|
|
58
58
|
HELP_EXAMPLES = [
|
|
59
59
|
"ace-git diff --since 7d # Changes from last week",
|
|
60
60
|
"ace-git diff -p 'lib/**' -f summary # Filtered summary",
|
|
61
|
-
"ace-git status --no-pr # Quick status, skip network"
|
|
61
|
+
"ace-git status --no-pr # Quick status, skip network",
|
|
62
|
+
"ace-git pr show 25 --format json # Normalized PR evidence"
|
|
62
63
|
].freeze
|
|
63
64
|
|
|
64
65
|
register "diff", Commands::Diff.new
|
|
65
66
|
register "status", Commands::Status.new
|
|
66
67
|
register "branch", Commands::Branch.new
|
|
67
|
-
register "pr", Commands::Pr.new
|
|
68
|
+
register "pr show", Commands::Pr::Show.new
|
|
69
|
+
register "pr create", Commands::Pr::Create.new
|
|
70
|
+
register "pr update", Commands::Pr::Update.new
|
|
71
|
+
register "pr ready", Commands::Pr::Ready.new
|
|
72
|
+
register "pr merge", Commands::Pr::Merge.new
|
|
68
73
|
|
|
69
74
|
version_cmd = Ace::Support::Cli::VersionCommand.build(
|
|
70
75
|
gem_name: "ace-git",
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Git
|
|
5
|
+
# Error handling pattern:
|
|
6
|
+
# - Atoms: Pure functions, raise exceptions for invalid inputs
|
|
7
|
+
# - Molecules: May return error hashes for "expected" errors (e.g., not in git repo)
|
|
8
|
+
# or raise exceptions for unexpected failures
|
|
9
|
+
# - Organisms: Orchestrate molecules, propagate or wrap exceptions
|
|
10
|
+
# - Commands: Catch exceptions and return exit codes (0=success, 1=error)
|
|
11
|
+
#
|
|
12
|
+
# All custom exceptions inherit from Ace::Git::Error for consistent catching.
|
|
13
|
+
class Error < StandardError; end
|
|
14
|
+
class GitError < Error; end
|
|
15
|
+
class ConfigError < Error; end
|
|
16
|
+
class TimeoutError < Error; end
|
|
17
|
+
|
|
18
|
+
# ---- Forge server resolution failures ----
|
|
19
|
+
|
|
20
|
+
# Default requested but no default server is configured.
|
|
21
|
+
class NoDefaultServerConfiguredError < Error; end
|
|
22
|
+
|
|
23
|
+
# More than one server is marked as default in configuration.
|
|
24
|
+
class MultipleDefaultServersError < Error; end
|
|
25
|
+
|
|
26
|
+
# Multiple servers are configured with the same identifier.
|
|
27
|
+
class DuplicateServerNameError < Error; end
|
|
28
|
+
|
|
29
|
+
# A server references a provider type that no provider package registered.
|
|
30
|
+
class UnknownProviderError < Error; end
|
|
31
|
+
|
|
32
|
+
# The requested server name is not present in configuration.
|
|
33
|
+
class UnknownServerNameError < Error; end
|
|
34
|
+
|
|
35
|
+
# A remote URL matches multiple configured servers, or none, so the server
|
|
36
|
+
# identity cannot be determined without caller input.
|
|
37
|
+
class AmbiguousRemoteError < Error; end
|
|
38
|
+
|
|
39
|
+
# ---- Provider interaction failures ----
|
|
40
|
+
|
|
41
|
+
# The provider CLI binary is not installed in PATH.
|
|
42
|
+
class ProviderCliMissingError < Error; end
|
|
43
|
+
|
|
44
|
+
# The provider CLI is unauthenticated or its credential lease expired.
|
|
45
|
+
class ProviderAuthenticationError < Error; end
|
|
46
|
+
|
|
47
|
+
# The remote forge endpoint is offline or unreachable.
|
|
48
|
+
class ProviderUnreachableError < Error; end
|
|
49
|
+
|
|
50
|
+
# The provider CLI returned invalid JSON or an unexpected schema.
|
|
51
|
+
class ProviderMalformedOutputError < Error; end
|
|
52
|
+
|
|
53
|
+
# The requested pull request, issue, branch, or commit does not exist.
|
|
54
|
+
class ProviderObjectNotFoundError < Error; end
|
|
55
|
+
|
|
56
|
+
# ---- PR lifecycle mutation failures ----
|
|
57
|
+
|
|
58
|
+
# A supplied PR URL or repository identity does not match the resolved
|
|
59
|
+
# server (or the explicit selection contradicts the identifier). Detected
|
|
60
|
+
# before any mutation.
|
|
61
|
+
class ProviderIdentityMismatchError < Error; end
|
|
62
|
+
|
|
63
|
+
# More than one open pull request exactly matches the requested
|
|
64
|
+
# base/head identity, so no single object can be selected.
|
|
65
|
+
class ProviderConflictingMatchesError < Error; end
|
|
66
|
+
|
|
67
|
+
# The pull request head changed relative to the expected head SHA the
|
|
68
|
+
# caller supplied. The operation did not proceed (or the provider refused
|
|
69
|
+
# it atomically).
|
|
70
|
+
class ProviderExpectedHeadConflictError < Error; end
|
|
71
|
+
|
|
72
|
+
# The provider CLI cannot enforce a required precondition atomically
|
|
73
|
+
# (e.g. expected-head merge) or does not offer the requested operation.
|
|
74
|
+
# Never worked around with a check-then-act fallback.
|
|
75
|
+
class ProviderUnsupportedCapabilityError < Error; end
|
|
76
|
+
|
|
77
|
+
# A mutation request was sent but its outcome is unknown (e.g. transport
|
|
78
|
+
# failure after the send). Contains the exact base/head identity needed to
|
|
79
|
+
# reconcile by lookup; never retried automatically.
|
|
80
|
+
class ProviderUnknownOutcomeError < Error; end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -91,7 +91,7 @@ module Ace
|
|
|
91
91
|
elsif exclude_renames?
|
|
92
92
|
flags << "--diff-filter=ACDMTUXB"
|
|
93
93
|
elsif exclude_moves?
|
|
94
|
-
# Moves appear as
|
|
94
|
+
# Moves appear as renames with total similarity; disable such exact renames
|
|
95
95
|
flags << "-M0" # Disable rename detection (moves are renames at 100% similarity)
|
|
96
96
|
end
|
|
97
97
|
flags
|
|
@@ -164,7 +164,7 @@ module Ace
|
|
|
164
164
|
hash.each_key do |key|
|
|
165
165
|
key_str = key.to_s
|
|
166
166
|
next if KNOWN_KEYS.include?(key_str)
|
|
167
|
-
# Skip nested sections that may be
|
|
167
|
+
# Skip nested sections that may be propagated
|
|
168
168
|
next if %w[diff rebase pr squash default_branch remote verbose].include?(key_str)
|
|
169
169
|
|
|
170
170
|
warn "[ace-git] Unknown config key '#{key_str}' in DiffConfig - did you mean one of: #{KNOWN_KEYS.join(", ")}?"
|
|
@@ -35,17 +35,14 @@ module Ace
|
|
|
35
35
|
# @param executor [Module] Command executor
|
|
36
36
|
# @return [Hash] { ahead: Integer, behind: Integer }
|
|
37
37
|
def tracking_status(executor: Atoms::CommandExecutor)
|
|
38
|
-
|
|
38
|
+
ahead = count_commits("@{upstream}..HEAD", executor: executor)
|
|
39
|
+
behind = count_commits("HEAD..@{upstream}", executor: executor)
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
if ahead.nil? || behind.nil?
|
|
41
42
|
return {ahead: 0, behind: 0, error: "No tracking branch or not in git repo"}
|
|
42
43
|
end
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
{
|
|
46
|
-
ahead: parts[1].to_i,
|
|
47
|
-
behind: parts[0].to_i
|
|
48
|
-
}
|
|
45
|
+
{ahead: ahead, behind: behind}
|
|
49
46
|
end
|
|
50
47
|
|
|
51
48
|
# Get full branch information
|
|
@@ -85,6 +82,15 @@ module Ace
|
|
|
85
82
|
"#{behind} behind"
|
|
86
83
|
end
|
|
87
84
|
end
|
|
85
|
+
|
|
86
|
+
# Count commits in a rev-list range; nil when the range is invalid
|
|
87
|
+
# (e.g. no upstream configured)
|
|
88
|
+
def count_commits(range, executor: Atoms::CommandExecutor)
|
|
89
|
+
result = executor.execute("git", "rev-list", "--count", range)
|
|
90
|
+
return nil unless result[:success]
|
|
91
|
+
|
|
92
|
+
result[:output].strip.to_i
|
|
93
|
+
end
|
|
88
94
|
end
|
|
89
95
|
end
|
|
90
96
|
end
|
|
@@ -13,7 +13,7 @@ module Ace
|
|
|
13
13
|
class << self
|
|
14
14
|
# Load configuration using ace-config cascade with deep merge
|
|
15
15
|
# Priority: instance_config merged over global config (which is already defaults + user)
|
|
16
|
-
# @param instance_config [Hash] Instance-level configuration (
|
|
16
|
+
# @param instance_config [Hash] Instance-level configuration (top priority)
|
|
17
17
|
# @return [Models::DiffConfig] Merged configuration
|
|
18
18
|
def load(instance_config = {})
|
|
19
19
|
# Get global config from ace-git (already merged defaults + user per ADR-022)
|