Files
gh-duongdev-ccpm/commands/commit.md
2025-11-29 18:24:24 +08:00

10 KiB
Raw Blame History

description, allowed-tools, argument-hint
description allowed-tools argument-hint
Smart git commit with Linear integration and conventional commits
Bash
LinearMCP
[issue-id] [message]

Smart Commit Command

You are executing the smart git commit command that integrates with Linear and follows conventional commits format.

🚨 CRITICAL: Safety Rules

READ FIRST: $CCPM_COMMANDS_DIR/SAFETY_RULES.md

This command performs git operations which are local and safe. No external PM system writes.

Conventional Commits Format

This command follows the Conventional Commits specification:

<type>(<scope>): <description>

[optional body]

[optional footer(s)]

Types:

  • feat: New feature
  • fix: Bug fix
  • docs: Documentation changes
  • style: Code style changes (formatting, etc.)
  • refactor: Code refactoring
  • test: Adding or updating tests
  • chore: Maintenance tasks

Implementation

Step 1: Determine Issue ID

const args = process.argv.slice(2)
let issueId = args[0]
let userMessage = args[1]

const ISSUE_ID_PATTERN = /^[A-Z]+-\d+$/

// If first arg doesn't look like issue ID, it might be the message
if (args[0] && !ISSUE_ID_PATTERN.test(args[0])) {
  userMessage = args[0]
  issueId = null
}

// Try to detect issue ID from git branch
if (!issueId) {
  try {
    const branch = execSync('git rev-parse --abbrev-ref HEAD', {
      encoding: 'utf-8'
    }).trim()

    const branchMatch = branch.match(/([A-Z]+-\d+)/)
    if (branchMatch) {
      issueId = branchMatch[1]
      console.log(`🔍 Detected issue from branch: ${issueId}`)
    }
  } catch (error) {
    // Not in a git repo or branch detection failed
    console.log("  Could not detect issue from branch")
  }
}

Step 2: Check for Uncommitted Changes

# Get status
git status --porcelain

# Check if there are changes to commit
if [ -z "$(git status --porcelain)" ]; then
  echo "✅ No changes to commit (working tree clean)"
  exit 0
fi

Step 3: Show Changes Summary

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📝 Smart Commit Command
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

${issueId ? `📋 Issue: ${issueId}` : ''}

📊 Changes to commit:
────────────────────

${changedFiles.map((file, i) => `  ${i+1}. ${file.status} ${file.path}`).join('\n')}

📈 Total: ${changedFiles.length} file(s) changed

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Step 4: Fetch Issue Context (if Issue ID available)

If issue ID is available, get context from Linear:

let issueTitle = null
let issueType = null

if (issueId) {
  try {
    const issue = await linear_get_issue(issueId)
    issueTitle = issue.title
    issueType = detectIssueType(issue)

    console.log(`📋 Issue: ${issueId} - ${issueTitle}`)
    console.log("")
  } catch (error) {
    console.log(`⚠️  Could not fetch issue ${issueId} from Linear`)
    console.log("   Proceeding without issue context")
  }
}

Step 5: Analyze Changes and Determine Commit Type

function analyzeChanges(changedFiles) {
  const analysis = {
    hasTests: false,
    hasSource: false,
    hasDocs: false,
    hasConfig: false,
    newFiles: 0,
    modifiedFiles: 0
  }

  changedFiles.forEach(file => {
    if (file.status === 'A' || file.status === '??') {
      analysis.newFiles++
    } else if (file.status === 'M') {
      analysis.modifiedFiles++
    }

    if (file.path.includes('test') || file.path.includes('spec')) {
      analysis.hasTests = true
    } else if (file.path.includes('src/') || file.path.includes('lib/')) {
      analysis.hasSource = true
    } else if (file.path.match(/\.(md|txt)$/)) {
      analysis.hasDocs = true
    } else if (file.path.match(/\.(config|json|yaml|yml)$/)) {
      analysis.hasConfig = true
    }
  })

  return analysis
}

function suggestCommitType(analysis, issueType) {
  // Priority order for determining type
  if (issueType === 'bug') return 'fix'
  if (issueType === 'feature') return 'feat'

  // Infer from changes
  if (analysis.hasSource && analysis.newFiles > 0) return 'feat'
  if (analysis.hasSource && analysis.modifiedFiles > 0) {
    // Could be feat, fix, or refactor - let user choose
    return 'feat' // default to feat
  }
  if (analysis.hasTests && !analysis.hasSource) return 'test'
  if (analysis.hasDocs && !analysis.hasSource) return 'docs'
  if (analysis.hasConfig) return 'chore'

  return 'feat' // default
}

Step 6: Generate or Collect Commit Message

let commitType, commitScope, commitDescription

if (userMessage) {
  // User provided message, parse or use as-is
  const conventionalMatch = userMessage.match(/^(\w+)(\([\w-]+\))?: (.+)$/)

  if (conventionalMatch) {
    // Already in conventional format
    commitType = conventionalMatch[1]
    commitScope = conventionalMatch[2]?.slice(1, -1) // Remove parens
    commitDescription = conventionalMatch[3]
  } else {
    // Plain message, add conventional format
    commitType = suggestCommitType(analysis, issueType)
    commitScope = issueId ? issueId : null
    commitDescription = userMessage
  }
} else {
  // Auto-generate from context
  commitType = suggestCommitType(analysis, issueType)
  commitScope = issueId ? issueId : null

  // Generate description
  if (issueTitle) {
    commitDescription = issueTitle
  } else {
    // Generate from file changes
    commitDescription = generateDescriptionFromChanges(analysis, changedFiles)
  }
}

Step 7: Display Proposed Commit Message

💬 Proposed Commit Message:
───────────────────────────

${commitType}${commitScope ? `(${commitScope})` : ''}: ${commitDescription}

${issueId ? `
Related to: ${issueId}
${issueTitle ? `Issue: ${issueTitle}` : ''}
` : ''}

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Step 8: Confirm and Commit

Use AskUserQuestion to confirm:

{
  questions: [{
    question: "Proceed with this commit?",
    header: "Confirm",
    multiSelect: false,
    options: [
      {
        label: "Yes, commit",
        description: "Create commit with this message"
      },
      {
        label: "Edit message",
        description: "Let me modify the commit message"
      },
      {
        label: "Cancel",
        description: "Don't commit, go back"
      }
    ]
  }]
}

If "Yes, commit":

# Stage all changes
git add .

# Create commit with conventional format
git commit -m "${commitType}${commitScope ? `(${commitScope})` : ''}: ${commitDescription}" \
  ${issueId ? `-m "Related to: ${issueId}"` : ''} \
  ${issueTitle ? `-m "${issueTitle}"` : ''}

echo "✅ Commit created successfully!"
echo ""
echo "Commit: $(git log -1 --oneline)"
echo ""
echo "Next steps:"
echo "  /ccpm:sync        # Sync progress to Linear"
echo "  /ccpm:work        # Continue working"
echo "  git push          # Push to remote"

If "Edit message":

Please provide your commit message (conventional format preferred):

Format: <type>(<scope>): <description>
Examples:
  - feat(auth): add JWT token validation
  - fix(PSN-27): resolve login button click handler
  - docs: update API documentation

Your message:
> [User input]

Then repeat confirmation.

Helper Functions

Detect Issue Type

function detectIssueType(issue) {
  const title = issue.title.toLowerCase()
  const labels = issue.labels || []

  // Check labels first
  if (labels.includes('bug') || labels.includes('fix')) return 'bug'
  if (labels.includes('feature') || labels.includes('enhancement')) return 'feature'

  // Check title keywords
  if (title.includes('fix') || title.includes('bug')) return 'bug'
  if (title.includes('add') || title.includes('implement')) return 'feature'

  return 'feature' // default
}

Generate Description from Changes

function generateDescriptionFromChanges(analysis, changedFiles) {
  if (analysis.newFiles > 0 && analysis.hasSource) {
    const mainFile = changedFiles.find(f => f.status === 'A' && f.path.includes('src/'))
    if (mainFile) {
      const fileName = mainFile.path.split('/').pop().replace(/\.(ts|js|tsx|jsx)$/, '')
      return `add ${fileName} module`
    }
    return `add new feature components`
  }

  if (analysis.modifiedFiles > 0 && analysis.hasSource) {
    return `update implementation`
  }

  if (analysis.hasTests) {
    return `add tests`
  }

  if (analysis.hasDocs) {
    return `update documentation`
  }

  return `update files`
}

Examples

Example 1: Commit with Auto-Detection

git checkout -b duongdev/PSN-27-add-auth
# ... make changes ...
/ccpm:commit

Detection: PSN-27 from branch, fetches issue title from Linear Generated: feat(PSN-27): Add user authentication Result: Conventional commit created with Linear link

Example 2: Commit with Custom Message

/ccpm:commit PSN-27 "Completed JWT token validation"

Result: feat(PSN-27): Completed JWT token validation

Example 3: Commit with Full Conventional Format

/ccpm:commit "fix(auth): resolve login button handler"

Result: Uses provided conventional format as-is

Example 4: Commit Without Issue ID

/ccpm:commit "update documentation"

Result: docs: update documentation

Benefits

Conventional Commits: Automatic format following best practices Linear Integration: Links commits to issues automatically Smart Detection: Auto-detects commit type from changes Auto-Generation: Creates meaningful messages from context Git Integration: Built into workflow (no context switching) Change Summary: Shows what's being committed before confirming

Migration Hint

This is a NEW command that integrates git commits into CCPM workflow:

  • Replaces manual git add . && git commit -m "message"
  • Automatically follows conventional commits format
  • Links commits to Linear issues
  • Part of natural workflow (plan → work → commit → sync → verify → done)