ace-git 0.18.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 +7 -0
- data/.ace-defaults/git/config.yml +83 -0
- data/.ace-defaults/nav/protocols/guide-sources/ace-git.yml +10 -0
- data/.ace-defaults/nav/protocols/tmpl-sources/ace-git.yml +19 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-git.yml +19 -0
- data/CHANGELOG.md +762 -0
- data/LICENSE +21 -0
- data/README.md +48 -0
- data/Rakefile +14 -0
- data/docs/demo/ace-git-getting-started.gif +0 -0
- data/docs/demo/ace-git-getting-started.tape.yml +18 -0
- data/docs/demo/fixtures/README.md +3 -0
- data/docs/demo/fixtures/sample.txt +1 -0
- data/docs/getting-started.md +87 -0
- data/docs/handbook.md +50 -0
- data/docs/usage.md +259 -0
- data/exe/ace-git +37 -0
- data/handbook/guides/version-control/ruby.md +41 -0
- data/handbook/guides/version-control/rust.md +49 -0
- data/handbook/guides/version-control/typescript.md +47 -0
- data/handbook/guides/version-control-system-git.g.md +829 -0
- data/handbook/skills/as-git-rebase/SKILL.md +43 -0
- data/handbook/skills/as-git-reorganize-commits/SKILL.md +41 -0
- data/handbook/skills/as-github-pr-create/SKILL.md +60 -0
- data/handbook/skills/as-github-pr-update/SKILL.md +41 -0
- data/handbook/skills/as-github-release-publish/SKILL.md +58 -0
- data/handbook/templates/commit/squash.template.md +59 -0
- data/handbook/templates/pr/bugfix.template.md +103 -0
- data/handbook/templates/pr/default.template.md +40 -0
- data/handbook/templates/pr/feature.template.md +41 -0
- data/handbook/workflow-instructions/git/rebase.wf.md +402 -0
- data/handbook/workflow-instructions/git/reorganize-commits.wf.md +158 -0
- data/handbook/workflow-instructions/github/pr/create.wf.md +282 -0
- data/handbook/workflow-instructions/github/pr/update.wf.md +199 -0
- data/handbook/workflow-instructions/github/release-publish.wf.md +162 -0
- data/lib/ace/git/atoms/command_executor.rb +253 -0
- data/lib/ace/git/atoms/date_resolver.rb +129 -0
- data/lib/ace/git/atoms/diff_numstat_parser.rb +82 -0
- data/lib/ace/git/atoms/diff_parser.rb +110 -0
- data/lib/ace/git/atoms/file_grouper.rb +152 -0
- data/lib/ace/git/atoms/git_scope_filter.rb +86 -0
- data/lib/ace/git/atoms/git_status_fetcher.rb +29 -0
- data/lib/ace/git/atoms/grouped_stats_formatter.rb +233 -0
- data/lib/ace/git/atoms/lock_error_detector.rb +79 -0
- data/lib/ace/git/atoms/pattern_filter.rb +156 -0
- data/lib/ace/git/atoms/pr_identifier_parser.rb +88 -0
- data/lib/ace/git/atoms/repository_checker.rb +97 -0
- data/lib/ace/git/atoms/repository_state_detector.rb +92 -0
- data/lib/ace/git/atoms/stale_lock_cleaner.rb +247 -0
- data/lib/ace/git/atoms/status_formatter.rb +180 -0
- data/lib/ace/git/atoms/task_pattern_extractor.rb +57 -0
- data/lib/ace/git/atoms/time_formatter.rb +84 -0
- data/lib/ace/git/cli/commands/branch.rb +62 -0
- data/lib/ace/git/cli/commands/diff.rb +252 -0
- data/lib/ace/git/cli/commands/pr.rb +119 -0
- data/lib/ace/git/cli/commands/status.rb +84 -0
- data/lib/ace/git/cli.rb +87 -0
- data/lib/ace/git/models/diff_config.rb +185 -0
- data/lib/ace/git/models/diff_result.rb +94 -0
- data/lib/ace/git/models/repo_status.rb +202 -0
- data/lib/ace/git/molecules/branch_reader.rb +92 -0
- data/lib/ace/git/molecules/config_loader.rb +108 -0
- data/lib/ace/git/molecules/diff_filter.rb +102 -0
- data/lib/ace/git/molecules/diff_generator.rb +160 -0
- data/lib/ace/git/molecules/git_status_fetcher.rb +32 -0
- data/lib/ace/git/molecules/pr_metadata_fetcher.rb +286 -0
- data/lib/ace/git/molecules/recent_commits_fetcher.rb +53 -0
- data/lib/ace/git/organisms/diff_orchestrator.rb +178 -0
- data/lib/ace/git/organisms/repo_status_loader.rb +264 -0
- data/lib/ace/git/version.rb +7 -0
- data/lib/ace/git.rb +230 -0
- metadata +201 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Git
|
|
5
|
+
module Organisms
|
|
6
|
+
# Orchestrates loading complete repository status
|
|
7
|
+
# Combines branch info, task pattern detection, and PR metadata
|
|
8
|
+
class RepoStatusLoader
|
|
9
|
+
class << self
|
|
10
|
+
# Load complete repository status
|
|
11
|
+
# @param options [Hash] Options for status loading
|
|
12
|
+
# @option options [Boolean] :include_pr Whether to fetch PR metadata (default: true)
|
|
13
|
+
# @option options [Boolean] :include_pr_activity Whether to fetch PR activity (default: true)
|
|
14
|
+
# @option options [Boolean] :include_commits Whether to fetch recent commits (default: true)
|
|
15
|
+
# @option options [Integer] :commits_limit Number of recent commits to fetch (default: 3)
|
|
16
|
+
# @option options [Integer] :timeout Timeout for network operations like PR fetch (default: network_timeout)
|
|
17
|
+
# @return [Models::RepoStatus] Complete repository status
|
|
18
|
+
def load(options = {})
|
|
19
|
+
include_pr = options.fetch(:include_pr, true)
|
|
20
|
+
include_pr_activity = options.fetch(:include_pr_activity, true)
|
|
21
|
+
include_commits = options.fetch(:include_commits, true)
|
|
22
|
+
commits_limit = options.fetch(:commits_limit, Ace::Git.commits_limit)
|
|
23
|
+
timeout = options.fetch(:timeout, Ace::Git.network_timeout)
|
|
24
|
+
|
|
25
|
+
# Get repository type and state
|
|
26
|
+
repo_type = Atoms::RepositoryChecker.repository_type
|
|
27
|
+
repo_state = Atoms::RepositoryStateDetector.detect
|
|
28
|
+
|
|
29
|
+
# Check if we can proceed
|
|
30
|
+
unless Atoms::RepositoryChecker.usable?
|
|
31
|
+
return Models::RepoStatus.new(
|
|
32
|
+
branch: nil,
|
|
33
|
+
repository_type: repo_type,
|
|
34
|
+
repository_state: repo_state
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Get branch information
|
|
39
|
+
branch_info = Molecules::BranchReader.full_info
|
|
40
|
+
|
|
41
|
+
# Extract task pattern from branch name
|
|
42
|
+
task_pattern = nil
|
|
43
|
+
if branch_info[:name] && branch_info[:name] != "HEAD"
|
|
44
|
+
task_pattern = Atoms::TaskPatternExtractor.extract(branch_info[:name])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Fetch git status (always, it's fast and local)
|
|
48
|
+
git_status_sb = fetch_git_status
|
|
49
|
+
|
|
50
|
+
# Fetch recent commits if requested
|
|
51
|
+
recent_commits = nil
|
|
52
|
+
if include_commits && commits_limit > 0
|
|
53
|
+
recent_commits = fetch_recent_commits(limit: commits_limit)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Fetch PR data (metadata and activity) - parallelized for performance
|
|
57
|
+
pr_metadata = nil
|
|
58
|
+
pr_activity = nil
|
|
59
|
+
if !branch_info[:detached]
|
|
60
|
+
if include_pr && include_pr_activity
|
|
61
|
+
# Both requested: use parallel fetch for ~50% speedup
|
|
62
|
+
pr_data = fetch_pr_data_parallel(
|
|
63
|
+
current_branch: branch_info[:name],
|
|
64
|
+
timeout: timeout
|
|
65
|
+
)
|
|
66
|
+
pr_metadata = pr_data[:pr_metadata]
|
|
67
|
+
pr_activity = pr_data[:pr_activity]
|
|
68
|
+
elsif include_pr
|
|
69
|
+
# Only PR metadata requested
|
|
70
|
+
pr_metadata = fetch_pr_metadata(timeout: timeout)
|
|
71
|
+
elsif include_pr_activity
|
|
72
|
+
# Only activity requested
|
|
73
|
+
pr_activity = fetch_pr_activity(
|
|
74
|
+
current_branch: branch_info[:name],
|
|
75
|
+
timeout: timeout
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Build and return status
|
|
81
|
+
Models::RepoStatus.from_data(
|
|
82
|
+
branch_info: branch_info,
|
|
83
|
+
task_pattern: task_pattern,
|
|
84
|
+
pr_metadata: pr_metadata,
|
|
85
|
+
pr_activity: pr_activity,
|
|
86
|
+
git_status_sb: git_status_sb,
|
|
87
|
+
recent_commits: recent_commits,
|
|
88
|
+
repo_type: repo_type,
|
|
89
|
+
repo_state: repo_state
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Load status for a specific PR
|
|
94
|
+
# @param pr_identifier [String] PR identifier
|
|
95
|
+
# @param options [Hash] Options
|
|
96
|
+
# @return [Models::RepoStatus] Status with PR data
|
|
97
|
+
def load_for_pr(pr_identifier, options = {})
|
|
98
|
+
timeout = options.fetch(:timeout, Ace::Git.network_timeout)
|
|
99
|
+
|
|
100
|
+
# Get basic status (skip PR activity since we're fetching a specific PR)
|
|
101
|
+
status = load(include_pr: false, include_pr_activity: false)
|
|
102
|
+
|
|
103
|
+
# Fetch specific PR metadata
|
|
104
|
+
begin
|
|
105
|
+
result = Molecules::PrMetadataFetcher.fetch_metadata(pr_identifier, timeout: timeout)
|
|
106
|
+
pr_metadata = result[:success] ? result[:metadata] : nil
|
|
107
|
+
rescue Ace::Git::Error
|
|
108
|
+
pr_metadata = nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Return status with PR data
|
|
112
|
+
Models::RepoStatus.from_data(
|
|
113
|
+
branch_info: {
|
|
114
|
+
name: status.branch,
|
|
115
|
+
tracking: status.tracking,
|
|
116
|
+
ahead: status.ahead,
|
|
117
|
+
behind: status.behind
|
|
118
|
+
},
|
|
119
|
+
task_pattern: status.task_pattern,
|
|
120
|
+
pr_metadata: pr_metadata,
|
|
121
|
+
repo_type: status.repository_type,
|
|
122
|
+
repo_state: status.repository_state
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Load minimal status (branch only, no PR)
|
|
127
|
+
# @return [Models::RepoStatus] Minimal status
|
|
128
|
+
def load_minimal
|
|
129
|
+
load(include_pr: false, include_pr_activity: false, include_commits: false)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
# Fetch git status in short branch format
|
|
135
|
+
# @return [String, nil] Git status output or nil
|
|
136
|
+
def fetch_git_status
|
|
137
|
+
result = Molecules::GitStatusFetcher.fetch_status_sb
|
|
138
|
+
result[:success] ? result[:output] : nil
|
|
139
|
+
rescue
|
|
140
|
+
nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Fetch recent commits
|
|
144
|
+
# @param limit [Integer] Number of commits to fetch
|
|
145
|
+
# @return [Array, nil] Array of commit hashes or nil
|
|
146
|
+
def fetch_recent_commits(limit:)
|
|
147
|
+
result = Molecules::RecentCommitsFetcher.fetch(limit: limit)
|
|
148
|
+
result[:success] ? result[:commits] : nil
|
|
149
|
+
rescue
|
|
150
|
+
nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Fetch PR metadata for current branch
|
|
154
|
+
# @param timeout [Integer] Timeout in seconds
|
|
155
|
+
# @return [Hash, nil] PR metadata or nil
|
|
156
|
+
def fetch_pr_metadata(timeout:)
|
|
157
|
+
# First try to find PR for current branch
|
|
158
|
+
pr_number = Molecules::PrMetadataFetcher.find_pr_for_branch(timeout: timeout)
|
|
159
|
+
return nil unless pr_number
|
|
160
|
+
|
|
161
|
+
# Then fetch full metadata
|
|
162
|
+
result = Molecules::PrMetadataFetcher.fetch_metadata(pr_number, timeout: timeout)
|
|
163
|
+
result[:success] ? result[:metadata] : nil
|
|
164
|
+
rescue Ace::Git::GhNotInstalledError, Ace::Git::GhAuthenticationError
|
|
165
|
+
# gh not available, skip PR metadata
|
|
166
|
+
nil
|
|
167
|
+
rescue Ace::Git::PrNotFoundError
|
|
168
|
+
# No PR for this branch
|
|
169
|
+
nil
|
|
170
|
+
rescue Ace::Git::TimeoutError
|
|
171
|
+
# Timeout, skip PR metadata
|
|
172
|
+
nil
|
|
173
|
+
rescue
|
|
174
|
+
# Any other error, skip PR metadata
|
|
175
|
+
nil
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Fetch PR activity (recently merged and open PRs)
|
|
179
|
+
# @param current_branch [String] Current branch name to exclude from open PRs
|
|
180
|
+
# @param timeout [Integer] Timeout in seconds
|
|
181
|
+
# @return [Hash, nil] PR activity with :merged and :open arrays (symbol keys), or nil
|
|
182
|
+
# Each PR in the arrays has string keys from JSON parsing: "number", "title", etc.
|
|
183
|
+
def fetch_pr_activity(current_branch:, timeout:)
|
|
184
|
+
merged_result = Molecules::PrMetadataFetcher.fetch_recently_merged(
|
|
185
|
+
limit: Ace::Git.merged_prs_limit,
|
|
186
|
+
timeout: timeout
|
|
187
|
+
)
|
|
188
|
+
open_result = Molecules::PrMetadataFetcher.fetch_open_prs(
|
|
189
|
+
exclude_branch: current_branch,
|
|
190
|
+
timeout: timeout
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Return nil if both failed
|
|
194
|
+
return nil unless merged_result[:success] || open_result[:success]
|
|
195
|
+
|
|
196
|
+
# Use symbol keys for outer hash, string keys for PR data (from JSON)
|
|
197
|
+
# This is documented behavior - consumers should access via pr_activity[:merged]
|
|
198
|
+
# and individual PRs via pr["number"], pr["title"], etc.
|
|
199
|
+
{
|
|
200
|
+
merged: merged_result[:success] ? merged_result[:prs] : [],
|
|
201
|
+
open: open_result[:success] ? open_result[:prs] : []
|
|
202
|
+
}
|
|
203
|
+
rescue
|
|
204
|
+
# Any error, skip PR activity
|
|
205
|
+
nil
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Fetch PR metadata and activity in a single API call for optimal performance
|
|
209
|
+
# Uses gh pr list --state all to get all PRs, then filters locally
|
|
210
|
+
# @param current_branch [String] Current branch name to match/exclude
|
|
211
|
+
# @param timeout [Integer] Timeout in seconds
|
|
212
|
+
# @return [Hash] Result with :pr_metadata and :pr_activity keys
|
|
213
|
+
def fetch_pr_data_parallel(current_branch:, timeout:)
|
|
214
|
+
# Single API call gets all recent PRs
|
|
215
|
+
result = Molecules::PrMetadataFetcher.fetch_all_prs(
|
|
216
|
+
limit: 15, # Enough for current + merged + open
|
|
217
|
+
timeout: timeout
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return {pr_metadata: nil, pr_activity: nil} unless result[:success]
|
|
221
|
+
|
|
222
|
+
prs = result[:prs]
|
|
223
|
+
|
|
224
|
+
# Find current PR (matching branch, prefer OPEN > MERGED > CLOSED)
|
|
225
|
+
branch_prs = prs.select { |pr| pr["headRefName"] == current_branch }
|
|
226
|
+
current_pr = branch_prs.min_by { |pr|
|
|
227
|
+
case pr["state"]
|
|
228
|
+
when "OPEN" then 0
|
|
229
|
+
when "MERGED" then 1
|
|
230
|
+
when "CLOSED" then 2
|
|
231
|
+
else 3
|
|
232
|
+
end
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
# Get merged PRs (sorted by mergedAt descending, limited)
|
|
236
|
+
merged_prs = prs
|
|
237
|
+
.select { |pr| pr["state"] == "MERGED" }
|
|
238
|
+
.sort_by { |pr| pr["mergedAt"] || "" }
|
|
239
|
+
.reverse
|
|
240
|
+
.take(Ace::Git.merged_prs_limit)
|
|
241
|
+
|
|
242
|
+
# Get open PRs (exclude current branch, limited)
|
|
243
|
+
open_prs = prs
|
|
244
|
+
.select { |pr| pr["state"] == "OPEN" && pr["headRefName"] != current_branch }
|
|
245
|
+
.take(Ace::Git.open_prs_limit)
|
|
246
|
+
|
|
247
|
+
# Build activity hash (nil if no activity to show)
|
|
248
|
+
pr_activity = nil
|
|
249
|
+
if merged_prs.any? || open_prs.any?
|
|
250
|
+
pr_activity = {merged: merged_prs, open: open_prs}
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
{
|
|
254
|
+
pr_metadata: current_pr,
|
|
255
|
+
pr_activity: pr_activity
|
|
256
|
+
}
|
|
257
|
+
rescue
|
|
258
|
+
{pr_metadata: nil, pr_activity: nil}
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
data/lib/ace/git.rb
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/config"
|
|
4
|
+
require_relative "git/version"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module Git
|
|
8
|
+
# Error handling pattern:
|
|
9
|
+
# - Atoms: Pure functions, raise exceptions for invalid inputs
|
|
10
|
+
# - Molecules: May return error hashes for "expected" errors (e.g., not in git repo)
|
|
11
|
+
# or raise exceptions for unexpected failures
|
|
12
|
+
# - Organisms: Orchestrate molecules, propagate or wrap exceptions
|
|
13
|
+
# - Commands: Catch exceptions and return exit codes (0=success, 1=error)
|
|
14
|
+
#
|
|
15
|
+
# All custom exceptions inherit from Ace::Git::Error for consistent catching.
|
|
16
|
+
class Error < StandardError; end
|
|
17
|
+
class GitError < Error; end
|
|
18
|
+
class ConfigError < Error; end
|
|
19
|
+
class GhNotInstalledError < Error; end
|
|
20
|
+
class GhAuthenticationError < Error; end
|
|
21
|
+
class PrNotFoundError < Error; end
|
|
22
|
+
class TimeoutError < Error; end
|
|
23
|
+
|
|
24
|
+
# Mutex for thread-safe config initialization
|
|
25
|
+
@config_mutex = Mutex.new
|
|
26
|
+
|
|
27
|
+
# Get configuration for ace-git
|
|
28
|
+
# Follows ADR-022: Configuration Default and Override Pattern
|
|
29
|
+
# Uses Ace::Support::Config.create() for configuration cascade resolution
|
|
30
|
+
# Thread-safe: uses mutex for initialization
|
|
31
|
+
# @return [Hash] merged configuration hash
|
|
32
|
+
# @example Get current configuration
|
|
33
|
+
# config = Ace::Git.config
|
|
34
|
+
# puts config["default_branch"] # => "main"
|
|
35
|
+
# @example Access nested diff config
|
|
36
|
+
# exclude = Ace::Git.config["exclude_patterns"]
|
|
37
|
+
def self.config
|
|
38
|
+
# Fast path: return cached config if already initialized
|
|
39
|
+
return @config if defined?(@config) && @config
|
|
40
|
+
|
|
41
|
+
# Thread-safe initialization
|
|
42
|
+
@config_mutex.synchronize do
|
|
43
|
+
@config ||= load_config
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Load configuration using Ace::Support::Config cascade
|
|
48
|
+
# Resolves gem defaults from .ace-defaults/ and user overrides from .ace/
|
|
49
|
+
# @return [Hash] Merged and transformed configuration
|
|
50
|
+
def self.load_config
|
|
51
|
+
gem_root = Gem.loaded_specs["ace-git"]&.gem_dir ||
|
|
52
|
+
File.expand_path("../..", __dir__)
|
|
53
|
+
|
|
54
|
+
resolver = Ace::Support::Config.create(
|
|
55
|
+
config_dir: ".ace",
|
|
56
|
+
defaults_dir: ".ace-defaults",
|
|
57
|
+
gem_path: gem_root
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Resolve config for git namespace
|
|
61
|
+
config = resolver.resolve_namespace("git")
|
|
62
|
+
|
|
63
|
+
# Extract and flatten the git section for backward compatibility
|
|
64
|
+
raw_config = config.data["git"] || config.data
|
|
65
|
+
extract_git_config(raw_config)
|
|
66
|
+
rescue Ace::Support::Config::YamlParseError => e
|
|
67
|
+
warn "ace-git: YAML syntax error in configuration"
|
|
68
|
+
warn " #{e.message}"
|
|
69
|
+
# Fall back to gem defaults instead of empty hash to prevent silent config erasure
|
|
70
|
+
extract_git_config(load_gem_defaults_fallback(gem_root))
|
|
71
|
+
rescue => e
|
|
72
|
+
warn "ace-git: Failed to load configuration: #{e.message}"
|
|
73
|
+
# Fall back to gem defaults instead of empty hash to prevent silent config erasure
|
|
74
|
+
gem_root = Gem.loaded_specs["ace-git"]&.gem_dir ||
|
|
75
|
+
File.expand_path("../..", __dir__)
|
|
76
|
+
extract_git_config(load_gem_defaults_fallback(gem_root))
|
|
77
|
+
end
|
|
78
|
+
private_class_method :load_config
|
|
79
|
+
|
|
80
|
+
# Load gem defaults directly as fallback when cascade resolution fails
|
|
81
|
+
# This ensures configuration is never silently erased due to YAML errors
|
|
82
|
+
# or user config issues
|
|
83
|
+
# @param gem_root [String] Path to gem root directory
|
|
84
|
+
# @return [Hash] Defaults hash or empty hash if defaults also fail
|
|
85
|
+
def self.load_gem_defaults_fallback(gem_root)
|
|
86
|
+
defaults_path = File.join(gem_root, ".ace-defaults", "git", "config.yml")
|
|
87
|
+
|
|
88
|
+
return {} unless File.exist?(defaults_path)
|
|
89
|
+
|
|
90
|
+
data = YAML.safe_load_file(defaults_path, permitted_classes: [Date], aliases: true) || {}
|
|
91
|
+
data["git"] || data
|
|
92
|
+
rescue
|
|
93
|
+
{} # Only return empty hash if even defaults fail to load
|
|
94
|
+
end
|
|
95
|
+
private_class_method :load_gem_defaults_fallback
|
|
96
|
+
|
|
97
|
+
# Extract git configuration from YAML structure
|
|
98
|
+
#
|
|
99
|
+
# BACKWARD COMPATIBILITY NOTE:
|
|
100
|
+
# This method flattens the nested `diff:` section to top-level keys.
|
|
101
|
+
# This is required because the original default_config structure used flat keys
|
|
102
|
+
# (e.g., `exclude_patterns`, `exclude_whitespace`) rather than nested `diff.exclude_patterns`.
|
|
103
|
+
# The YAML config uses `git.diff.exclude_patterns` for clarity, but internally we flatten
|
|
104
|
+
# to maintain compatibility with DiffConfig.from_hash and existing consumers.
|
|
105
|
+
#
|
|
106
|
+
# Example transformation:
|
|
107
|
+
# git:
|
|
108
|
+
# diff:
|
|
109
|
+
# exclude_patterns: ["*.log"]
|
|
110
|
+
# becomes:
|
|
111
|
+
# { "exclude_patterns" => ["*.log"] }
|
|
112
|
+
#
|
|
113
|
+
# Keys are kept as strings for consistency with YAML loading.
|
|
114
|
+
# Use config["key"] or config.key?("key") for access.
|
|
115
|
+
#
|
|
116
|
+
# @param git_section [Hash] The git: section from YAML
|
|
117
|
+
# @return [Hash] Flattened configuration with string keys
|
|
118
|
+
def self.extract_git_config(git_section)
|
|
119
|
+
return {} if git_section.nil? || git_section.empty?
|
|
120
|
+
|
|
121
|
+
config = {}
|
|
122
|
+
|
|
123
|
+
# Normalize keys to strings for consistency
|
|
124
|
+
normalized = normalize_keys(git_section)
|
|
125
|
+
|
|
126
|
+
# Copy top-level settings
|
|
127
|
+
%w[default_branch remote verbose timeout network_timeout].each do |key|
|
|
128
|
+
config[key] = normalized[key] if normalized.key?(key)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Flatten diff: section to top-level for backward compatibility (see note above)
|
|
132
|
+
diff_section = normalized["diff"]
|
|
133
|
+
if diff_section.is_a?(Hash)
|
|
134
|
+
normalize_keys(diff_section).each do |key, value|
|
|
135
|
+
config[key] = value
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Copy other sections as-is (rebase, pr, squash, status, lock_retry, etc.)
|
|
140
|
+
%w[rebase pr squash status lock_retry].each do |key|
|
|
141
|
+
config[key] = normalized[key] if normalized.key?(key)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
config
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Normalize hash keys to strings for consistent access
|
|
148
|
+
# @param hash [Hash] Hash with potentially mixed string/symbol keys
|
|
149
|
+
# @return [Hash] Hash with string keys
|
|
150
|
+
def self.normalize_keys(hash)
|
|
151
|
+
return {} unless hash.is_a?(Hash)
|
|
152
|
+
|
|
153
|
+
hash.transform_keys(&:to_s)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Reset configuration cache (mainly for testing)
|
|
157
|
+
# Thread-safe: uses mutex to prevent race conditions
|
|
158
|
+
def self.reset_config!
|
|
159
|
+
@config_mutex.synchronize do
|
|
160
|
+
@config = nil
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# ---- Configuration Helper Methods (ADR-022 compliant) ----
|
|
165
|
+
# These read from config instead of using hardcoded constants
|
|
166
|
+
|
|
167
|
+
# Timeout for local git operations (diff, status, log)
|
|
168
|
+
# @return [Integer] Timeout in seconds (default: 30)
|
|
169
|
+
def self.git_timeout
|
|
170
|
+
config["timeout"] || 30
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Timeout for network operations (gh CLI, remote operations)
|
|
174
|
+
# @return [Integer] Timeout in seconds (default: 60)
|
|
175
|
+
def self.network_timeout
|
|
176
|
+
config["network_timeout"] || 60
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Number of recent commits to show in status output
|
|
180
|
+
# @return [Integer] Limit (default: 3)
|
|
181
|
+
def self.commits_limit
|
|
182
|
+
config.dig("status", "commits_limit") || 3
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Number of recently merged PRs to show in status output
|
|
186
|
+
# @return [Integer] Limit (default: 3)
|
|
187
|
+
def self.merged_prs_limit
|
|
188
|
+
config.dig("status", "merged_prs_limit") || 3
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Number of open PRs to show in status output
|
|
192
|
+
# @return [Integer] Limit (default: 10)
|
|
193
|
+
def self.open_prs_limit
|
|
194
|
+
config.dig("status", "open_prs_limit") || 10
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Require ATOM architecture components
|
|
200
|
+
require_relative "git/atoms/command_executor"
|
|
201
|
+
require_relative "git/atoms/pattern_filter"
|
|
202
|
+
require_relative "git/atoms/diff_parser"
|
|
203
|
+
require_relative "git/atoms/diff_numstat_parser"
|
|
204
|
+
require_relative "git/atoms/file_grouper"
|
|
205
|
+
require_relative "git/atoms/grouped_stats_formatter"
|
|
206
|
+
require_relative "git/atoms/date_resolver"
|
|
207
|
+
require_relative "git/atoms/task_pattern_extractor"
|
|
208
|
+
require_relative "git/atoms/pr_identifier_parser"
|
|
209
|
+
require_relative "git/atoms/repository_state_detector"
|
|
210
|
+
require_relative "git/atoms/repository_checker"
|
|
211
|
+
require_relative "git/atoms/git_scope_filter"
|
|
212
|
+
require_relative "git/atoms/time_formatter"
|
|
213
|
+
require_relative "git/atoms/status_formatter"
|
|
214
|
+
require_relative "git/atoms/lock_error_detector"
|
|
215
|
+
require_relative "git/atoms/stale_lock_cleaner"
|
|
216
|
+
|
|
217
|
+
require_relative "git/molecules/diff_generator"
|
|
218
|
+
require_relative "git/molecules/config_loader"
|
|
219
|
+
require_relative "git/molecules/diff_filter"
|
|
220
|
+
require_relative "git/molecules/branch_reader"
|
|
221
|
+
require_relative "git/molecules/pr_metadata_fetcher"
|
|
222
|
+
require_relative "git/molecules/recent_commits_fetcher"
|
|
223
|
+
require_relative "git/molecules/git_status_fetcher"
|
|
224
|
+
|
|
225
|
+
require_relative "git/organisms/diff_orchestrator"
|
|
226
|
+
require_relative "git/organisms/repo_status_loader"
|
|
227
|
+
|
|
228
|
+
require_relative "git/models/diff_result"
|
|
229
|
+
require_relative "git/models/diff_config"
|
|
230
|
+
require_relative "git/models/repo_status"
|