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
14 changes: 9 additions & 5 deletions src/commands/commit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,12 @@ export function registerCommitCommand(
1,
);
}
const loreId = await headLoreIdReader.read();
const result = await gitClient.commit('', { amend: true, noEdit: true });
console.log(formatter.formatSuccess(`Commit amended: ${result.hash}`, { hash: result.hash }));
const message = loreId
? `Commit amended: ${result.hash} | Lore-id: ${loreId}`
: `Commit amended: ${result.hash}`;
console.log(formatter.formatSuccess(message, { hash: result.hash, loreId }));
return;
}

Expand All @@ -103,17 +107,17 @@ export function registerCommitCommand(
}

// Build the commit message (reuse existing Lore-id on amend)
const message = commitBuilder.build(input, existingLoreId ?? undefined);
const { message: commitMessage, loreId } = commitBuilder.build(input, existingLoreId ?? undefined);

// Run git commit
const result = await gitClient.commit(message, options.amend ? { amend: true } : undefined);
const result = await gitClient.commit(commitMessage, options.amend ? { amend: true } : undefined);

// Output
const verb = options.amend ? 'amended' : 'created';
console.log(
formatter.formatSuccess(
`Commit ${verb}: ${result.hash}`,
{ hash: result.hash },
`Commit ${verb}: ${result.hash} | Lore-id: ${loreId}`,
{ hash: result.hash, loreId },
),
);
});
Expand Down
7 changes: 5 additions & 2 deletions src/services/commit-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class CommitBuilder {
this.config = config;
}

build(input: CommitInput, existingLoreId?: LoreId): string {
build(input: CommitInput, existingLoreId?: LoreId): { message: string; loreId: LoreId } {
const loreId = existingLoreId ?? this.loreIdGenerator.generate();
const trailers = this.buildTrailers(loreId, input);
const serialized = this.trailerParser.serialize(trailers);
Expand All @@ -60,7 +60,10 @@ export class CommitBuilder {
parts.push('');
parts.push(serialized);

return parts.join('\n');
return {
message: parts.join('\n'),
loreId,
};
}

validate(input: CommitInput): ValidationIssue[] {
Expand Down
11 changes: 10 additions & 1 deletion tests/unit/commands/commit-amend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function createMockFormatter(): IOutputFormatter {

function createMockCommitBuilder(): CommitBuilder {
return {
build: vi.fn().mockReturnValue('built message'),
build: vi.fn().mockReturnValue({ message: 'built message', loreId: 'a1b2c3d4' }),
validate: vi.fn().mockReturnValue([]),
} as unknown as CommitBuilder;
}
Expand Down Expand Up @@ -131,6 +131,15 @@ describe('lore commit --amend', () => {
);
});

it('should read Lore-id from HEAD even with --amend --no-edit', async () => {
const headLoreIdReader = createMockHeadLoreIdReader('cafebabe');
const deps = createDeps({ headLoreIdReader });

await runCommitCommand(['--amend', '--no-edit'], deps);

expect(headLoreIdReader.read).toHaveBeenCalledOnce();
});

it('should throw when --no-edit is combined with --file', async () => {
const deps = createDeps();
await expect(
Expand Down
66 changes: 38 additions & 28 deletions tests/unit/services/commit-builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ function createMockTrailerParser() {
for (const v of trailers.Supersedes) lines.push(`Supersedes: ${v}`);
for (const v of trailers['Depends-on']) lines.push(`Depends-on: ${v}`);
for (const v of trailers.Related) lines.push(`Related: ${v}`);
for (const [key, values] of trailers.custom) {
for (const v of values) lines.push(`${key}: ${v}`);
}
return lines.join('\n');
}),
containsLoreTrailers: vi.fn(),
Expand Down Expand Up @@ -61,12 +64,13 @@ describe('CommitBuilder', () => {
intent: 'feat(auth): add login flow',
};

const result = builder.build(input);
const { message, loreId } = builder.build(input);

expect(mockIdGen.generate).toHaveBeenCalledOnce();
expect(mockParser.serialize).toHaveBeenCalledOnce();
expect(result).toContain('feat(auth): add login flow');
expect(result).toContain('Lore-id: a1b2c3d4');
expect(message).toContain('feat(auth): add login flow');
expect(message).toContain('Lore-id: a1b2c3d4');
expect(loreId).toBe('a1b2c3d4');
});

it('should include body separated by blank lines', () => {
Expand All @@ -75,10 +79,10 @@ describe('CommitBuilder', () => {
body: 'This is a detailed explanation.',
};

const result = builder.build(input);
const { message } = builder.build(input);

expect(result).toContain('feat: add feature');
expect(result).toContain('\n\nThis is a detailed explanation.\n\n');
expect(message).toContain('feat: add feature');
expect(message).toContain('\n\nThis is a detailed explanation.\n\n');
});

it('should include all trailer types', () => {
Expand All @@ -99,46 +103,49 @@ describe('CommitBuilder', () => {
},
};

const result = builder.build(input);

expect(result).toContain('Constraint: Must use HTTPS');
expect(result).toContain('Constraint: No external deps');
expect(result).toContain('Rejected: Polling approach');
expect(result).toContain('Confidence: high');
expect(result).toContain('Scope-risk: narrow');
expect(result).toContain('Reversibility: clean');
expect(result).toContain('Directive: Review in 3 months');
expect(result).toContain('Tested: Unit tests for auth module');
expect(result).toContain('Not-tested: Edge case with expired tokens');
expect(result).toContain('Supersedes: bbccddee');
expect(result).toContain('Depends-on: 11223344');
expect(result).toContain('Related: aabbccdd');
const { message } = builder.build(input);

expect(message).toContain('Constraint: Must use HTTPS');
expect(message).toContain('Constraint: No external deps');
expect(message).toContain('Rejected: Polling approach');
expect(message).toContain('Confidence: high');
expect(message).toContain('Scope-risk: narrow');
expect(message).toContain('Reversibility: clean');
expect(message).toContain('Directive: Review in 3 months');
expect(message).toContain('Tested: Unit tests for auth module');
expect(message).toContain('Not-tested: Edge case with expired tokens');
expect(message).toContain('Supersedes: bbccddee');
expect(message).toContain('Depends-on: 11223344');
expect(message).toContain('Related: aabbccdd');
});

it('should auto-generate Lore-id', () => {
mockIdGen.generate.mockReturnValue('deadbeef');
const input: CommitInput = { intent: 'test' };

const result = builder.build(input);
const { message, loreId } = builder.build(input);

expect(result).toContain('Lore-id: deadbeef');
expect(message).toContain('Lore-id: deadbeef');
expect(loreId).toBe('deadbeef');
});

it('should use provided existingLoreId instead of generating one', () => {
const input: CommitInput = { intent: 'amend: update commit' };

const result = builder.build(input, 'cafebabe');
const { message, loreId } = builder.build(input, 'cafebabe');

expect(result).toContain('Lore-id: cafebabe');
expect(message).toContain('Lore-id: cafebabe');
expect(loreId).toBe('cafebabe');
expect(mockIdGen.generate).not.toHaveBeenCalled();
});

it('should generate new Lore-id when no existingLoreId is provided', () => {
const input: CommitInput = { intent: 'new commit' };

builder.build(input);
const { message } = builder.build(input);

expect(mockIdGen.generate).toHaveBeenCalledOnce();
expect(message).toContain('Lore-id: a1b2c3d4');
});

it('should pass correct trailers to serialize', () => {
Expand All @@ -147,12 +154,13 @@ describe('CommitBuilder', () => {
trailers: { Confidence: 'medium' },
};

builder.build(input);
const { message } = builder.build(input);

const passedTrailers = mockParser.serialize.mock.calls[0][0] as LoreTrailers;
expect(passedTrailers['Lore-id']).toBe('a1b2c3d4');
expect(passedTrailers.Confidence).toBe('medium');
expect(passedTrailers.Constraint).toEqual([]);
expect(message).toContain('Lore-id: a1b2c3d4');
});

it('should pass custom trailers through to LoreTrailers.custom map', () => {
Expand All @@ -167,11 +175,12 @@ describe('CommitBuilder', () => {
},
};

builder.build(input);
const { message } = builder.build(input);

const passedTrailers = vi.mocked(mockParser.serialize).mock.calls[0][0] as LoreTrailers;
expect(passedTrailers.custom.get('Assisted-by')).toEqual(['Gemini:CLI']);
expect(passedTrailers.custom.get('Ticket')).toEqual(['PROJ-123']);
expect(message).toContain('Assisted-by: Gemini:CLI');
});

it('should produce empty custom map when no custom trailers provided', () => {
Expand All @@ -180,10 +189,11 @@ describe('CommitBuilder', () => {
trailers: { Confidence: 'high' as const },
};

builder.build(input);
const { message } = builder.build(input);

const passedTrailers = vi.mocked(mockParser.serialize).mock.calls[0][0] as LoreTrailers;
expect(passedTrailers.custom.size).toBe(0);
expect(message).toContain('Lore-id: a1b2c3d4');
});
});

Expand Down
Loading