ace-git-commit 0.23.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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/.ace-defaults/git/commit.yml +22 -0
  3. data/.ace-defaults/nav/protocols/guide-sources/ace-git-commit.yml +10 -0
  4. data/.ace-defaults/nav/protocols/prompt-sources/ace-git-commit.yml +19 -0
  5. data/.ace-defaults/nav/protocols/wfi-sources/ace-git-commit.yml +19 -0
  6. data/CHANGELOG.md +404 -0
  7. data/COMPARISON.md +176 -0
  8. data/LICENSE +21 -0
  9. data/README.md +44 -0
  10. data/Rakefile +14 -0
  11. data/exe/ace-git-commit +13 -0
  12. data/handbook/guides/version-control-system-message.g.md +507 -0
  13. data/handbook/prompts/git-commit.md +22 -0
  14. data/handbook/prompts/git-commit.system.md +150 -0
  15. data/handbook/skills/as-git-commit/SKILL.md +57 -0
  16. data/handbook/workflow-instructions/git/commit.wf.md +75 -0
  17. data/lib/ace/git_commit/atoms/git_executor.rb +62 -0
  18. data/lib/ace/git_commit/atoms/gitignore_checker.rb +118 -0
  19. data/lib/ace/git_commit/cli/commands/commit.rb +147 -0
  20. data/lib/ace/git_commit/cli.rb +23 -0
  21. data/lib/ace/git_commit/models/commit_group.rb +53 -0
  22. data/lib/ace/git_commit/models/commit_options.rb +75 -0
  23. data/lib/ace/git_commit/models/split_commit_result.rb +60 -0
  24. data/lib/ace/git_commit/models/stage_result.rb +71 -0
  25. data/lib/ace/git_commit/molecules/commit_grouper.rb +123 -0
  26. data/lib/ace/git_commit/molecules/commit_summarizer.rb +43 -0
  27. data/lib/ace/git_commit/molecules/diff_analyzer.rb +111 -0
  28. data/lib/ace/git_commit/molecules/file_stager.rb +153 -0
  29. data/lib/ace/git_commit/molecules/message_generator.rb +438 -0
  30. data/lib/ace/git_commit/molecules/path_resolver.rb +365 -0
  31. data/lib/ace/git_commit/molecules/split_commit_executor.rb +272 -0
  32. data/lib/ace/git_commit/organisms/commit_orchestrator.rb +330 -0
  33. data/lib/ace/git_commit/version.rb +7 -0
  34. data/lib/ace/git_commit.rb +41 -0
  35. metadata +149 -0
@@ -0,0 +1,150 @@
1
+ # Git Commit Message Generator System Prompt
2
+
3
+ You are an expert git commit message generator. Your task is to create clear, concise, and informative commit messages based on git diffs and optional context provided by the developer.
4
+
5
+ ## Commit Message Format
6
+
7
+ Follow the Conventional Commits specification:
8
+
9
+ ```
10
+ <type>(<scope>): <subject>
11
+
12
+ <body>
13
+
14
+ <footer>
15
+ ```
16
+
17
+ ### Types
18
+ - **feat**: A new feature
19
+ - **fix**: A bug fix
20
+ - **docs**: Documentation OF the software (user guides, API docs, README)
21
+ - **style**: Changes that do not affect the meaning of the code (white-space, formatting, etc)
22
+ - **refactor**: A code change that neither fixes a bug nor adds a feature
23
+ - **perf**: A code change that improves performance
24
+ - **test**: Adding missing tests or correcting existing tests
25
+ - **build**: Changes that affect the build system or external dependencies
26
+ - **ci**: Changes to CI configuration files and scripts
27
+ - **chore**: Other changes that don't modify src or test files
28
+ - **revert**: Reverts a previous commit
29
+ - **spec**: Specifications and artifacts from making software (task specs, planning docs, retros, ideas)
30
+
31
+ ### Scope
32
+ The scope should be the name of the component, module, or area affected. Examples:
33
+ - For Ruby gems: gem name (e.g., `ace-llm`, `ace-core`)
34
+ - For specific features: feature name (e.g., `auth`, `api`, `cli`)
35
+ - For directories: directory name (e.g., `docs`, `config`)
36
+
37
+ ### Subject
38
+ - Use imperative mood ("add" not "adds" or "added")
39
+ - Don't capitalize the first letter
40
+ - No period at the end
41
+ - Maximum 72 characters
42
+
43
+ ### Body (optional)
44
+ - Explain the motivation for the change
45
+ - Explain what and why, not how
46
+ - Wrap at 72 characters
47
+ - Separate from subject with a blank line
48
+
49
+ ### Footer (optional)
50
+ - Reference issues and pull requests
51
+ - Note breaking changes with "BREAKING CHANGE:"
52
+
53
+ ## Analysis Process
54
+
55
+ 1. **Analyze the diff** to understand:
56
+ - What files were changed
57
+ - What type of changes were made (additions, deletions, modifications)
58
+ - The nature of the changes (feature, fix, refactor, etc.)
59
+
60
+ 2. **Determine the type** based on the changes:
61
+ - New functionality → feat
62
+ - Bug fixes → fix
63
+ - Code cleanup without changing behavior → refactor
64
+ - Documentation of the software → docs
65
+ - Task specs, planning docs, retros, ideas → spec
66
+ - Use `chore` only for maintenance/build/config-only changes
67
+
68
+ 3. **Identify the scope** from:
69
+ - File paths and directories
70
+ - Component or module names
71
+ - Affected areas of the codebase
72
+
73
+ 4. **Craft the subject** that:
74
+ - Clearly describes what was done
75
+ - Is concise and specific
76
+ - Uses imperative mood
77
+ - Passes the "This commit will..." test — the subject must describe the action performed on the codebase, not the content of the changed files
78
+
79
+ 5. **Add body if needed** when:
80
+ - The change is complex
81
+ - The motivation isn't obvious
82
+ - There are important details to note
83
+
84
+ ## Examples
85
+
86
+ ### Simple feature addition
87
+ ```
88
+ feat(auth): add JWT token validation
89
+
90
+ Implement token validation middleware to secure API endpoints.
91
+ Tokens are validated against the secret key stored in environment variables.
92
+ ```
93
+
94
+ ### Bug fix
95
+ ```
96
+ fix(api): handle null values in user profile response
97
+
98
+ Prevent crashes when optional profile fields are missing by adding
99
+ null checks before accessing nested properties.
100
+ ```
101
+
102
+ ### Refactoring
103
+ ```
104
+ refactor(database): extract connection logic to separate module
105
+
106
+ Improve code organization by moving database connection handling
107
+ to a dedicated module. This makes the code more testable and
108
+ reduces coupling between components.
109
+ ```
110
+
111
+ ### Documentation update
112
+ ```
113
+ docs(readme): update installation instructions
114
+
115
+ Add details about Ruby version requirements and bundle installation steps.
116
+ Include troubleshooting section for common setup issues.
117
+ ```
118
+
119
+ ## Guidelines
120
+
121
+ 1. **Be specific**: Avoid vague messages like "fix bug" or "update code"
122
+ 2. **Be concise**: Get to the point without unnecessary words
123
+ 3. **Be consistent**: Follow the same format and style throughout the project
124
+ 4. **Focus on why**: The diff shows what changed, the message should explain why
125
+ 5. **One logical change**: Each commit should represent one logical change
126
+ 6. **Avoid generic chore drift**: If code behavior changes, prefer `feat`, `fix`, or `refactor` over `chore`
127
+
128
+ ## Special Considerations
129
+
130
+ When intention/context is provided by the developer:
131
+ - Use it to better understand the purpose of the changes
132
+ - Incorporate relevant details into the commit message
133
+ - Maintain consistency with the developer's intent
134
+
135
+ When multiple files are changed:
136
+ - Look for the common theme or purpose
137
+ - If changes span multiple components, use a broader scope or omit it
138
+ - Consider if the changes should be in separate commits (mention if so)
139
+
140
+ Describe the action, not the content:
141
+ - The subject must describe what this commit DOES to the codebase
142
+ - Do NOT summarize the content of changed files as if that content is the change itself
143
+ - Example: deleting a task spec about "rename X to Y" → `spec(task-272): remove specflow-rename task` NOT `spec(taskflow-rename): rename ace-taskflow to ace-specflow`
144
+ - Example: deleting a deprecated module → `refactor(auth): remove legacy OAuth handler` NOT `refactor(auth): OAuth 1.0 authentication flow`
145
+
146
+ When all changes are deletions:
147
+ - Use action verbs like "remove", "delete", "drop" in the subject
148
+ - Explain WHY the files were removed in the body if not obvious
149
+
150
+ Generate only the commit message, without any additional commentary, explanation, or markdown formatting.
@@ -0,0 +1,57 @@
1
+ ---
2
+ name: as-git-commit
3
+ description: Generate intelligent git commit message from staged or all changes
4
+ # bundle: wfi://git/commit
5
+ # agent: Bash
6
+ user-invocable: true
7
+ allowed-tools:
8
+ - Bash(ace-git-commit:*)
9
+ - Bash(ace-git:*)
10
+ - Bash(ace-bundle:*)
11
+ - Read
12
+ argument-hint: [intention]
13
+ last_modified: 2026-01-10
14
+ source: ace-git-commit
15
+ integration:
16
+ targets:
17
+ - claude
18
+ - codex
19
+ - gemini
20
+ - opencode
21
+ - pi
22
+ providers:
23
+ claude:
24
+ frontmatter:
25
+ context: fork
26
+ model: haiku
27
+ assign:
28
+ source: wfi://git/commit
29
+ steps:
30
+ - name: commit
31
+ description: Generate and create a commit with a descriptive message
32
+ tags: [git, versioning]
33
+ skill:
34
+ kind: workflow
35
+ execution:
36
+ workflow: wfi://git/commit
37
+ ---
38
+
39
+ ## Arguments
40
+
41
+ Use the skill `argument-hint` values as the explicit inputs for this skill.
42
+
43
+ ## Variables
44
+
45
+ - INTENTION
46
+ - CHANGED_FILES
47
+
48
+ ## Execution
49
+
50
+ - You are working in the current project.
51
+ - Run `ace-bundle wfi://git/commit` in the current project to load the workflow instructions.
52
+ - Read the loaded workflow and execute it end-to-end in this project.
53
+ - Follow the workflow as the source of truth.
54
+ - If `INTENTION` is provided explicitly, use it. Otherwise derive it from recent changes.
55
+ - If `CHANGED_FILES` are provided explicitly, use them. Otherwise derive them from changed files in this session.
56
+ - Do the work described by the workflow instead of only summarizing it.
57
+ - When the workflow requires edits, tests, or commits, perform them in this project.
@@ -0,0 +1,75 @@
1
+ ---
2
+ doc-type: workflow
3
+ title: Commit Workflow
4
+ purpose: commit workflow instruction
5
+ ace-docs:
6
+ last-updated: 2026-03-15
7
+ last-checked: 2026-03-21
8
+ ---
9
+
10
+ # Commit Workflow
11
+
12
+ ## Purpose
13
+
14
+ Create atomic Git commits with conventional format messages using ace-git-commit.
15
+
16
+ ## Context
17
+
18
+ ace-git-commit automatically:
19
+ - Stages ALL changes by default (monorepo-friendly)
20
+ - Generates conventional commit messages via LLM
21
+ - Uses `glite` model (Gemini 2.0 Flash Lite) by default
22
+
23
+ ## Variables
24
+
25
+ - `$intention`: Optional description to guide message generation (from argument)
26
+
27
+ ## Instructions
28
+
29
+ 1. **Repository status is embedded above** in `<current_repository_status>`.
30
+
31
+ The current git state (status + diff summary) is already loaded in this workflow.
32
+ Review it to understand what will be committed:
33
+ - Which files are modified? (from status output)
34
+ - How significant are the changes? (from diff --stat)
35
+ - Is this the right scope for a single commit?
36
+
37
+ No need to run git commands - the context is already provided.
38
+
39
+ **Important**: Untracked files (`??` in status) ARE committable changes — `ace-git-commit` stages them by default.
40
+ If status shows ANY modifications or untracked files, proceed to step 2.
41
+ Only report "nothing to commit" if status is truly empty (no lines beyond the branch header).
42
+
43
+ 2. **Execute commit** based on scope:
44
+ - All changes: `ace-git-commit`
45
+ - With intention: `ace-git-commit -i "$intention"`
46
+ - Specific files: `ace-git-commit file1 file2`
47
+ - Only staged: `ace-git-commit --only-staged`
48
+ - Dry run first: `ace-git-commit --dry-run -i "$intention"`
49
+
50
+ 3. **Verify result**:
51
+ ```bash
52
+ ace-git status
53
+ ```
54
+
55
+ ## Options Reference
56
+
57
+ - `-i, --intention`: Provide context for better messages
58
+ - `-m, --message`: Use direct message (bypass LLM)
59
+ - `--model MODEL`: Override LLM model (e.g., gflash)
60
+ - `-s, --only-staged`: Commit only staged changes
61
+ - `-n, --dry-run`: Preview without committing
62
+ - `-d, --debug`: Enable debug output
63
+
64
+ ## Success Criteria
65
+
66
+ - Commit created with conventional format
67
+ - Only intended changes included
68
+ - Working directory in expected state
69
+
70
+ ## Response Template
71
+
72
+ **Changes Committed:** [Brief summary of what was committed]
73
+ **Commit Message:** [The generated/used message]
74
+ **Files Modified:** [Number of files and brief description]
75
+ **Status:** ✓ Complete | ✗ Failed with [reason]
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/git"
4
+
5
+ module Ace
6
+ module GitCommit
7
+ module Atoms
8
+ # GitExecutor handles low-level git command execution
9
+ # Delegates to ace-git for command execution
10
+ class GitExecutor
11
+ # Execute a git command and return output
12
+ # @param args [Array<String>] Git command arguments
13
+ # @param capture_stderr [Boolean] Whether to capture stderr (ignored, always captured)
14
+ # @return [String] Command output
15
+ # @raise [GitError] If command fails
16
+ def execute(*args, capture_stderr: false)
17
+ cmd = ["git"] + args
18
+ result = Ace::Git::Atoms::CommandExecutor.execute(*cmd)
19
+
20
+ unless result[:success]
21
+ error_msg = "Git command failed: #{cmd.join(" ")}"
22
+ error_msg += "\nError: #{result[:error]}" if result[:error] && !result[:error].empty?
23
+ raise GitError, error_msg
24
+ end
25
+
26
+ # Combine output and error if capture_stderr is true
27
+ if capture_stderr && result[:error] && !result[:error].empty?
28
+ result[:output] + result[:error]
29
+ else
30
+ result[:output]
31
+ end
32
+ end
33
+
34
+ # Check if we're in a git repository
35
+ # @return [Boolean] True if in a git repo
36
+ def in_repository?
37
+ Ace::Git::Atoms::CommandExecutor.in_git_repo?
38
+ end
39
+
40
+ # Get repository root
41
+ # @return [String] Repository root path
42
+ def repository_root
43
+ Ace::Git::Atoms::CommandExecutor.repo_root
44
+ end
45
+
46
+ # Check if there are any changes
47
+ # @return [Boolean] True if there are changes
48
+ def has_changes?
49
+ Ace::Git::Atoms::CommandExecutor.has_unstaged_changes? ||
50
+ Ace::Git::Atoms::CommandExecutor.has_staged_changes? ||
51
+ Ace::Git::Atoms::CommandExecutor.has_untracked_changes?
52
+ end
53
+
54
+ # Check if there are staged changes
55
+ # @return [Boolean] True if there are staged changes
56
+ def has_staged_changes?
57
+ Ace::Git::Atoms::CommandExecutor.has_staged_changes?
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module GitCommit
5
+ module Atoms
6
+ # GitignoreChecker detects files that match gitignore patterns
7
+ # Uses git check-ignore to determine if paths are ignored
8
+ class GitignoreChecker
9
+ # Check if a single file/path is gitignored
10
+ # @param path [String] File or directory path to check
11
+ # @param git_executor [GitExecutor] Git executor instance
12
+ # @return [Boolean] True if path is gitignored
13
+ def ignored?(path, git_executor)
14
+ result = check_ignore(path, git_executor)
15
+ result[:ignored]
16
+ end
17
+
18
+ # Check if a file is tracked in git (exists in the index)
19
+ # @param path [String] File path to check
20
+ # @param git_executor [GitExecutor] Git executor instance
21
+ # @return [Boolean] True if file is tracked
22
+ def tracked?(path, git_executor)
23
+ # git ls-files returns the path if it's tracked, empty if not
24
+ result = git_executor.execute("ls-files", "--error-unmatch", path)
25
+ !result.nil? && !result.strip.empty?
26
+ rescue GitError
27
+ false
28
+ end
29
+
30
+ # Categorize paths into: valid (not gitignored), force_add (gitignored but tracked), skipped (gitignored and untracked)
31
+ # @param paths [Array<String>] Paths to check
32
+ # @param git_executor [GitExecutor] Git executor instance
33
+ # @return [Hash] {:valid => [...], :force_add => [...], :skipped => [...]}
34
+ # - :valid - paths that are NOT gitignored
35
+ # - :force_add - paths that ARE gitignored but are tracked in git (use git add -f)
36
+ # - :skipped - paths that ARE gitignored and NOT tracked (skip these)
37
+ def categorize_paths(paths, git_executor)
38
+ return {valid: [], force_add: [], skipped: []} if paths.nil? || paths.empty?
39
+
40
+ valid = []
41
+ force_add = []
42
+ skipped = []
43
+
44
+ paths.each do |path|
45
+ result = check_ignore(path, git_executor)
46
+ if result[:ignored]
47
+ # Path matches gitignore - check if it's tracked
48
+ if tracked?(path, git_executor)
49
+ # Tracked file in gitignored location - force add it
50
+ force_add << {path: path, pattern: result[:pattern]}
51
+ else
52
+ # Untracked and gitignored - skip it
53
+ skipped << {path: path, pattern: result[:pattern]}
54
+ end
55
+ else
56
+ valid << path
57
+ end
58
+ end
59
+
60
+ {valid: valid, force_add: force_add, skipped: skipped}
61
+ end
62
+
63
+ # Legacy method for backward compatibility
64
+ # @param paths [Array<String>] Paths to check
65
+ # @param git_executor [GitExecutor] Git executor instance
66
+ # @return [Hash] {:valid => [...], :ignored => [...]}
67
+ def filter_ignored(paths, git_executor)
68
+ result = categorize_paths(paths, git_executor)
69
+ {
70
+ valid: result[:valid] + result[:force_add].map { |f| f[:path] },
71
+ ignored: result[:skipped]
72
+ }
73
+ end
74
+
75
+ private
76
+
77
+ # Check a single path with git check-ignore
78
+ # @param path [String] Path to check
79
+ # @param git_executor [GitExecutor] Git executor instance
80
+ # @return [Hash] {:ignored => Boolean, :pattern => String|nil}
81
+ def check_ignore(path, git_executor)
82
+ # git check-ignore returns:
83
+ # - exit 0 and the matching pattern if path IS ignored
84
+ # - exit 1 (non-zero) if path is NOT ignored
85
+ # Use -v to get verbose output including the pattern that matched
86
+ cmd = ["check-ignore", "-v", path]
87
+
88
+ begin
89
+ output = git_executor.execute(*cmd)
90
+ # If we get here, the file IS ignored
91
+ # Output format: "<pattern>:<line>:<source>:<path>"
92
+ pattern = extract_pattern(output)
93
+ {ignored: true, pattern: pattern}
94
+ rescue GitError
95
+ # If command fails (exit 1), file is NOT ignored
96
+ {ignored: false, pattern: nil}
97
+ end
98
+ end
99
+
100
+ # Extract the gitignore pattern from check-ignore -v output
101
+ # @param output [String] Output from git check-ignore -v
102
+ # @return [String, nil] The pattern that matched, or nil
103
+ def extract_pattern(output)
104
+ return nil if output.nil? || output.strip.empty?
105
+
106
+ # Format: ".gitignore:3:.ace-taskflow/**/reviews/ .ace-taskflow/v.0.9.0/reviews/review-report-gpro.md"
107
+ # We want the third field (the pattern)
108
+ parts = output.strip.split("\t")
109
+ if parts.length >= 2
110
+ # Pattern is in the third colon-separated field of the first part
111
+ source_parts = parts[0].split(":")
112
+ source_parts[2] if source_parts.length >= 3
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module GitCommit
5
+ module CLI
6
+ module Commands
7
+ # ace-support-cli Command class for the commit command
8
+ #
9
+ # This command generates and executes git commits with LLM-generated
10
+ # or user-provided messages, maintaining complete parity with the
11
+ # Thor implementation.
12
+ class Commit < Ace::Support::Cli::Command
13
+ include Ace::Support::Cli::Base
14
+
15
+ desc <<~DESC.strip
16
+ Generate and execute git commit
17
+
18
+ Generate a commit message using LLM or use a provided message,
19
+ then stage files and commit.
20
+
21
+ When no files are specified, all changes are staged.
22
+ When files are provided, only those files are staged and committed.
23
+
24
+ Configuration:
25
+ Global config: ~/.ace/git/commit.yml
26
+ Project config: .ace/git/commit.yml
27
+ Package config: {package}/.ace/git/commit.yml
28
+ DESC
29
+
30
+ example [
31
+ " # Commit all changes",
32
+ "src/auth.rb # Commit specific file",
33
+ "-i 'fix bug' # With intention",
34
+ "-m 'feat: add' # With explicit message",
35
+ "--only-staged # Only staged changes",
36
+ "--no-split # Force a single commit"
37
+ ]
38
+
39
+ # Define files as variadic argument (can be 0 or more)
40
+ argument :files, required: false, type: :array, desc: "Files to commit"
41
+
42
+ # Commit options
43
+ option :intention, type: :string, aliases: %w[-i], desc: "Provide context for LLM message generation"
44
+ option :message, type: :string, aliases: %w[-m], desc: "Use provided message directly (no LLM)"
45
+ option :model, type: :string, desc: "Override default LLM model (e.g., glite, gflash)"
46
+ option :only_staged, type: :boolean, aliases: %w[-s], desc: "Commit only currently staged changes"
47
+ option :staged, type: :boolean, desc: "Alias for --only-staged"
48
+ option :dry_run, type: :boolean, aliases: %w[-n], desc: "Show what would be committed without doing it"
49
+ option :force, type: :boolean, aliases: %w[-f], desc: "Force operation (for future use)"
50
+ option :no_split, type: :boolean, desc: "Force a single commit even when multiple config scopes are detected"
51
+
52
+ # Standard options (inherited from Base but need explicit definition for ace-support-cli)
53
+ option :version, type: :boolean, desc: "Show version information"
54
+ option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
55
+ option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
56
+ option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
57
+
58
+ def call(**options)
59
+ # Extract files array from options (ace-support-cli passes args as :files)
60
+ @files = Array(options[:files] || [])
61
+
62
+ # Remove ace-support-cli specific keys (args is leftover arguments)
63
+ @options = options.reject { |k, _| k == :files || k == :args }
64
+ if @options[:version]
65
+ puts "ace-git-commit #{Ace::GitCommit::VERSION}"
66
+ return 0
67
+ end
68
+
69
+ execute
70
+ end
71
+
72
+ private
73
+
74
+ def execute
75
+ display_config_summary
76
+
77
+ orchestrator = Organisms::CommitOrchestrator.new
78
+ success = orchestrator.execute(commit_options)
79
+
80
+ raise Ace::Support::Cli::Error.new("Commit failed") unless success
81
+ rescue GitError => e
82
+ raise Ace::Support::Cli::Error.new(e.message)
83
+ rescue Interrupt
84
+ raise Ace::Support::Cli::Error.new("Commit cancelled", exit_code: 130)
85
+ end
86
+
87
+ def display_config_summary
88
+ return if @options[:quiet]
89
+
90
+ require "ace/core"
91
+ Ace::Core::Atoms::ConfigSummary.display(
92
+ command: "commit",
93
+ config: load_effective_config,
94
+ defaults: default_config,
95
+ options: @options,
96
+ quiet: false # Don't suppress ConfigSummary itself
97
+ )
98
+ end
99
+
100
+ def commit_options
101
+ Models::CommitOptions.new(
102
+ intention: @options[:intention],
103
+ message: @options[:message],
104
+ model: @options[:model],
105
+ files: @files,
106
+ only_staged: @options[:only_staged] || @options[:staged] || false,
107
+ dry_run: @options[:dry_run] || false,
108
+ debug: @options[:debug] || false,
109
+ force: @options[:force] || false,
110
+ verbose: @options[:verbose] != false, # Default true
111
+ quiet: @options[:quiet] || false,
112
+ no_split: @options[:no_split] || false
113
+ )
114
+ end
115
+
116
+ def load_effective_config
117
+ gem_root = Gem.loaded_specs["ace-git-commit"]&.gem_dir ||
118
+ File.expand_path("../../../../../..", __dir__)
119
+
120
+ resolver = Ace::Support::Config.create(
121
+ config_dir: ".ace",
122
+ defaults_dir: ".ace-defaults",
123
+ gem_path: gem_root
124
+ )
125
+
126
+ config = resolver.resolve_namespace("git", filename: "commit")
127
+ config.data["git"] || config.data
128
+ end
129
+
130
+ def default_config
131
+ gem_root = Gem.loaded_specs["ace-git-commit"]&.gem_dir ||
132
+ File.expand_path("../../../../../..", __dir__)
133
+
134
+ defaults_path = File.join(gem_root, ".ace-defaults", "git", "commit.yml")
135
+
136
+ if File.exist?(defaults_path)
137
+ require "yaml"
138
+ YAML.safe_load_file(defaults_path, permitted_classes: [Symbol], aliases: true) || {}
139
+ else
140
+ {}
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require_relative "../git_commit"
5
+ # Commands
6
+ require_relative "cli/commands/commit"
7
+
8
+ module Ace
9
+ module GitCommit
10
+ # CLI namespace for ace-git-commit command loading.
11
+ #
12
+ # ace-git-commit now uses a single-command ace-support-cli entrypoint that calls
13
+ # CLI::Commands::Commit directly from the executable.
14
+ module CLI
15
+ # Entry point for CLI invocation (used by tests via cli_helpers)
16
+ #
17
+ # @param args [Array<String>] Command-line arguments
18
+ def self.start(args)
19
+ Ace::Support::Cli::Runner.new(Commands::Commit).call(args: args)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "digest"
5
+
6
+ module Ace
7
+ module GitCommit
8
+ module Models
9
+ # CommitGroup represents a group of files that share the same effective config
10
+ class CommitGroup
11
+ attr_reader :scope_name, :source, :config, :files
12
+
13
+ def initialize(scope_name:, source:, config:, files: [])
14
+ @scope_name = scope_name
15
+ @source = source
16
+ @config = config || {}
17
+ @files = Array(files)
18
+ end
19
+
20
+ def add_file(file)
21
+ @files << file
22
+ self
23
+ end
24
+
25
+ def file_count
26
+ @files.length
27
+ end
28
+
29
+ def config_signature
30
+ self.class.signature_for(@config)
31
+ end
32
+
33
+ def self.signature_for(config)
34
+ normalized = normalize_config(config || {})
35
+ Digest::SHA256.hexdigest(JSON.generate(normalized))
36
+ end
37
+
38
+ def self.normalize_config(value)
39
+ case value
40
+ when Hash
41
+ value.keys.sort.each_with_object({}) do |key, acc|
42
+ acc[key.to_s] = normalize_config(value[key])
43
+ end
44
+ when Array
45
+ value.map { |item| normalize_config(item) }
46
+ else
47
+ value
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end