diff --git a/CHANGELOG.md b/CHANGELOG.md index 761efac..8118ecd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,27 @@ Follows [Semantic Versioning](https://semver.org/). ### Added +-- `git suggest-coauthors ` can filter by author name or email. Addresses [issue 90](https://github.com/rkotze/git-mob/issues/90) + +### Refactored + - Remove legacy git-message API and replace it with git-mob-core `git-message` API - Remove legacy git-add-coauthor API and replace it with git-mob-core `saveNewCoAuthors` +- Remove legacy git-suggest-coauthor API and replace it with git-mob-core `repoAuthorList` +- Migrated `git-suggest-coauthors` to TypeScript ## git-mob-core next +### Added + +- Added `repoAuthorList` which will list all contributors from a repo +- Added filter to `repoAuthorList` which uses `--author` flag from `git shortlog`. + +### Refactored + - Add new co-author module migrated to TypeScript and tested - Change to async `topLevelDirectory`, `insideWorkTree` - may not be needed in future versions -- Resolve-git-message-path migrated to TypeScript +- `resolve-git-message-path` migrated to TypeScript - Changed to async `resolveGitMessagePath`, `setCommitTemplate` ## git-mob 3.0.0 diff --git a/packages/git-mob-core/README.md b/packages/git-mob-core/README.md index b204fd3..a212398 100644 --- a/packages/git-mob-core/README.md +++ b/packages/git-mob-core/README.md @@ -25,6 +25,7 @@ fetchGitHubAuthors(userNames: string[], userAgent: string): > pathToCoAuthors(): > getConfig(prop: string): string | undefined updateConfig(prop: string, value: string): void +repoAuthorList(authorFilter?: string): Promise gitMobConfig = { localTemplate(): >, fetchFromGitHub(): >, @@ -39,7 +40,6 @@ gitRevParse = { insideWorkTree(): >, topLevelDirectory(): >, }; -class Author ``` ## Author class diff --git a/packages/git-mob-core/src/commands.js b/packages/git-mob-core/src/commands.js index f1b19ec..b25ae36 100644 --- a/packages/git-mob-core/src/commands.js +++ b/packages/git-mob-core/src/commands.js @@ -1,5 +1,4 @@ import { silentRun } from './silent-run.js'; -import { execCommand } from './git-mob-api/exec-command.js'; function handleResponse(query) { try { @@ -59,10 +58,6 @@ function removeGitMobSection() { return silentRun(`git config --global --remove-section git-mob`); } -export async function getRepoAuthors() { - return execCommand('git shortlog -sen HEAD'); -} - export const config = { getAll, get, diff --git a/packages/git-mob-core/src/git-mob-api/exec-command.ts b/packages/git-mob-core/src/git-mob-api/exec-command.ts index b20c5a6..7492c40 100644 --- a/packages/git-mob-core/src/git-mob-api/exec-command.ts +++ b/packages/git-mob-core/src/git-mob-api/exec-command.ts @@ -46,3 +46,12 @@ export async function setConfig(key: string, value: string) { throw new Error(`Git mob core setConfig: ${message}`); } } + +export async function getRepoAuthors(authorFilter?: string) { + let repoAuthorQuery = 'git shortlog -seni HEAD'; + if (authorFilter) { + repoAuthorQuery += ` --author="${authorFilter}"`; + } + + return execCommand(repoAuthorQuery); +} diff --git a/packages/git-mob-core/src/git-mob-api/git-authors/repo-author-list.spec.ts b/packages/git-mob-core/src/git-mob-api/git-authors/repo-author-list.spec.ts new file mode 100644 index 0000000..3056d85 --- /dev/null +++ b/packages/git-mob-core/src/git-mob-api/git-authors/repo-author-list.spec.ts @@ -0,0 +1,74 @@ +import os from 'node:os'; +import { getRepoAuthors } from '../exec-command'; +import { Author } from '../author'; +import { repoAuthorList } from './repo-author-list'; + +jest.mock('../exec-command'); +const mockedGetRepoAuthors = jest.mocked(getRepoAuthors); + +describe('Extract repository authors', function () { + it('Given a list of authors extract the name and email', async function () { + mockedGetRepoAuthors.mockResolvedValueOnce( + ` 33\tRichard Kotze ${os.EOL} 53\tTony Stark ` + ); + const listOfAuthors = await repoAuthorList(); + expect(listOfAuthors).toEqual([ + new Author('rkrk', 'Richard Kotze', 'rkotze@email.com'), + new Author('tsto', 'Tony Stark', 'tony@stark.com'), + ]); + }); + + it('author has one name', async function () { + mockedGetRepoAuthors.mockResolvedValueOnce( + ` 33\tRichard ${os.EOL} 53\tTony Stark ` + ); + const listOfAuthors = await repoAuthorList(); + expect(listOfAuthors).toEqual([ + new Author('rrk', 'Richard', 'rkotze@email.com'), + new Author('tsto', 'Tony Stark', 'tony@stark.com'), + ]); + }); + + it('author uses a private GitHub email', async function () { + mockedGetRepoAuthors.mockResolvedValueOnce( + ` 33\tRichard ${os.EOL} 53\tTony Stark <20342323+tony[bot]@users.noreply.github.com>` + ); + const listOfAuthors = await repoAuthorList(); + expect(listOfAuthors).toEqual([ + new Author('rrk', 'Richard', 'rkotze@email.com'), + new Author( + 'ts20', + 'Tony Stark', + '20342323+tony[bot]@users.noreply.github.com' + ), + ]); + }); + + it('only one author on repository', async function () { + mockedGetRepoAuthors.mockResolvedValueOnce( + ` 33\tRichard Kotze ` + ); + const listOfAuthors = await repoAuthorList(); + expect(listOfAuthors).toEqual([ + new Author('rkrk', 'Richard Kotze', 'rkotze@email.com'), + ]); + }); + + it('author has special characters in name', async function () { + mockedGetRepoAuthors.mockResolvedValueOnce( + ` 33\tRic<8D>rd Kotze ` + ); + const listOfAuthors = await repoAuthorList(); + expect(listOfAuthors).toEqual([ + new Author('rkrk', 'Ric<8D>rd Kotze', 'rkotze@email.com'), + ]); + }); + + it('exclude if fails to match author pattern in list', async function () { + mockedGetRepoAuthors.mockResolvedValueOnce( + ` 33\tRichard Kotze { + const repoAuthorsString = await getRepoAuthors(authorFilter); + const splitEndOfLine = repoAuthorsString.split(EOL); + const authorList = splitEndOfLine + .map(createRepoAuthor) + .filter(author => author !== undefined) as Author[]; + + if (authorList.length > 0) return authorList; +} + +function createRepoAuthor(authorString: string) { + const regexList = /\d+\t(.+)\s<(.+)>/; + const authorArray = regexList.exec(authorString); + if (authorArray !== null) { + const [, name, email] = authorArray; + return new Author(genKey(name, email), name, email); + } +} + +function genKey(name: string, email: string) { + const nameInitials = name + .toLowerCase() + .split(' ') + .reduce(function (acc, cur) { + return acc + cur[0]; + }, ''); + + const domainFirstTwoLetters = email.slice(0, 2); + return nameInitials + domainFirstTwoLetters; +} diff --git a/packages/git-mob-core/src/index.ts b/packages/git-mob-core/src/index.ts index 53c706b..56cb298 100644 --- a/packages/git-mob-core/src/index.ts +++ b/packages/git-mob-core/src/index.ts @@ -132,6 +132,7 @@ export const gitRevParse = { }; export { saveNewCoAuthors } from './git-mob-api/manage-authors/add-new-coauthor.js'; +export { repoAuthorList } from './git-mob-api/git-authors/repo-author-list.js'; export { pathToCoAuthors } from './git-mob-api/git-authors/index.js'; export { fetchGitHubAuthors } from './git-mob-api/git-authors/fetch-github-authors.js'; export { getConfig, updateConfig } from './config-manager.js'; diff --git a/packages/git-mob/README.md b/packages/git-mob/README.md index 4b00f8b..41a8a28 100644 --- a/packages/git-mob/README.md +++ b/packages/git-mob/README.md @@ -27,7 +27,7 @@ _Add co-authors to commits_ when you collaborate on code. Use when pairing with - [Add co-author](#add-co-author) - [Delete co-author](#delete-co-author) - [Edit co-author](#edit-co-author) - - [Suggest co-authors base on current repo](#suggest-co-authors-base-on-current-repo) + - [Suggest co-authors](#suggest-co-authors) - [Help](#help) - [Add initials of current mob to your prompt](#add-initials-of-current-mob-to-your-prompt) - [Bash](#bash) @@ -242,13 +242,14 @@ $ git edit-coauthor bb --name="Barry Butterworth" $ git edit-coauthor bb --email="barry@butterworth.org" ``` -### Suggest co-authors base on current repo +### Suggest co-authors -Suggest some co-authors to add based on existing committers to your -current repo +Suggest co-authors to save based on contributors to the current Git repo. + +Optional author filter by name or email. ``` -$ git suggest-coauthors +$ git suggest-coauthors ``` ### Help diff --git a/packages/git-mob/src/git-commands.js b/packages/git-mob/src/git-commands.js index 4cc501e..12ff82d 100644 --- a/packages/git-mob/src/git-commands.js +++ b/packages/git-mob/src/git-commands.js @@ -129,15 +129,6 @@ function topLevelDirectory() { return silentRun('git rev-parse --show-toplevel').stdout.trim(); } -/** - * Returns a list of the existing authors for the git repository - * including their names and email addresses - * @returns {string} of output from git command - */ -function shortLogAuthorSummary() { - return silentRun('git shortlog --summary --email --number HEAD').stdout.trim(); -} - function getTemplatePath() { return get('commit.template'); } @@ -182,6 +173,3 @@ export const revParse = { insideWorkTree, topLevelDirectory, }; -export const authors = { - shortLogAuthorSummary, -}; diff --git a/packages/git-mob/src/git-suggest-coauthors.js b/packages/git-mob/src/git-suggest-coauthors.js deleted file mode 100644 index f1c156d..0000000 --- a/packages/git-mob/src/git-suggest-coauthors.js +++ /dev/null @@ -1,98 +0,0 @@ -import os from 'node:os'; -import minimist from 'minimist'; -import { gitRevParse } from 'git-mob-core'; -import { runSuggestCoauthorsHelp } from './helpers.js'; -import { authors } from './git-commands.js'; - -const argv = minimist(process.argv.slice(2), { - alias: { - h: 'help', - }, -}); - -async function execute(argv) { - if (argv.help) { - runSuggestCoauthorsHelp(); - process.exit(0); - } - - const isGitRepo = await gitRevParse.insideWorkTree(); - if (!isGitRepo) { - console.error('Error: not a git repository'); - process.exit(1); - } - - await printCoauthorSuggestions(); - process.exit(0); -} - -async function printCoauthorSuggestions() { - try { - const shortLogAuthorSummary = authors.shortLogAuthorSummary(); - const gitAuthors = shortLogAuthorSummary - .split('\n') - .filter(summary => summary !== '') - .map(summary => convertSummaryToCoauthor(summary)); - - if (gitAuthors.length > 0) { - console.log( - os.EOL + - 'Here are some suggestions for coauthors based on existing authors of this repository' + - os.EOL - ); - - console.log(suggestedCoauthorAddCommands(gitAuthors)); - - console.log( - os.EOL + - 'Paste any line above into your console to add them as an author' + - os.EOL - ); - } else { - console.log('Unable to find existing authors'); - } - } catch (error) { - console.error(`Error: ${error.message}`); - process.exit(1); - } -} - -function convertSummaryToCoauthor(summaryLine) { - const name = nameFromSummaryLine(summaryLine); - return { - name, - email: emailFromSummaryLine(summaryLine), - initials: initialsFromName(name), - }; -} - -function suggestedCoauthorAddCommands(coauthors) { - return coauthors - .sort() - .map(coauthor => - [ - 'git add-coauthor', - coauthor.initials, - JSON.stringify(coauthor.name), - coauthor.email, - ].join(' ') - ) - .join(os.EOL); -} - -function initialsFromName(name) { - return name - .split(' ') - .map(word => word[0].toLowerCase()) - .join(''); -} - -function nameFromSummaryLine(summaryLine) { - return summaryLine.split('\t')[1].split(' ').slice(0, -1).join(' '); -} - -function emailFromSummaryLine(summaryLine) { - return summaryLine.split('\t')[1].split(' ').pop().slice(1, -1); -} - -await execute(argv); diff --git a/packages/git-mob/src/git-suggest-coauthors.spec.js b/packages/git-mob/src/git-suggest-coauthors.spec.js deleted file mode 100644 index e4b66b4..0000000 --- a/packages/git-mob/src/git-suggest-coauthors.spec.js +++ /dev/null @@ -1,18 +0,0 @@ -import test from 'ava'; -import { exec } from '../test-helpers/index.js'; - -test('suggests potential coauthors', t => { - const { stdout } = exec('git suggest-coauthors'); - - t.regex(stdout, /Here are some suggestions/); - t.regex(stdout, /git add-coauthor rk "Richard Kotze" rkotze@findmypast.com/); - t.regex(stdout, /Paste any line above/); -}); - -test('-h prints help', t => { - const { stdout } = exec('git suggest-coauthors -h'); - - t.regex(stdout, /usage/i); - t.regex(stdout, /options/i); - t.regex(stdout, /example/i); -}); diff --git a/packages/git-mob/src/git-suggest-coauthors.spec.ts b/packages/git-mob/src/git-suggest-coauthors.spec.ts new file mode 100644 index 0000000..fcfe4c4 --- /dev/null +++ b/packages/git-mob/src/git-suggest-coauthors.spec.ts @@ -0,0 +1,26 @@ +import { EOL } from 'node:os'; +import test from 'ava'; +import { exec } from '../test-helpers/index.js'; + +test('Suggests coauthors using repo contributors', t => { + const { stdout } = exec('git suggest-coauthors'); + + t.regex(stdout, /Here are some suggestions/); + t.regex(stdout, /git add-coauthor rkri "Richard Kotze" richkotze@outlook.com/); + t.regex(stdout, /Paste any line above/); +}); + +test('Filter suggestions of coauthors', t => { + const { stdout } = exec('git suggest-coauthors dennis i'); + + t.regex(stdout, /git add-coauthor diid "Dennis Ideler" ideler.dennis@gmail.com/); + t.is(stdout.split(EOL).filter(a => a.includes('git add-coauthor')).length, 2); +}); + +test('Prints help message', t => { + const { stdout } = exec('git suggest-coauthors -h'); + + t.regex(stdout, /usage/i); + t.regex(stdout, /options/i); + t.regex(stdout, /example/i); +}); diff --git a/packages/git-mob/src/git-suggest-coauthors.ts b/packages/git-mob/src/git-suggest-coauthors.ts new file mode 100644 index 0000000..7d1b216 --- /dev/null +++ b/packages/git-mob/src/git-suggest-coauthors.ts @@ -0,0 +1,66 @@ +import os from 'node:os'; +import minimist from 'minimist'; +import { type Author, gitRevParse, repoAuthorList } from 'git-mob-core'; +import { runSuggestCoauthorsHelp } from './helpers.js'; + +const argv = minimist(process.argv.slice(2), { + alias: { + h: 'help', + }, +}); + +async function execute(argv: minimist.ParsedArgs) { + if (argv.help) { + runSuggestCoauthorsHelp(); + process.exit(0); + } + + const isGitRepo = await gitRevParse.insideWorkTree(); + if (!isGitRepo) { + console.error('Error: not a git repository'); + process.exit(1); + } + + await printCoauthorSuggestions(argv._.join(' ')); + process.exit(0); +} + +async function printCoauthorSuggestions(authorFilter: string) { + try { + const gitAuthors = await repoAuthorList(authorFilter.trim()); + + if (gitAuthors && gitAuthors.length > 0) { + console.log( + os.EOL + + 'Here are some suggestions for coauthors from contributors to this repository' + + os.EOL + ); + + console.log(suggestedCoauthorAddCommands(gitAuthors)); + + console.log( + os.EOL + + 'Paste any line above into your console to add them as an author' + + os.EOL + ); + } else { + console.log('Unable to find existing authors'); + } + } catch (error: unknown) { + const errorSuggest = error as Error; + console.error(`Error: ${errorSuggest.message}`); + process.exit(1); + } +} + +function suggestedCoauthorAddCommands(coauthors: Author[]): string { + return coauthors + .map(coauthor => + ['git add-coauthor', coauthor.key, `"${coauthor.name}"`, coauthor.email].join( + ' ' + ) + ) + .join(os.EOL); +} + +await execute(argv); diff --git a/packages/git-mob/src/helpers.js b/packages/git-mob/src/helpers.js index a4bcf01..77a04cb 100644 --- a/packages/git-mob/src/helpers.js +++ b/packages/git-mob/src/helpers.js @@ -90,11 +90,12 @@ function runMobPrintHelp() { function runSuggestCoauthorsHelp() { const message = stripIndent` Usage - $ git suggest-coauthors + $ git suggest-coauthors Options -h Prints usage information Example - $ git suggest-coauthors # suggests coauthors to add based on existing committers to the repo + $ git suggest-coauthors # suggests coauthors who have contributed to this repo + $ git suggest-coauthors rich # filter suggested coauthors `; console.log(message); }