Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Rename this file to .env and fill in the values
CLIENT_ID=
CLIENT_ID=
ANTHROPIC_API_KEY=
1 change: 1 addition & 0 deletions .husky/commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npx --no -- commitlint --edit $1
125 changes: 125 additions & 0 deletions .husky/prepare-commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#!/bin/bash

# .husky/prepare-commit-msg

if ! command -v jq &> /dev/null; then
echo "Warning: jq is required for AI commit message generation but is not installed" >&2
exit 0
fi

# Load environment variables from .env file if it exists
if [ -f .env ]; then
set -a
source .env
set +a
fi

Comment on lines +10 to +16

Copilot AI Dec 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Loading environment variables directly from a .env file in a git hook could expose sensitive information if the .env file is accidentally committed. Consider adding .env to .gitignore if not already present, or use a more secure method like direnv (as mentioned in the PR description) which keeps environment variables separate from the repository.

Suggested change
# Load environment variables from .env file if it exists
if [ -f .env ]; then
set -a
source .env
set +a
fi
# Environment variables should be managed securely (e.g., with direnv).
# Do NOT source .env files directly in git hooks to avoid leaking secrets.
# See project documentation for recommended environment management.

Copilot uses AI. Check for mistakes.
COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2

# Only generate for normal commits (not merges, squashes, etc.)
if [ -n "$COMMIT_SOURCE" ]; then
exit 0
fi

# Get the staged diff

Copilot AI Dec 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a comment explaining why --no-color is used in the git diff command. While it's clear to experienced developers that this prevents ANSI color codes from being included in the API request, documenting this helps with maintainability.

Suggested change
# Get the staged diff
# Get the staged diff
# Use --no-color to prevent ANSI color codes from being included in the diff output,
# which ensures clean input for API requests and downstream processing.

Copilot uses AI. Check for mistakes.
DIFF=$(git diff --cached --no-color)

# Skip if no changes staged
if [ -z "$DIFF" ]; then
exit 0
fi

# Get list of changed files for context
FILES=$(git diff --cached --name-only)

# Truncate very large diffs to avoid token limits

Copilot AI Dec 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The MAX_CHARS value of 8000 is a magic number without explanation. Given that Claude API has specific token limits (and the request sets max_tokens to 512 for the response), it would be helpful to document why 8000 characters was chosen and how it relates to the model's input token limits.

Consider adding a comment:

# Truncate very large diffs to avoid token limits
# Claude models typically support ~200k tokens input; 8000 chars ≈ 2000 tokens
MAX_CHARS=8000
Suggested change
# Truncate very large diffs to avoid token limits
# Truncate very large diffs to avoid token limits
# Claude models typically support ~200k tokens input; 8000 chars ≈ 2000 tokens

Copilot uses AI. Check for mistakes.
MAX_CHARS=8000

Copilot AI Dec 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded MAX_CHARS value of 8000 could be made more maintainable by defining it as a configurable constant at the top of the script or documenting why this specific value was chosen. This would make it easier to adjust based on actual token limits and API constraints.

Copilot uses AI. Check for mistakes.
if [ ${#DIFF} -gt $MAX_CHARS ]; then
DIFF="${DIFF:0:$MAX_CHARS}... [truncated]"
Comment on lines +37 to +39

Copilot AI Dec 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The diff truncation logic uses bash-specific string length syntax ${#DIFF} which may not work in all POSIX-compliant shells, even though the shebang specifies bash. Additionally, the truncation is done on character count rather than byte count, which could lead to issues with the API's token limit calculation.

While the shebang correctly specifies #!/bin/bash, consider documenting this dependency or using more portable POSIX syntax if cross-shell compatibility is desired. Alternatively, consider truncating based on line count or using a more sophisticated tokenization approach that respects code boundaries.

Suggested change
MAX_CHARS=8000
if [ ${#DIFF} -gt $MAX_CHARS ]; then
DIFF="${DIFF:0:$MAX_CHARS}... [truncated]"
# Note: This script requires Bash (see shebang above). To avoid issues with multi-byte characters and API token limits,
# we truncate the diff by byte count using head -c, which is POSIX-compliant and more predictable.
MAX_BYTES=8000
if [ "$(printf "%s" "$DIFF" | wc -c)" -gt "$MAX_BYTES" ]; then
DIFF="$(printf "%s" "$DIFF" | head -c "$MAX_BYTES")... [truncated]"

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +39

Copilot AI Dec 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string truncation using bash substring expansion doesn't account for potentially cutting in the middle of a multi-byte UTF-8 character, which could corrupt the diff content sent to the API. While this may work in many cases, consider using a more robust truncation method or at least documenting this limitation.

Suggested change
# Truncate very large diffs to avoid token limits
MAX_CHARS=8000
if [ ${#DIFF} -gt $MAX_CHARS ]; then
DIFF="${DIFF:0:$MAX_CHARS}... [truncated]"
# Truncate very large diffs to avoid token limits, ensuring we do not split multi-byte UTF-8 characters.
MAX_CHARS=8000
if [ $(printf "%s" "$DIFF" | wc -c) -gt $MAX_CHARS ]; then
# Use head -c to truncate by bytes, then iconv -c to remove incomplete UTF-8 sequences.
DIFF="$(printf "%s" "$DIFF" | head -c $MAX_CHARS | iconv -c -f utf-8 -t utf-8)... [truncated]"

Copilot uses AI. Check for mistakes.
fi

# Build the Commitizen-style prompt
read -r -d '' PROMPT << 'EOF'
Generate a Git commit message following the Commitizen conventional commit format.

FORMAT:
<type>(<scope>): <subject>

<body>

<footer>

TYPES (required):
- feat: A new feature
- fix: A bug fix
- docs: Documentation only changes
- style: Code style changes (formatting, semicolons, etc.)
- refactor: Code change that neither fixes a bug nor adds a feature
- perf: Performance improvement
- test: Adding or correcting tests
- build: Changes to build system or dependencies
- ci: Changes to CI configuration
- chore: Other changes that don't modify src or test files
- revert: Reverts a previous commit

RULES:
1. Type is required, scope is optional but encouraged
2. Subject: imperative mood, lowercase, no period, max 50 chars
3. Body: optional, wrap at 72 chars, explain what and why
4. Footer: optional, reference issues like "Closes #123"
5. Breaking changes: add "!" after type/scope and explain in footer

Output ONLY the commit message, no explanations or markdown.

EOF

if [ -z "$ANTHROPIC_API_KEY" ]; then
echo "Warning: ANTHROPIC_API_KEY not set, skipping AI commit message generation" >&2
exit 0
fi

PROMPT="$PROMPT

Changed files:
$FILES

Diff:
$DIFF"

# Call Claude API
RESPONSE=$(curl -s https://api.anthropic.com/v1/messages \
-H "Content-Type: application/json" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-d "$(jq -n \
--arg prompt "$PROMPT" \
'{
model: "claude-sonnet-4-5-20250929",
max_tokens: 512,
messages: [{role: "user", content: $prompt}]
}')")
Comment on lines +91 to +101

Copilot AI Dec 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The curl command lacks timeout settings, which could cause the git commit process to hang indefinitely if the API is slow or unresponsive. Consider adding a timeout using the --max-time flag (e.g., --max-time 30) to ensure the hook doesn't block the commit process for too long.

Copilot uses AI. Check for mistakes.

# Extract the message text from the API response
MESSAGE=$(echo "$RESPONSE" | jq -r '.content[0].text // empty')
Comment thread
ikem-legend marked this conversation as resolved.
Comment thread
ikem-legend marked this conversation as resolved.

# Check if extraction succeeded
if [ -z "$MESSAGE" ]; then
echo "Warning: Failed to generate commit message from Claude API" >&2

Copilot AI Dec 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message "Failed to generate commit message from Claude API" doesn't provide sufficient context for debugging. Consider including more details such as the HTTP status code or a hint about checking the API key validity, especially since this could fail for multiple reasons (network issues, invalid API key, rate limiting, etc.).

Copilot uses AI. Check for mistakes.
# Check for error in RESPONSE
ERROR=$(echo "$RESPONSE" | jq -r '.error.message // empty')
if [ -n "$ERROR" ]; then
echo "API Error: $ERROR" >&2
fi
exit 0
fi

if [ -n "$MESSAGE" ]; then
# Write generated message, keep any existing content below
echo "$MESSAGE" > "$COMMIT_MSG_FILE.tmp"
echo "" >> "$COMMIT_MSG_FILE.tmp"
echo "# --- Generated by Claude (Commitizen format) ---" >> "$COMMIT_MSG_FILE.tmp"
echo "# Types: feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert" >> "$COMMIT_MSG_FILE.tmp"
cat "$COMMIT_MSG_FILE" >> "$COMMIT_MSG_FILE.tmp"
mv "$COMMIT_MSG_FILE.tmp" "$COMMIT_MSG_FILE"
fi
Comment on lines +117 to +125

Copilot AI Dec 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition is redundant because MESSAGE is already checked for emptiness at line 107, and the script exits if MESSAGE is empty. This if statement will always evaluate to true at this point. Consider removing this redundant check or restructuring the logic if there's a case where MESSAGE could become empty between lines 107 and 117.

Suggested change
if [ -n "$MESSAGE" ]; then
# Write generated message, keep any existing content below
echo "$MESSAGE" > "$COMMIT_MSG_FILE.tmp"
echo "" >> "$COMMIT_MSG_FILE.tmp"
echo "# --- Generated by Claude (Commitizen format) ---" >> "$COMMIT_MSG_FILE.tmp"
echo "# Types: feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert" >> "$COMMIT_MSG_FILE.tmp"
cat "$COMMIT_MSG_FILE" >> "$COMMIT_MSG_FILE.tmp"
mv "$COMMIT_MSG_FILE.tmp" "$COMMIT_MSG_FILE"
fi
# Write generated message, keep any existing content below
echo "$MESSAGE" > "$COMMIT_MSG_FILE.tmp"
echo "" >> "$COMMIT_MSG_FILE.tmp"
echo "# --- Generated by Claude (Commitizen format) ---" >> "$COMMIT_MSG_FILE.tmp"
echo "# Types: feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert" >> "$COMMIT_MSG_FILE.tmp"
cat "$COMMIT_MSG_FILE" >> "$COMMIT_MSG_FILE.tmp"
mv "$COMMIT_MSG_FILE.tmp" "$COMMIT_MSG_FILE"

Copilot uses AI. Check for mistakes.
1 change: 1 addition & 0 deletions commitlint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default { extends: ['@commitlint/config-conventional'] };