Skip to content
Open
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
1 change: 1 addition & 0 deletions src/cli/commands/commit.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export function createCommitCommand({ config }) {
model: config.get('model'),
language: config.get('language'),
prefixEnabled: config.get('prefixEnabled'),
allowEmojis: config.get('allowEmojis'),
},
)

Expand Down
35 changes: 35 additions & 0 deletions src/cli/commands/emoji.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { select } from '../../services/prompt.js'

/**
* Create the emoji toggle command handler
* @param {object} deps - Dependencies
* @param {import('../../config/index.js').ConfigManager} deps.config - Config manager
* @returns {Function} The command handler
*/
export function createEmojiCommand({ config }) {
return async function emojiAction() {
const currentEnabled = config.get('allowEmojis')

console.log(
`Emojis are currently ${currentEnabled ? 'enabled' : 'disabled'}.`,
)

const choices = [
{ title: 'Enable emojis 🎉', value: true },
{ title: 'Disable emojis', value: false },
]

const selectedValue = await select(
'Allow emojis in commit messages?',
choices,
currentEnabled ? 0 : 1,
)

if (selectedValue !== undefined) {
config.set('allowEmojis', selectedValue)
console.log(
`Emojis ${selectedValue ? 'enabled' : 'disabled'} and saved to configuration`,
)
}
}
}
1 change: 1 addition & 0 deletions src/cli/commands/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export { createCommitCommand } from './commit.js'
export { createModelCommand } from './model.js'
export { createLangCommand } from './lang.js'
export { createPrefixCommand } from './prefix.js'
export { createEmojiCommand } from './emoji.js'
export { createApiKeyCommand } from './apiKey.js'
export { createConfigCommand } from './config.js'
6 changes: 6 additions & 0 deletions src/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
createModelCommand,
createLangCommand,
createPrefixCommand,
createEmojiCommand,
createApiKeyCommand,
createConfigCommand,
} from './commands/index.js'
Expand Down Expand Up @@ -49,6 +50,11 @@ export function setupCli(options = {}) {
.description('Toggle commit message prefix (e.g., fix:, feat:, refactor:)')
.action(createPrefixCommand(deps))

program
.command('emoji')
.description('Toggle emoji support in commit messages')
.action(createEmojiCommand(deps))

program
.command('open-api-key')
.description('Manage your OpenAI API key')
Expand Down
1 change: 1 addition & 0 deletions src/config/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const defaults = {
model: 'gpt-4o-mini',
language: 'English',
prefixEnabled: true,
allowEmojis: true,
apiKey: null,
}

Expand Down
5 changes: 3 additions & 2 deletions src/core/commitGenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import { buildMessages } from './messageBuilder.js'
* @param {string} options.model - The GPT model to use
* @param {string} options.language - The target language
* @param {boolean} options.prefixEnabled - Whether to use prefixes
* @param {boolean} [options.allowEmojis=true] - Whether to allow emojis
* @returns {Promise<string>} The sanitized commit message
*/
export async function generateCommitMessage(deps, options) {
const { openaiService, client } = deps
const { diff, model, language, prefixEnabled } = options
const { diff, model, language, prefixEnabled, allowEmojis = true } = options

const messages = buildMessages({ diff, language, prefixEnabled })

Expand All @@ -27,5 +28,5 @@ export async function generateCommitMessage(deps, options) {
maxTokens: 200,
})

return sanitizeCommitMessage(rawMessage)
return sanitizeCommitMessage(rawMessage, allowEmojis)
}
19 changes: 15 additions & 4 deletions src/utils/sanitizeCommitMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,23 @@
* issues with shell commands.
*
* Only removes: " (double quote), ' (single quote), ` (backtick)
* All other characters including emojis, symbols, and Unicode are preserved.
* When allowEmojis is false, also removes emoji characters.
*
* @param {string} message - The commit message to sanitize
* @param {boolean} [allowEmojis=true] - Whether to preserve emoji characters
* @returns {string} The sanitized commit message
*/
export function sanitizeCommitMessage(message) {
// Remove only quote literals that can cause shell escaping issues
return message.replace(/["'`]/g, '')
export function sanitizeCommitMessage(message, allowEmojis = true) {
// Remove quote literals that can cause shell escaping issues
let result = message.replace(/["'`]/g, '')

// Strip emojis if disabled
if (!allowEmojis) {
result = result.replace(
/\p{Emoji_Presentation}|\p{Extended_Pictographic}/gu,
'',
)
Comment on lines +16 to +21
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

git ls-files | grep -E "(sanitize|emoji)" | head -20

Repository: laststance/git-gpt-commit

Length of output: 196


🏁 Script executed:

fd -t f "sanitizeCommitMessage" .

Repository: laststance/git-gpt-commit

Length of output: 176


🏁 Script executed:

cat -n src/utils/sanitizeCommitMessage.js

Repository: laststance/git-gpt-commit

Length of output: 1036


🏁 Script executed:

cat -n utils/sanitizeCommitMessage.js

Repository: laststance/git-gpt-commit

Length of output: 218


🏁 Script executed:

cat -n utils/sanitizeCommitMessage.test.js

Repository: laststance/git-gpt-commit

Length of output: 3257


🏁 Script executed:

# Check Node.js version requirements and package.json
cat package.json | grep -A 5 -B 5 "engines"

Repository: laststance/git-gpt-commit

Length of output: 51


🏁 Script executed:

# Test the emoji regex behavior to understand current vs suggested approach
node << 'EOF'
// Test if Unicode properties work
try {
  const testStr = '🐛🔥💥';
  const current = testStr.replace(/\p{Emoji_Presentation}|\p{Extended_Pictographic}/gu, '');
  console.log('Current regex works:', current === '');
} catch (e) {
  console.log('Current regex error:', e.message);
}

// Test ZWJ sequences (family emoji)
const familyEmoji = '👨‍👩‍👧‍👦'; // Man, ZWJ, Woman, ZWJ, Girl, ZWJ, Boy
console.log('Family emoji:', familyEmoji);
console.log('Family emoji length:', familyEmoji.length);
console.log('Family emoji codepoints:', [...familyEmoji].length);

// Test with variation selector
const emojiWithVS = '❤️'; // Heart with VS16
console.log('Heart with VS16:', emojiWithVS);
console.log('Heart with VS16 length:', emojiWithVS.length);

// Test current regex on complex emoji
try {
  const result = familyEmoji.replace(/\p{Emoji_Presentation}|\p{Extended_Pictographic}/gu, '');
  console.log('Family emoji after current regex:', result);
  console.log('Family emoji after current regex length:', result.length);
  console.log('Invisible characters remain:', result.length > 0);
} catch (e) {
  console.log('Error testing family emoji:', e.message);
}
EOF

Repository: laststance/git-gpt-commit

Length of output: 332


🏁 Script executed:

# Test the suggested fix
node << 'EOF'
// Test suggested regex with additional emoji components
const familyEmoji = '👨‍👩‍👧‍👦'; // Family emoji with ZWJ
const heartWithVS = '❤️'; // Heart with VS16

// Current regex
const currentResult = familyEmoji.replace(/\p{Emoji_Presentation}|\p{Extended_Pictographic}/gu, '');
console.log('Current regex leaves:', currentResult.length, 'chars');

// Suggested regex from review
try {
  const suggestedResult = familyEmoji.replace(
    /(\p{Emoji_Presentation}|\p{Extended_Pictographic}|\p{Emoji_Modifier}|\p{Emoji_Component}|\u200D|\uFE0F)/gu,
    ''
  );
  console.log('Suggested regex leaves:', suggestedResult.length, 'chars');
} catch (e) {
  console.log('Suggested regex error:', e.message);
}

// Test with more comprehensive emoji patterns
const testCases = [
  { name: 'Basic emoji', text: '🐛' },
  { name: 'ZWJ family', text: '👨‍👩‍👧‍👦' },
  { name: 'Emoji with VS16', text: '❤️' },
  { name: 'Skin tone modifier', text: '👋🏽' },
];

console.log('\n--- Testing current regex ---');
testCases.forEach(({ name, text }) => {
  const result = text.replace(/\p{Emoji_Presentation}|\p{Extended_Pictographic}/gu, '');
  console.log(`${name}: left ${result.length} chars`);
});

console.log('\n--- Testing suggested regex ---');
testCases.forEach(({ name, text }) => {
  try {
    const result = text.replace(
      /(\p{Emoji_Presentation}|\p{Extended_Pictographic}|\p{Emoji_Modifier}|\p{Emoji_Component}|\u200D|\uFE0F)/gu,
      ''
    );
    console.log(`${name}: left ${result.length} chars`);
  } catch (e) {
    console.log(`${name}: error - ${e.message}`);
  }
});
EOF

Repository: laststance/git-gpt-commit

Length of output: 422


🏁 Script executed:

# Check Node.js version and what Unicode property support is available
node << 'EOF'
console.log('Node.js version:', process.version);

// Check which Unicode properties are supported
const props = [
  'Emoji_Presentation',
  'Extended_Pictographic',
  'Emoji_Modifier',
  'Emoji_Component',
];

props.forEach(prop => {
  try {
    const regex = new RegExp(`\\p{${prop}}`, 'u');
    console.log(`✓ ${prop} supported`);
  } catch (e) {
    console.log(`✗ ${prop} not supported`);
  }
});
EOF

Repository: laststance/git-gpt-commit

Length of output: 213


Remove ZWJ sequences and variation selectors to fully strip emojis.

The current regex leaves invisible characters behind when removing emojis:

  • ZWJ sequences (e.g., family emojis like 👨‍👩‍👧‍👦) leave zero-width joiners (\u200D)
  • Emoji with variation selectors (e.g., ❤️) leave VS16 (\uFE0F)

Apply the suggested regex to handle these components:

✅ Suggested regex refinement
-      /\p{Emoji_Presentation}|\p{Extended_Pictographic}/gu,
+      /(\p{Emoji_Presentation}|\p{Extended_Pictographic}|\p{Emoji_Modifier}|\p{Emoji_Component}|\u200D|\uFE0F)/gu,

All required Unicode properties are supported in current Node.js versions.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Strip emojis if disabled
if (!allowEmojis) {
result = result.replace(
/\p{Emoji_Presentation}|\p{Extended_Pictographic}/gu,
'',
)
// Strip emojis if disabled
if (!allowEmojis) {
result = result.replace(
/(\p{Emoji_Presentation}|\p{Extended_Pictographic}|\p{Emoji_Modifier}|\p{Emoji_Component}|\u200D|\uFE0F)/gu,
'',
)
🤖 Prompt for AI Agents
In `@src/utils/sanitizeCommitMessage.js` around lines 16 - 21, The current
emoji-strip in sanitizeCommitMessage (the block using allowEmojis and
result.replace) leaves behind zero-width joiners and variation selectors; update
the replacement to remove ZWJ (U+200D) and VS16 (U+FE0F) and to match emoji
sequences so no invisible characters remain—i.e., refine the regex used in the
result.replace call to include \u200D and \uFE0F handling (or use a pattern that
matches Extended_Pictographic sequences including joiners/variation selectors)
so the function fully strips emojis when allowEmojis is false.

}

return result
}
12 changes: 11 additions & 1 deletion utils/sanitizeCommitMessage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,20 @@ describe('sanitizeCommitMessage', () => {
)
})

it('should allow emojis', () => {
it('should allow emojis by default', () => {
expect(sanitizeCommitMessage('fix: bug 🐛🔥💥')).toBe('fix: bug 🐛🔥💥')
})

it('should allow emojis when allowEmojis is true', () => {
expect(sanitizeCommitMessage('fix: bug 🐛🔥💥', true)).toBe(
'fix: bug 🐛🔥💥',
)
})

it('should strip emojis when allowEmojis is false', () => {
expect(sanitizeCommitMessage('fix: bug 🐛🔥💥', false)).toBe('fix: bug ')
})

it('should only remove quotes from mixed content', () => {
expect(
sanitizeCommitMessage('feat(scope)!: add "dark mode" for issue #42 🎨'),
Expand Down