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,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "ace/support/cli"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module Git
|
|
8
|
+
module CLI
|
|
9
|
+
module Commands
|
|
10
|
+
# ace-support-cli command for showing PR information
|
|
11
|
+
class Pr < Ace::Support::Cli::Command
|
|
12
|
+
include Ace::Support::Cli::Base
|
|
13
|
+
|
|
14
|
+
desc "Show PR information"
|
|
15
|
+
|
|
16
|
+
argument :number, required: false, desc: "PR number (auto-detected if not provided)"
|
|
17
|
+
|
|
18
|
+
option :format, type: :string, aliases: ["f"], default: "markdown",
|
|
19
|
+
desc: "Output format: markdown, json"
|
|
20
|
+
option :with_diff, type: :boolean, default: false,
|
|
21
|
+
desc: "Include PR diff in output"
|
|
22
|
+
|
|
23
|
+
# Standard options
|
|
24
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
25
|
+
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
26
|
+
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
27
|
+
|
|
28
|
+
def call(number: nil, **options)
|
|
29
|
+
# Check if gh is installed
|
|
30
|
+
unless Molecules::PrMetadataFetcher.gh_installed?
|
|
31
|
+
raise Ace::Support::Cli::Error.new("GitHub CLI (gh) not installed. Install with: brew install gh")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Determine PR identifier
|
|
35
|
+
identifier = if number
|
|
36
|
+
number.to_s
|
|
37
|
+
else
|
|
38
|
+
# Try to find PR for current branch
|
|
39
|
+
found = Molecules::PrMetadataFetcher.find_pr_for_branch
|
|
40
|
+
unless found
|
|
41
|
+
raise Ace::Support::Cli::Error.new("No PR found for current branch. Specify a PR number.")
|
|
42
|
+
end
|
|
43
|
+
found
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Fetch PR data
|
|
47
|
+
result = if options[:with_diff]
|
|
48
|
+
Molecules::PrMetadataFetcher.fetch_pr(identifier)
|
|
49
|
+
else
|
|
50
|
+
Molecules::PrMetadataFetcher.fetch_metadata(identifier)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
unless result[:success]
|
|
54
|
+
raise Ace::Support::Cli::Error.new(result[:error])
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Output based on format
|
|
58
|
+
case options[:format]
|
|
59
|
+
when "json"
|
|
60
|
+
output_data = {metadata: result[:metadata]}
|
|
61
|
+
output_data[:diff] = result[:diff] if options[:with_diff]
|
|
62
|
+
puts JSON.pretty_generate(output_data)
|
|
63
|
+
else
|
|
64
|
+
output_markdown(result[:metadata], result[:diff], options)
|
|
65
|
+
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
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def output_markdown(metadata, diff, options)
|
|
75
|
+
lines = []
|
|
76
|
+
|
|
77
|
+
# Header line: # PR #82: Title... [OPEN]
|
|
78
|
+
header = "# PR ##{metadata["number"]}"
|
|
79
|
+
header += ": #{metadata["title"]}" if metadata["title"]
|
|
80
|
+
header += " [#{metadata["state"]}]" if metadata["state"]
|
|
81
|
+
lines << header
|
|
82
|
+
|
|
83
|
+
# Branch line: Branch: source → target | Draft: No
|
|
84
|
+
branch_parts = []
|
|
85
|
+
if metadata["headRefName"] && metadata["baseRefName"]
|
|
86
|
+
branch_parts << "Branch: #{metadata["headRefName"]} → #{metadata["baseRefName"]}"
|
|
87
|
+
elsif metadata["headRefName"]
|
|
88
|
+
branch_parts << "Branch: #{metadata["headRefName"]}"
|
|
89
|
+
elsif metadata["baseRefName"]
|
|
90
|
+
branch_parts << "Target: #{metadata["baseRefName"]}"
|
|
91
|
+
end
|
|
92
|
+
branch_parts << "Draft: #{metadata["isDraft"] ? "Yes" : "No"}" if metadata.key?("isDraft")
|
|
93
|
+
lines << branch_parts.join(" | ") unless branch_parts.empty?
|
|
94
|
+
|
|
95
|
+
# Author line
|
|
96
|
+
if metadata["author"]
|
|
97
|
+
author = metadata["author"].is_a?(Hash) ? metadata["author"]["login"] : metadata["author"]
|
|
98
|
+
lines << "Author: #{author}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# URL line
|
|
102
|
+
lines << "URL: #{metadata["url"]}" if metadata["url"]
|
|
103
|
+
|
|
104
|
+
if options[:with_diff] && diff
|
|
105
|
+
lines << ""
|
|
106
|
+
lines << "## Diff"
|
|
107
|
+
lines << ""
|
|
108
|
+
lines << "```diff"
|
|
109
|
+
lines << diff
|
|
110
|
+
lines << "```"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
puts lines.join("\n")
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "ace/support/cli"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module Git
|
|
8
|
+
module CLI
|
|
9
|
+
module Commands
|
|
10
|
+
# ace-support-cli command for showing repository status
|
|
11
|
+
class Status < Ace::Support::Cli::Command
|
|
12
|
+
include Ace::Support::Cli::Base
|
|
13
|
+
|
|
14
|
+
desc "Show repository context (branch, PR, activity)"
|
|
15
|
+
|
|
16
|
+
option :format, type: :string, aliases: ["f"], default: "markdown",
|
|
17
|
+
desc: "Output format: markdown, json"
|
|
18
|
+
option :with_diff, type: :boolean, default: false,
|
|
19
|
+
desc: "Include PR diff in output"
|
|
20
|
+
option :no_pr, type: :boolean, default: false, aliases: ["n"],
|
|
21
|
+
desc: "Skip all PR lookups (faster, no network)"
|
|
22
|
+
option :commits, type: :integer, aliases: ["c"],
|
|
23
|
+
desc: "Number of recent commits to show (0 to disable, default: config)"
|
|
24
|
+
|
|
25
|
+
# Standard options
|
|
26
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
27
|
+
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
28
|
+
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
29
|
+
|
|
30
|
+
def call(**options)
|
|
31
|
+
# Determine PR settings based on --no-pr flag
|
|
32
|
+
skip_pr = options[:no_pr]
|
|
33
|
+
commits_limit = options[:commits] || Ace::Git.commits_limit
|
|
34
|
+
|
|
35
|
+
# Load status
|
|
36
|
+
status_options = {
|
|
37
|
+
include_pr: !skip_pr,
|
|
38
|
+
include_pr_activity: !skip_pr,
|
|
39
|
+
include_commits: commits_limit > 0,
|
|
40
|
+
commits_limit: commits_limit,
|
|
41
|
+
timeout: Ace::Git.network_timeout
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
status = Organisms::RepoStatusLoader.load(status_options)
|
|
45
|
+
|
|
46
|
+
# Check for errors
|
|
47
|
+
if status.branch.nil? && status.repository_type == :not_git
|
|
48
|
+
raise Ace::Support::Cli::Error.new("Not in a git repository")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Output based on format
|
|
52
|
+
case options[:format]
|
|
53
|
+
when "json"
|
|
54
|
+
puts JSON.pretty_generate(status.to_h)
|
|
55
|
+
else
|
|
56
|
+
puts status.to_markdown
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Include diff if requested
|
|
60
|
+
if options[:with_diff] && status.has_pr?
|
|
61
|
+
begin
|
|
62
|
+
diff_result = Molecules::PrMetadataFetcher.fetch_diff(
|
|
63
|
+
status.pr_metadata["number"].to_s
|
|
64
|
+
)
|
|
65
|
+
if diff_result[:success]
|
|
66
|
+
puts ""
|
|
67
|
+
puts "## PR Diff"
|
|
68
|
+
puts ""
|
|
69
|
+
puts "```diff"
|
|
70
|
+
puts diff_result[:diff]
|
|
71
|
+
puts "```"
|
|
72
|
+
end
|
|
73
|
+
rescue Ace::Git::Error
|
|
74
|
+
# Silently skip diff if it fails
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
rescue Ace::Git::Error => e
|
|
78
|
+
raise Ace::Support::Cli::Error.new(e.message)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
data/lib/ace/git/cli.rb
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
require "ace/core"
|
|
5
|
+
require_relative "../git"
|
|
6
|
+
require_relative "cli/commands/diff"
|
|
7
|
+
require_relative "cli/commands/status"
|
|
8
|
+
require_relative "cli/commands/branch"
|
|
9
|
+
require_relative "cli/commands/pr"
|
|
10
|
+
|
|
11
|
+
module Ace
|
|
12
|
+
module Git
|
|
13
|
+
# ace-support-cli command registry for ace-git.
|
|
14
|
+
module CLI
|
|
15
|
+
extend Ace::Support::Cli::RegistryDsl
|
|
16
|
+
|
|
17
|
+
# Entry point for CLI invocation (used by tests via cli_helpers)
|
|
18
|
+
#
|
|
19
|
+
# Mirrors the exe's normalized_args logic so in-process tests
|
|
20
|
+
# can exercise range-pattern routing.
|
|
21
|
+
#
|
|
22
|
+
# @param args [Array<String>] Command-line arguments
|
|
23
|
+
def self.start(args)
|
|
24
|
+
Ace::Support::Cli::Runner.new(self).call(args: normalized_args(args))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Normalize arguments to handle empty args and git range patterns.
|
|
28
|
+
# Extracted from exe/ace-git for in-process testability.
|
|
29
|
+
def self.normalized_args(argv)
|
|
30
|
+
return ["--help"] if argv.empty?
|
|
31
|
+
return ["diff"] + argv if git_range_pattern?(argv.first)
|
|
32
|
+
|
|
33
|
+
argv
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.git_range_pattern?(arg)
|
|
37
|
+
return false if arg.nil?
|
|
38
|
+
|
|
39
|
+
return true if arg.match?(/\.\.\.?/) # Range operators: .. or ...
|
|
40
|
+
return true if arg.match?(/[~^]\d*/) # Ref modifiers: ~, ~2, ^, ^2
|
|
41
|
+
return true if arg == "HEAD" # Exact HEAD match
|
|
42
|
+
return true if arg.match?(/@\{/) # Reflog: @{1}, @{yesterday}
|
|
43
|
+
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private_class_method :git_range_pattern?
|
|
48
|
+
|
|
49
|
+
PROGRAM_NAME = "ace-git"
|
|
50
|
+
|
|
51
|
+
REGISTERED_COMMANDS = [
|
|
52
|
+
["diff", "Show filtered git diff output"],
|
|
53
|
+
["status", "Show repository status and PR context"],
|
|
54
|
+
["branch", "Show current branch information"],
|
|
55
|
+
["pr", "Show pull request information"]
|
|
56
|
+
].freeze
|
|
57
|
+
|
|
58
|
+
HELP_EXAMPLES = [
|
|
59
|
+
"ace-git diff --since 7d # Changes from last week",
|
|
60
|
+
"ace-git diff -p 'lib/**' -f summary # Filtered summary",
|
|
61
|
+
"ace-git status --no-pr # Quick status, skip network"
|
|
62
|
+
].freeze
|
|
63
|
+
|
|
64
|
+
register "diff", Commands::Diff.new
|
|
65
|
+
register "status", Commands::Status.new
|
|
66
|
+
register "branch", Commands::Branch.new
|
|
67
|
+
register "pr", Commands::Pr.new
|
|
68
|
+
|
|
69
|
+
version_cmd = Ace::Support::Cli::VersionCommand.build(
|
|
70
|
+
gem_name: "ace-git",
|
|
71
|
+
version: Ace::Git::VERSION
|
|
72
|
+
)
|
|
73
|
+
register "version", version_cmd
|
|
74
|
+
register "--version", version_cmd
|
|
75
|
+
|
|
76
|
+
help_cmd = Ace::Support::Cli::HelpCommand.build(
|
|
77
|
+
program_name: PROGRAM_NAME,
|
|
78
|
+
version: Ace::Git::VERSION,
|
|
79
|
+
commands: REGISTERED_COMMANDS,
|
|
80
|
+
examples: HELP_EXAMPLES
|
|
81
|
+
)
|
|
82
|
+
register "help", help_cmd
|
|
83
|
+
register "--help", help_cmd
|
|
84
|
+
register "-h", help_cmd
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Git
|
|
5
|
+
module Models
|
|
6
|
+
# Data structure representing diff configuration
|
|
7
|
+
# Migrated from ace-git-diff
|
|
8
|
+
class DiffConfig
|
|
9
|
+
attr_reader :exclude_patterns, :exclude_whitespace, :exclude_renames,
|
|
10
|
+
:exclude_moves, :max_lines, :ranges, :paths, :since, :format, :timeout,
|
|
11
|
+
:grouped_stats_layers, :grouped_stats_collapse_above,
|
|
12
|
+
:grouped_stats_show_full_tree, :grouped_stats_dotfile_groups
|
|
13
|
+
|
|
14
|
+
# @param exclude_patterns [Array<String>] Glob patterns to exclude
|
|
15
|
+
# @param exclude_whitespace [Boolean] Whether to exclude whitespace changes
|
|
16
|
+
# @param exclude_renames [Boolean] Whether to exclude renames
|
|
17
|
+
# @param exclude_moves [Boolean] Whether to exclude moves
|
|
18
|
+
# @param max_lines [Integer] Maximum lines in diff output
|
|
19
|
+
# @param ranges [Array<String>] Git ranges to diff (e.g., ["origin/main...HEAD"])
|
|
20
|
+
# @param paths [Array<String>] Path patterns to include
|
|
21
|
+
# @param since [String] Date or commit to diff from
|
|
22
|
+
# @param format [Symbol] Output format (:diff or :summary)
|
|
23
|
+
# @param timeout [Integer] Command timeout in seconds (default from config)
|
|
24
|
+
# @param grouped_stats_layers [Array<String>] Layer order for grouped-stats output
|
|
25
|
+
# @param grouped_stats_collapse_above [Integer] Markdown collapse threshold
|
|
26
|
+
# @param grouped_stats_show_full_tree [String] Tree rendering mode
|
|
27
|
+
# @param grouped_stats_dotfile_groups [Array<String>] Dot directories to prioritize
|
|
28
|
+
def initialize(
|
|
29
|
+
exclude_patterns: [],
|
|
30
|
+
exclude_whitespace: true,
|
|
31
|
+
exclude_renames: false,
|
|
32
|
+
exclude_moves: false,
|
|
33
|
+
max_lines: 10_000,
|
|
34
|
+
ranges: [],
|
|
35
|
+
paths: [],
|
|
36
|
+
since: nil,
|
|
37
|
+
format: :diff,
|
|
38
|
+
timeout: Ace::Git.git_timeout,
|
|
39
|
+
grouped_stats_layers: %w[lib test handbook],
|
|
40
|
+
grouped_stats_collapse_above: 5,
|
|
41
|
+
grouped_stats_show_full_tree: "collapsible",
|
|
42
|
+
grouped_stats_dotfile_groups: %w[.ace-taskflow .ace]
|
|
43
|
+
)
|
|
44
|
+
@exclude_patterns = Array(exclude_patterns)
|
|
45
|
+
@exclude_whitespace = exclude_whitespace
|
|
46
|
+
@exclude_renames = exclude_renames
|
|
47
|
+
@exclude_moves = exclude_moves
|
|
48
|
+
@max_lines = max_lines
|
|
49
|
+
@ranges = Array(ranges)
|
|
50
|
+
@paths = Array(paths)
|
|
51
|
+
@since = since
|
|
52
|
+
@format = format&.to_sym || :diff
|
|
53
|
+
@timeout = timeout || Ace::Git.git_timeout
|
|
54
|
+
@grouped_stats_layers = Array(grouped_stats_layers).map(&:to_s)
|
|
55
|
+
@grouped_stats_collapse_above = grouped_stats_collapse_above.to_i
|
|
56
|
+
@grouped_stats_show_full_tree = grouped_stats_show_full_tree.to_s
|
|
57
|
+
@grouped_stats_dotfile_groups = Array(grouped_stats_dotfile_groups).map(&:to_s)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Check if diff should exclude whitespace changes
|
|
61
|
+
# @return [Boolean] True if whitespace should be excluded
|
|
62
|
+
def exclude_whitespace?
|
|
63
|
+
@exclude_whitespace
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Check if diff should exclude renames
|
|
67
|
+
# @return [Boolean] True if renames should be excluded
|
|
68
|
+
def exclude_renames?
|
|
69
|
+
@exclude_renames
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Check if diff should exclude moves
|
|
73
|
+
# @return [Boolean] True if moves should be excluded
|
|
74
|
+
def exclude_moves?
|
|
75
|
+
@exclude_moves
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Get git diff command flags based on configuration
|
|
79
|
+
# @return [Array<String>] Command flags
|
|
80
|
+
def git_flags
|
|
81
|
+
flags = []
|
|
82
|
+
flags << "-w" if exclude_whitespace?
|
|
83
|
+
# Use --diff-filter to exclude renames (R) and optionally moves
|
|
84
|
+
# This is more explicit and avoids redundant flags
|
|
85
|
+
# ACDMTUXB includes: Added, Copied, Deleted, Modified, Type, Unmerged, Unknown, Broken
|
|
86
|
+
# When excluding moves, we also disable copy detection (C) as moves are detected as copy+delete
|
|
87
|
+
if exclude_renames? && exclude_moves?
|
|
88
|
+
# Exclude both renames (R) and copies (C) which covers moves
|
|
89
|
+
flags << "--diff-filter=ADMTUXB"
|
|
90
|
+
flags << "-M0" # Disable rename detection entirely
|
|
91
|
+
elsif exclude_renames?
|
|
92
|
+
flags << "--diff-filter=ACDMTUXB"
|
|
93
|
+
elsif exclude_moves?
|
|
94
|
+
# Moves appear as rename with 100% similarity, disable high-similarity renames
|
|
95
|
+
flags << "-M0" # Disable rename detection (moves are renames at 100% similarity)
|
|
96
|
+
end
|
|
97
|
+
flags
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Convert to hash representation
|
|
101
|
+
# @return [Hash] Hash representation of the config
|
|
102
|
+
def to_h
|
|
103
|
+
{
|
|
104
|
+
exclude_patterns: exclude_patterns,
|
|
105
|
+
exclude_whitespace: exclude_whitespace?,
|
|
106
|
+
exclude_renames: exclude_renames?,
|
|
107
|
+
exclude_moves: exclude_moves?,
|
|
108
|
+
max_lines: max_lines,
|
|
109
|
+
ranges: ranges,
|
|
110
|
+
paths: paths,
|
|
111
|
+
since: since,
|
|
112
|
+
format: format,
|
|
113
|
+
timeout: timeout,
|
|
114
|
+
grouped_stats: {
|
|
115
|
+
layers: grouped_stats_layers,
|
|
116
|
+
collapse_above: grouped_stats_collapse_above,
|
|
117
|
+
show_full_tree: grouped_stats_show_full_tree,
|
|
118
|
+
dotfile_groups: grouped_stats_dotfile_groups
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Known configuration keys
|
|
124
|
+
KNOWN_KEYS = %w[
|
|
125
|
+
exclude_patterns exclude_whitespace exclude_renames exclude_moves
|
|
126
|
+
max_lines ranges paths since format timeout grouped_stats
|
|
127
|
+
].freeze
|
|
128
|
+
|
|
129
|
+
# Create DiffConfig from hash (e.g., from YAML config)
|
|
130
|
+
# Warns on unknown keys to help users catch typos
|
|
131
|
+
# @param hash [Hash] Configuration hash
|
|
132
|
+
# @return [DiffConfig] New DiffConfig instance
|
|
133
|
+
def self.from_hash(hash)
|
|
134
|
+
return new if hash.nil? || hash.empty?
|
|
135
|
+
|
|
136
|
+
# Warn about unknown keys to help catch typos
|
|
137
|
+
warn_unknown_keys(hash)
|
|
138
|
+
|
|
139
|
+
grouped_stats = hash["grouped_stats"] || hash[:grouped_stats] || {}
|
|
140
|
+
|
|
141
|
+
new(
|
|
142
|
+
exclude_patterns: hash["exclude_patterns"] || hash[:exclude_patterns] || [],
|
|
143
|
+
exclude_whitespace: hash.fetch("exclude_whitespace", hash.fetch(:exclude_whitespace, true)),
|
|
144
|
+
exclude_renames: hash.fetch("exclude_renames", hash.fetch(:exclude_renames, false)),
|
|
145
|
+
exclude_moves: hash.fetch("exclude_moves", hash.fetch(:exclude_moves, false)),
|
|
146
|
+
max_lines: hash.fetch("max_lines", hash.fetch(:max_lines, 10_000)),
|
|
147
|
+
ranges: hash["ranges"] || hash[:ranges] || [],
|
|
148
|
+
paths: hash["paths"] || hash[:paths] || [],
|
|
149
|
+
since: hash["since"] || hash[:since],
|
|
150
|
+
format: hash["format"] || hash[:format] || :diff,
|
|
151
|
+
timeout: hash.fetch("timeout", hash.fetch(:timeout, Ace::Git.git_timeout)),
|
|
152
|
+
grouped_stats_layers: grouped_stats["layers"] || grouped_stats[:layers] || %w[lib test handbook],
|
|
153
|
+
grouped_stats_collapse_above: grouped_stats.fetch("collapse_above", grouped_stats.fetch(:collapse_above, 5)),
|
|
154
|
+
grouped_stats_show_full_tree: grouped_stats["show_full_tree"] || grouped_stats[:show_full_tree] || "collapsible",
|
|
155
|
+
grouped_stats_dotfile_groups: grouped_stats["dotfile_groups"] || grouped_stats[:dotfile_groups] || %w[.ace-taskflow .ace]
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Warn about unknown configuration keys
|
|
160
|
+
# @param hash [Hash] Configuration hash
|
|
161
|
+
def self.warn_unknown_keys(hash)
|
|
162
|
+
return if hash.nil? || hash.empty?
|
|
163
|
+
|
|
164
|
+
hash.each_key do |key|
|
|
165
|
+
key_str = key.to_s
|
|
166
|
+
next if KNOWN_KEYS.include?(key_str)
|
|
167
|
+
# Skip nested sections that may be passed through
|
|
168
|
+
next if %w[diff rebase pr squash default_branch remote verbose].include?(key_str)
|
|
169
|
+
|
|
170
|
+
warn "[ace-git] Unknown config key '#{key_str}' in DiffConfig - did you mean one of: #{KNOWN_KEYS.join(", ")}?"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Merge with another config (other takes precedence - complete override)
|
|
175
|
+
# @param other [DiffConfig, Hash] Other config to merge
|
|
176
|
+
# @return [DiffConfig] New merged DiffConfig
|
|
177
|
+
def merge(other)
|
|
178
|
+
other_hash = other.is_a?(DiffConfig) ? other.to_h : other
|
|
179
|
+
|
|
180
|
+
self.class.from_hash(to_h.merge(other_hash))
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Git
|
|
5
|
+
module Models
|
|
6
|
+
# Data structure representing the result of a diff operation
|
|
7
|
+
# Migrated from ace-git-diff
|
|
8
|
+
class DiffResult
|
|
9
|
+
attr_reader :content, :stats, :files, :metadata, :filtered
|
|
10
|
+
|
|
11
|
+
# @param content [String] The diff content
|
|
12
|
+
# @param stats [Hash] Statistics about the diff (additions, deletions, files)
|
|
13
|
+
# @param files [Array<String>] List of files in the diff
|
|
14
|
+
# @param metadata [Hash] Additional metadata (range, since, options, etc)
|
|
15
|
+
# @param filtered [Boolean] Whether the diff has been filtered
|
|
16
|
+
def initialize(content:, stats:, files:, metadata: {}, filtered: false)
|
|
17
|
+
@content = content
|
|
18
|
+
@stats = stats
|
|
19
|
+
@files = files
|
|
20
|
+
@metadata = metadata
|
|
21
|
+
@filtered = filtered
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Check if the diff is empty
|
|
25
|
+
# @return [Boolean] True if diff has no content
|
|
26
|
+
def empty?
|
|
27
|
+
content.nil? || content.strip.empty?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Check if the diff has changes
|
|
31
|
+
# @return [Boolean] True if diff contains additions or deletions
|
|
32
|
+
def has_changes?
|
|
33
|
+
stats[:total_changes].to_i > 0
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get the number of lines in the diff
|
|
37
|
+
# @return [Integer] Line count
|
|
38
|
+
def line_count
|
|
39
|
+
stats[:line_count] || content&.count("\n")&.+(1) || 0
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Get human-readable summary
|
|
43
|
+
# @return [String] Summary string
|
|
44
|
+
def summary
|
|
45
|
+
"#{files.length} files, +#{stats[:additions]} -#{stats[:deletions]}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Convert to hash representation
|
|
49
|
+
# @return [Hash] Hash representation of the diff result
|
|
50
|
+
def to_h
|
|
51
|
+
{
|
|
52
|
+
content: content,
|
|
53
|
+
stats: stats,
|
|
54
|
+
files: files,
|
|
55
|
+
metadata: metadata,
|
|
56
|
+
filtered: filtered,
|
|
57
|
+
empty: empty?,
|
|
58
|
+
has_changes: has_changes?,
|
|
59
|
+
line_count: line_count,
|
|
60
|
+
summary: summary
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Create a DiffResult from parsed diff data
|
|
65
|
+
# @param parsed_data [Hash] Parsed diff data from DiffParser
|
|
66
|
+
# @param metadata [Hash] Additional metadata
|
|
67
|
+
# @param filtered [Boolean] Whether the diff was filtered
|
|
68
|
+
# @return [DiffResult] New DiffResult instance
|
|
69
|
+
def self.from_parsed(parsed_data, metadata: {}, filtered: false)
|
|
70
|
+
new(
|
|
71
|
+
content: parsed_data[:content],
|
|
72
|
+
stats: parsed_data[:stats].merge(line_count: parsed_data[:line_count]),
|
|
73
|
+
files: parsed_data[:files],
|
|
74
|
+
metadata: metadata,
|
|
75
|
+
filtered: filtered
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Create an empty DiffResult
|
|
80
|
+
# @param metadata [Hash] Optional metadata
|
|
81
|
+
# @return [DiffResult] Empty DiffResult instance
|
|
82
|
+
def self.empty(metadata: {})
|
|
83
|
+
new(
|
|
84
|
+
content: "",
|
|
85
|
+
stats: {additions: 0, deletions: 0, files: 0, total_changes: 0, line_count: 0},
|
|
86
|
+
files: [],
|
|
87
|
+
metadata: metadata,
|
|
88
|
+
filtered: false
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|