Skip to content
Draft
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
2 changes: 1 addition & 1 deletion packages/git-mob-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"scripts": {
"build": "rimraf dist && tsc --project tsconfig.test.json && bob -c=core",
"pretest": "npm run build -- -t",
"test": "jest",
"test": "jest --coverage --verbose",
"minifytest": "npm run build -- -m -t && npm run test",
"prepack": "rimraf dist && tsc --project tsconfig.prod.json && bob -c=core -m",
"checks": "npm run test && npm run lint",
Expand Down
2 changes: 1 addition & 1 deletion packages/git-mob-core/src/git-mob-api/author.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class Author {
}

format() {
return `${this.trailer} ${this.toString()}`;
return `${this.trailer}: ${this.toString()}`;
}

toString() {
Expand Down
8 changes: 8 additions & 0 deletions packages/git-mob-core/src/git-mob-api/exec-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ export async function getAllConfig(key: string) {
}
}

export async function getRegexpConfig(key: string) {
try {
return await execCommand(`git config --get-regexp ${key}`);
} catch {
return undefined;
}
}

export async function setConfig(key: string, value: string) {
try {
await execCommand(`git config ${key} "${value}"`);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { EOL } from 'node:os';
import { Author } from '../author';
import { messageFormatter } from './message-formatter';
import { AuthorTrailers, messageFormatter } from './message-formatter';

test('MessageFormatter: No authors to append to git message', () => {
const txt = `git message`;
Expand Down Expand Up @@ -28,6 +28,32 @@ test('MessageFormatter: Append co-authors to git message', () => {
);
});

test('MessageFormatter: Append mixed trailers to git message', () => {
const txt = `git message`;
const message = messageFormatter(txt, [
new Author('jd', 'Jane Doe', 'jane@gitmob.com', AuthorTrailers.CoAuthorBy),
new Author(
'fb',
'Frances Bar',
'frances-bar@gitmob.com',
AuthorTrailers.SignedOffBy
),
new Author('ab', 'Alex Baz', 'alex-baz@gitmob.com', AuthorTrailers.ReviewedBy),
]);
expect(message).toBe(
[
txt,
EOL,
EOL,
'Co-authored-by: Jane Doe <jane@gitmob.com>',
EOL,
'Signed-off-by: Frances Bar <frances-bar@gitmob.com>',
EOL,
'Reviewed-by: Alex Baz <alex-baz@gitmob.com>',
].join('')
);
});

test('MessageFormatter: Replace co-author in the git message', () => {
const firstLine = 'git message';
const txt = [
Expand All @@ -50,6 +76,37 @@ test('MessageFormatter: Replace co-author in the git message', () => {
);
});

test('MessageFormatter: Replace mixed trailers in the git message', () => {
const firstLine = 'git message';
const txt = [
firstLine,
EOL,
EOL,
'Co-authored-by: Jane Doe <jane@gitmob.com>',
EOL,
'Signed-off-by: Jane Doe <jane@gitmob.com>',
].join('');
const message = messageFormatter(txt, [
new Author(
'fb',
'Frances Bar',
'frances-bar@gitmob.com',
AuthorTrailers.SignedOffBy
),
new Author('ab', 'Alex Baz', 'alex-baz@gitmob.com', AuthorTrailers.CoAuthorBy),
]);
expect(message).toBe(
[
firstLine,
EOL,
EOL,
'Signed-off-by: Frances Bar <frances-bar@gitmob.com>',
EOL,
'Co-authored-by: Alex Baz <alex-baz@gitmob.com>',
].join('')
);
});

test('MessageFormatter: Replace co-author in the git message with no line break', () => {
const firstLine = 'git message';
const txt = [firstLine, 'Co-authored-by: Jane Doe <jane@gitmob.com>'].join('');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ import { EOL } from 'node:os';
import { type Author } from '../author';

export enum AuthorTrailers {
CoAuthorBy = 'Co-authored-by:',
CoAuthorBy = 'Co-authored-by',
SignedOffBy = 'Signed-off-by',
ReviewedBy = 'Reviewed-by',
}

export function messageFormatter(txt: string, authors: Author[]): string {
const trailers = AuthorTrailers.CoAuthorBy;
function removeTrailers(txt: string): string {
const trailers = Object.values(AuthorTrailers).join('|');
const regex = new RegExp(`(\r\n|\r|\n){0,2}(${trailers}).*`, 'g');
const message = txt.replaceAll(regex, '');
return txt.replaceAll(regex, '');
}

export function messageFormatter(txt: string, authors: Author[]): string {
const message = removeTrailers(txt);

if (authors && authors.length > 0) {
const authorTrailerTxt = authors.map(author => author.format()).join(EOL);
Expand Down
8 changes: 4 additions & 4 deletions packages/git-mob-core/src/git-mob-api/git-mob-config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getConfig, getAllConfig, execCommand } from './exec-command.js';
import { getConfig, execCommand, getRegexpConfig } from './exec-command.js';

export async function localTemplate() {
const localTemplate = await getConfig('--local git-mob-config.use-local-template');
Expand All @@ -11,11 +11,11 @@ export async function fetchFromGitHub() {
}

export async function getSetCoAuthors() {
return getAllConfig('--global git-mob.co-author');
return getRegexpConfig(`--global 'git-mob.*'`);
}

export async function addCoAuthor(coAuthor: string) {
const addAuthorQuery = `git config --add --global git-mob.co-author "${coAuthor}"`;
export async function addCoAuthor(coAuthor: string, trailerKey = 'co-author') {
const addAuthorQuery = `git config --add --global git-mob.${trailerKey} "${coAuthor}"`;

return execCommand(addAuthorQuery);
}
Expand Down
90 changes: 77 additions & 13 deletions packages/git-mob-core/src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { EOL } from 'node:os';
import { gitAuthors } from './git-mob-api/git-authors';
import { gitMessage } from './git-mob-api/git-message';
import { AuthorNotFound } from './git-mob-api/errors/author-not-found';
Expand All @@ -8,11 +9,13 @@ import {
getSetCoAuthors,
removeGitMobSection,
} from './git-mob-api/git-mob-config';
import { AuthorTrailers } from './git-mob-api/git-message/message-formatter';
import {
getPrimaryAuthor,
getSelectedCoAuthors,
setCoAuthors,
setPrimaryAuthor,
setSelectedAuthors,
updateGitTemplate,
} from '.';

Expand All @@ -28,13 +31,15 @@ const mockedGitMessage = jest.mocked(gitMessage);
const mockedRemoveGitMobSection = jest.mocked(removeGitMobSection);
const mockedGitConfig = jest.mocked(gitConfig);
const mockedGetSetCoAuthors = jest.mocked(getSetCoAuthors);
const mockedAddCoAuthor = jest.mocked(addCoAuthor);

describe('Git Mob core API', () => {
afterEach(() => {
mockedRemoveGitMobSection.mockReset();
mockedGetSetCoAuthors.mockReset();
mockedGitConfig.getGlobalCommitTemplate.mockReset();
mockedGitConfig.getLocalCommitTemplate.mockReset();
mockedAddCoAuthor.mockReset();
});

it('missing author to pick for list throws error', async () => {
Expand All @@ -45,7 +50,7 @@ describe('Git Mob core API', () => {

mockedGitMessage.mockReturnValue({
writeCoAuthors: mockWriteCoAuthors,
readCoAuthors: () => '',
readCoAuthors: async () => '',
removeCoAuthors: mockRemoveCoAuthors,
});

Expand All @@ -65,15 +70,50 @@ describe('Git Mob core API', () => {

mockedGitMessage.mockReturnValue({
writeCoAuthors: mockWriteCoAuthors,
readCoAuthors: () => '',
readCoAuthors: async () => '',
removeCoAuthors: mockRemoveCoAuthors,
});

const coAuthors = await setCoAuthors(authorKeys);

expect(mockedRemoveGitMobSection).toHaveBeenCalledTimes(1);
expect(mockRemoveCoAuthors).toHaveBeenCalledTimes(1);
expect(addCoAuthor).toHaveBeenCalledTimes(2);
expect(mockedAddCoAuthor).toHaveBeenCalledTimes(2);
expect(mockWriteCoAuthors).toHaveBeenCalledWith(authorList);
expect(coAuthors).toEqual(authorList);
});

it('apply co-authors to git config and git message with custom trailers', async () => {
const authorKeys = ['ab', 'cd'];
const authorTrailers = [AuthorTrailers.CoAuthorBy, AuthorTrailers.ReviewedBy];
const authorList = buildAuthorList(authorKeys, authorTrailers);
const mockWriteCoAuthors = jest.fn(async () => undefined);
const mockRemoveCoAuthors = jest.fn(async () => '');
mockedGitAuthors.mockReturnValue(mockGitAuthors([...authorKeys, 'ef']));

mockedGitMessage.mockReturnValue({
writeCoAuthors: mockWriteCoAuthors,
readCoAuthors: async () => '',
removeCoAuthors: mockRemoveCoAuthors,
});

const coAuthors = await setSelectedAuthors([
['ab', AuthorTrailers.CoAuthorBy],
['cd', AuthorTrailers.ReviewedBy],
]);

expect(mockedRemoveGitMobSection).toHaveBeenCalledTimes(1);
expect(mockRemoveCoAuthors).toHaveBeenCalledTimes(1);
expect(mockedAddCoAuthor).toHaveBeenNthCalledWith(
1,
authorList[0].toString(),
AuthorTrailers.CoAuthorBy
);
expect(mockedAddCoAuthor).toHaveBeenNthCalledWith(
2,
authorList[1].toString(),
AuthorTrailers.ReviewedBy
);
expect(mockWriteCoAuthors).toHaveBeenCalledWith(authorList);
expect(coAuthors).toEqual(authorList);
});
Expand Down Expand Up @@ -133,24 +173,48 @@ describe('Git Mob core API', () => {
expect(mockWriteCoAuthors).not.toHaveBeenCalled();
});

it('Get the selected co-authors', async () => {
const listAll = buildAuthorList(['ab', 'cd']);
const selectedAuthor = listAll[1];
mockedGetSetCoAuthors.mockResolvedValueOnce(selectedAuthor.toString());
it('Use exact email for selected co-authors', async () => {
const listAll = buildAuthorList(['ab', 'efcd', 'cd']);
const selectedAuthor = `git-mob.co-author ${listAll[1].toString()}`;
mockedGetSetCoAuthors.mockResolvedValueOnce(selectedAuthor);
const selected = await getSelectedCoAuthors(listAll);

expect(mockedGetSetCoAuthors).toHaveBeenCalledTimes(1);
expect(selected).toEqual([selectedAuthor]);
expect(selected).toEqual([listAll[1]]);
});

it('Use exact email for selected co-authors', async () => {
const listAll = buildAuthorList(['ab', 'efcd', 'cd']);
const selectedAuthor = listAll[1];
mockedGetSetCoAuthors.mockResolvedValueOnce(selectedAuthor.toString());
it('Backward compatibility get the selected co-author using "git-mob.co-author"', async () => {
const listAll = buildAuthorList(['ab', 'cd', 'ef', 'gh']);
const selectedAuthors = [
`git-mob.co-author ${listAll[1].toString()}`,
`git-mob.${AuthorTrailers.ReviewedBy} ${listAll[2].toString()}`,
].join(EOL);

mockedGetSetCoAuthors.mockResolvedValueOnce(selectedAuthors);
const selected = await getSelectedCoAuthors(listAll);

expect(mockedGetSetCoAuthors).toHaveBeenCalledTimes(1);
expect(selected.length).toEqual(2);
expect(selected[0]?.trailer).toEqual(AuthorTrailers.CoAuthorBy);
expect(selected[1]?.trailer).toEqual(AuthorTrailers.ReviewedBy);
});

it('Get the selected co-authors and update respective trailers', async () => {
const listAll = buildAuthorList(['ab', 'cd', 'ef', 'gh']);
const selectedAuthors = [
`git-mob.${AuthorTrailers.CoAuthorBy} ${listAll[1].toString()}`,
`git-mob.${AuthorTrailers.SignedOffBy} ${listAll[2].toString()}`,
`git-mob.${AuthorTrailers.ReviewedBy} ${listAll[3].toString()}`,
].join(EOL);

mockedGetSetCoAuthors.mockResolvedValueOnce(selectedAuthors);
const selected = await getSelectedCoAuthors(listAll);

expect(mockedGetSetCoAuthors).toHaveBeenCalledTimes(1);
expect(selected).toEqual([selectedAuthor]);
expect(selected.length).toEqual(3);
expect(selected[0]?.trailer).toEqual(AuthorTrailers.CoAuthorBy);
expect(selected[1]?.trailer).toEqual(AuthorTrailers.SignedOffBy);
expect(selected[2]?.trailer).toEqual(AuthorTrailers.ReviewedBy);
});

it('Get the Git primary author', async () => {
Expand Down
Loading