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.
- checksums.yaml +7 -0
- data/.ace-defaults/git/commit.yml +22 -0
- data/.ace-defaults/nav/protocols/guide-sources/ace-git-commit.yml +10 -0
- data/.ace-defaults/nav/protocols/prompt-sources/ace-git-commit.yml +19 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-git-commit.yml +19 -0
- data/CHANGELOG.md +404 -0
- data/COMPARISON.md +176 -0
- data/LICENSE +21 -0
- data/README.md +44 -0
- data/Rakefile +14 -0
- data/exe/ace-git-commit +13 -0
- data/handbook/guides/version-control-system-message.g.md +507 -0
- data/handbook/prompts/git-commit.md +22 -0
- data/handbook/prompts/git-commit.system.md +150 -0
- data/handbook/skills/as-git-commit/SKILL.md +57 -0
- data/handbook/workflow-instructions/git/commit.wf.md +75 -0
- data/lib/ace/git_commit/atoms/git_executor.rb +62 -0
- data/lib/ace/git_commit/atoms/gitignore_checker.rb +118 -0
- data/lib/ace/git_commit/cli/commands/commit.rb +147 -0
- data/lib/ace/git_commit/cli.rb +23 -0
- data/lib/ace/git_commit/models/commit_group.rb +53 -0
- data/lib/ace/git_commit/models/commit_options.rb +75 -0
- data/lib/ace/git_commit/models/split_commit_result.rb +60 -0
- data/lib/ace/git_commit/models/stage_result.rb +71 -0
- data/lib/ace/git_commit/molecules/commit_grouper.rb +123 -0
- data/lib/ace/git_commit/molecules/commit_summarizer.rb +43 -0
- data/lib/ace/git_commit/molecules/diff_analyzer.rb +111 -0
- data/lib/ace/git_commit/molecules/file_stager.rb +153 -0
- data/lib/ace/git_commit/molecules/message_generator.rb +438 -0
- data/lib/ace/git_commit/molecules/path_resolver.rb +365 -0
- data/lib/ace/git_commit/molecules/split_commit_executor.rb +272 -0
- data/lib/ace/git_commit/organisms/commit_orchestrator.rb +330 -0
- data/lib/ace/git_commit/version.rb +7 -0
- data/lib/ace/git_commit.rb +41 -0
- 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
|