diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55e2c8f..de16c2f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,9 +39,6 @@ jobs: - name: Build package run: npm run build - - name: Install Git-Mob (link) - run: npm link - - name: Run lint and tests run: npm run checks diff --git a/CHANGELOG.md b/CHANGELOG.md index c8e1e8d..6ce9661 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,35 @@ Follows [Semantic Versioning](https://semver.org/). +## git-mob-core 0.6.0 current + +### Added + +```ts +gitMobConfig = { + localTemplate(): >, + fetchFromGitHub(): >, +}; + +gitConfig = { + getLocalCommitTemplate(): >, + getGlobalCommitTemplate(): >, +}; + +gitRevParse = { + insideWorkTree(): string, + topLevelDirectory(): boolean, +}; +``` + +## git-mob 2.5.0 current + +### Added + +- Integrated git-mob-core for main `git mob` features +- Reduced the calls to `git` CLI to speed up command execution for `git mob` +- Convert src git-mob and spec files from JS to TS + ## git-mob-core 0.5.0 10-06-2023 ### Added diff --git a/package-lock.json b/package-lock.json index c68a39e..da23d1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "git-mob-workspace", - "version": "2.3.5", + "version": "2.4.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "git-mob-workspace", - "version": "2.3.5", + "version": "2.4.1", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -1778,6 +1778,12 @@ "@babel/types": "^7.3.0" } }, + "node_modules/@types/common-tags": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@types/common-tags/-/common-tags-1.8.3.tgz", + "integrity": "sha512-v3smfzf7umSwpkJrmlUe+apSv6bVnrIFCeBeprnP4f8lIh6pECZxyD50e8yFwfouIt85TdxN5yXiFwS5fnsS3w==", + "dev": true + }, "node_modules/@types/eslint": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz", @@ -11285,7 +11291,7 @@ } }, "packages/bob": { - "version": "1.1.0", + "version": "1.0.0", "license": "ISC", "dependencies": { "esbuild": "^0.17.4", @@ -11368,7 +11374,7 @@ "license": "MIT", "dependencies": { "common-tags": "^1.8.0", - "git-mob-core": "^0.3.0", + "git-mob-core": "^0.5.0", "minimist": "^1.2.6", "update-notifier": "^5.1.0" }, @@ -11383,6 +11389,7 @@ }, "devDependencies": { "@ava/typescript": "^3.0.1", + "@types/common-tags": "^1.8.3", "@types/node": "^18.7.14", "@types/sinon": "^10.0.13", "ava": "^5.0.1", @@ -11399,7 +11406,7 @@ } }, "packages/git-mob-core": { - "version": "0.4.0", + "version": "0.5.0", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.0", @@ -12744,6 +12751,12 @@ "@babel/types": "^7.3.0" } }, + "@types/common-tags": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@types/common-tags/-/common-tags-1.8.3.tgz", + "integrity": "sha512-v3smfzf7umSwpkJrmlUe+apSv6bVnrIFCeBeprnP4f8lIh6pECZxyD50e8yFwfouIt85TdxN5yXiFwS5fnsS3w==", + "dev": true + }, "@types/eslint": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz", @@ -15436,6 +15449,7 @@ "version": "file:packages/git-mob", "requires": { "@ava/typescript": "^3.0.1", + "@types/common-tags": "*", "@types/node": "^18.7.14", "@types/sinon": "^10.0.13", "ava": "^5.0.1", @@ -15443,7 +15457,7 @@ "common-tags": "^1.8.0", "env-cmd": "^10.1.0", "eol": "^0.9.1", - "git-mob-core": "^0.3.0", + "git-mob-core": "^0.5.0", "minimist": "^1.2.6", "rimraf": "^3.0.2", "sinon": "^14.0.0", diff --git a/packages/bob/git-mob.config.js b/packages/bob/git-mob.config.js index 92a4ed7..daf48c2 100644 --- a/packages/bob/git-mob.config.js +++ b/packages/bob/git-mob.config.js @@ -2,7 +2,7 @@ const glob = require('glob'); const baseConfig = { entryPoints: [ - './src/git-mob.js', + './src/git-mob.ts', './src/solo.js', './src/git-add-coauthor.ts', './src/git-delete-coauthor.js', diff --git a/packages/git-mob-core/README.md b/packages/git-mob-core/README.md index fdeb897..f6ba612 100644 --- a/packages/git-mob-core/README.md +++ b/packages/git-mob-core/README.md @@ -15,22 +15,38 @@ npm i git-mob-core ```TS saveNewCoAuthors(authors): > getAllAuthors(): > -getPrimaryAuthor(): Author | null +getPrimaryAuthor(): Author | undefined getSelectedCoAuthors(allAuthors): Author[] setCoAuthors(keys): > setPrimaryAuthor(author): void -solo(): void +solo(): > updateGitTemplate(selectedAuthors): void fetchGitHubAuthors(userNames: string[], userAgent: string): > pathToCoAuthors(): string getConfig(prop: string): string | undefined updateConfig(prop: string, value: string): void +gitMobConfig = { + localTemplate(): >, + fetchFromGitHub(): >, +}; + +gitConfig = { + getLocalCommitTemplate(): >, + getGlobalCommitTemplate(): >, +}; + +gitRevParse = { + insideWorkTree(): string, + topLevelDirectory(): boolean, +}; class Author ``` ## Author class ```TS +class Author; + // Properties Author.key: string Author.name: string diff --git a/packages/git-mob-core/package.json b/packages/git-mob-core/package.json index 7997e7c..f5bde73 100644 --- a/packages/git-mob-core/package.json +++ b/packages/git-mob-core/package.json @@ -2,6 +2,7 @@ "name": "git-mob-core", "version": "0.5.0", "description": "Core Git Mob library to manage co-authoring", + "homepage": "https://github.com/rkotze/git-mob/blob/master/packages/git-mob-core/README.md", "main": "./dist/index.js", "types": "./dist/index.d.ts", "scripts": { diff --git a/packages/git-mob-core/src/commands.js b/packages/git-mob-core/src/commands.js index 402d1e3..3cb44e6 100644 --- a/packages/git-mob-core/src/commands.js +++ b/packages/git-mob-core/src/commands.js @@ -1,26 +1,5 @@ -const { exec } = require('child_process'); -const { promisify } = require('util'); const { silentRun } = require('./silent-run'); -const { getConfig } = require('./config-manager'); - -/** - * Runs the given command in a shell. - * @param {string} command The command to execute - * @returns {Promise} stdout string - */ -async function silentExec(command) { - const execAsync = promisify(exec); - try { - const cmdConfig = {}; - const processCwd = getConfig('processCwd'); - if (processCwd) cmdConfig.cwd = processCwd; - const response = await execAsync(command, cmdOptions(cmdConfig)); - - return response.stdout; - } catch (error) { - return `GitMob silentExec: "${command}" ${error.message}`; - } -} +const { execCommand } = require('./git-mob-api/exec-command'); function handleResponse(query) { try { @@ -77,20 +56,13 @@ function gitAddCoAuthor(coAuthor) { } async function getRepoAuthors() { - return silentExec(`git shortlog -sen HEAD`); + return execCommand('git shortlog -sen HEAD'); } function removeGitMobSection() { return silentRun(`git config --global --remove-section git-mob`); } -function cmdOptions(extendOptions = {}) { - return { - ...extendOptions, - encoding: 'utf8', - }; -} - module.exports = { config: { getAll, diff --git a/packages/git-mob-core/src/git-mob-api/errors/author-not-found.ts b/packages/git-mob-core/src/git-mob-api/errors/author-not-found.ts new file mode 100644 index 0000000..65ad55f --- /dev/null +++ b/packages/git-mob-core/src/git-mob-api/errors/author-not-found.ts @@ -0,0 +1,5 @@ +export class AuthorNotFound extends Error { + constructor(initials: string) { + super(`Author with initials "${initials}" not found!`); + } +} 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 new file mode 100644 index 0000000..b947bae --- /dev/null +++ b/packages/git-mob-core/src/git-mob-api/exec-command.ts @@ -0,0 +1,39 @@ +import { exec } from 'node:child_process'; +import { promisify } from 'node:util'; +import { getConfig as cmGetConfig } from '../config-manager'; + +type ExecCommandOptions = { + encoding: string; + cwd?: string; +}; + +// Runs the given command in a shell. +export async function execCommand(command: string): Promise { + const cmdConfig: ExecCommandOptions = { encoding: 'utf8' }; + const processCwd = cmGetConfig('processCwd'); + if (processCwd) cmdConfig.cwd = processCwd; + const execAsync = promisify(exec); + const { stderr, stdout } = await execAsync(command, cmdConfig); + + if (stderr) { + throw new Error(`GitMob execCommand: "${command}" ${stderr.trim()}`); + } + + return stdout.trim(); +} + +export async function getConfig(key: string) { + try { + return await execCommand(`git config --get ${key}`); + } catch { + return undefined; + } +} + +export async function getAllConfig(key: string) { + try { + return await execCommand(`git config --get-all ${key}`); + } catch { + return undefined; + } +} diff --git a/packages/git-mob-core/src/git-mob-api/git-authors/fetch-github-authors.spec.ts b/packages/git-mob-core/src/git-mob-api/git-authors/fetch-github-authors.spec.ts index 2271631..e18ee72 100644 --- a/packages/git-mob-core/src/git-mob-api/git-authors/fetch-github-authors.spec.ts +++ b/packages/git-mob-core/src/git-mob-api/git-authors/fetch-github-authors.spec.ts @@ -9,6 +9,7 @@ const ghRkotzeResponse = { id: 123, login: 'rkotze', name: 'Richard Kotze', + stars: 2, }; const ghDidelerResponse = { @@ -83,6 +84,26 @@ test('Query for two GitHub users and build AuthorList', async () => { ]); }); +test('Handle GitHub user with no name', async () => { + mockedFetch.mockResolvedValue( + buildBasicResponse({ + id: 329, + name: null, + login: 'kotze', + }) + ); + + const actualAuthorList = await fetchGitHubAuthors(['kotze'], agentHeader); + + expect(actualAuthorList).toEqual([ + { + key: 'kotze', + name: 'kotze', + email: '329+kotze@users.noreply.github.com', + }, + ]); +}); + test('Error if no user agent specified', async () => { await expect(fetchGitHubAuthors(['badrequestuser'], '')).rejects.toThrow( /Error no user-agent header string given./ diff --git a/packages/git-mob-core/src/git-mob-api/git-authors/fetch-github-authors.ts b/packages/git-mob-core/src/git-mob-api/git-authors/fetch-github-authors.ts index 1a473b7..16b98a7 100644 --- a/packages/git-mob-core/src/git-mob-api/git-authors/fetch-github-authors.ts +++ b/packages/git-mob-core/src/git-mob-api/git-authors/fetch-github-authors.ts @@ -14,7 +14,7 @@ const getHeaders: RequestOptions = { type GitHubUser = { id: number; login: string; - name: string; + name?: string; }; function validateGhUser(o: any): o is GitHubUser { @@ -49,7 +49,7 @@ async function fetchGitHubAuthors( if (validateGhUser(ghUser.data)) { const { login, id, name } = ghUser.data; authorAuthorList.push( - new Author(login, name, `${id}+${login}@users.noreply.github.com`) + new Author(login, name || login, `${id}+${login}@users.noreply.github.com`) ); } } diff --git a/packages/git-mob-core/src/git-mob-api/git-authors/index.js b/packages/git-mob-core/src/git-mob-api/git-authors/index.js index 98502a4..7473881 100644 --- a/packages/git-mob-core/src/git-mob-api/git-authors/index.js +++ b/packages/git-mob-core/src/git-mob-api/git-authors/index.js @@ -3,7 +3,7 @@ const os = require('os'); const path = require('path'); const { promisify } = require('util'); const { Author } = require('../author'); -const { topLevelDirectory } = require('../../git-rev-parse'); +const { topLevelDirectory } = require('../git-rev-parse'); function gitAuthors(readFilePromise, writeFilePromise, overwriteFilePromise) { async function readFile(path) { diff --git a/packages/git-mob-core/src/git-mob-api/git-authors/index.spec.js b/packages/git-mob-core/src/git-mob-api/git-authors/index.spec.js index 73c1ab4..f3a549c 100644 --- a/packages/git-mob-core/src/git-mob-api/git-authors/index.spec.js +++ b/packages/git-mob-core/src/git-mob-api/git-authors/index.spec.js @@ -1,7 +1,7 @@ const path = require('path'); const fs = require('fs'); const { Author } = require('../author'); -const { topLevelDirectory } = require('../../git-rev-parse'); +const { topLevelDirectory } = require('../git-rev-parse'); const { gitAuthors, pathToCoAuthors } = require('.'); const validJsonString = ` diff --git a/packages/git-mob-core/src/git-mob-api/git-config.ts b/packages/git-mob-core/src/git-mob-api/git-config.ts new file mode 100644 index 0000000..58f25ba --- /dev/null +++ b/packages/git-mob-core/src/git-mob-api/git-config.ts @@ -0,0 +1,9 @@ +import { getConfig } from './exec-command'; + +export async function getLocalCommitTemplate() { + return getConfig('--local commit.template'); +} + +export async function getGlobalCommitTemplate() { + return getConfig('--global commit.template'); +} diff --git a/packages/git-mob-core/src/git-mob-api/git-mob-config.ts b/packages/git-mob-core/src/git-mob-api/git-mob-config.ts new file mode 100644 index 0000000..027f63c --- /dev/null +++ b/packages/git-mob-core/src/git-mob-api/git-mob-config.ts @@ -0,0 +1,15 @@ +import { getConfig, getAllConfig } from './exec-command'; + +export async function localTemplate() { + const localTemplate = await getConfig('--local git-mob-config.use-local-template'); + return localTemplate === 'true'; +} + +export async function fetchFromGitHub() { + const githubFetch = await getConfig('--global git-mob-config.github-fetch'); + return githubFetch === 'true'; +} + +export async function getSetCoAuthors() { + return getAllConfig('--global git-mob.co-author'); +} diff --git a/packages/git-mob-core/src/git-rev-parse.js b/packages/git-mob-core/src/git-mob-api/git-rev-parse.ts similarity index 64% rename from packages/git-mob-core/src/git-rev-parse.js rename to packages/git-mob-core/src/git-mob-api/git-rev-parse.ts index 5db5186..ab5deb0 100644 --- a/packages/git-mob-core/src/git-rev-parse.js +++ b/packages/git-mob-core/src/git-mob-api/git-rev-parse.ts @@ -1,11 +1,12 @@ -const { silentRun } = require('./silent-run'); +import { silentRun } from '../silent-run'; /** * Computes the path to the top-level directory of the git repository. * @returns {string} Path to the top-level directory of the git repository. */ -function topLevelDirectory() { - return silentRun('git rev-parse --show-toplevel').stdout.trim(); +function topLevelDirectory(): string { + const { stdout } = silentRun('git rev-parse --show-toplevel'); + return stdout.toString().trim(); } /** @@ -16,7 +17,4 @@ function insideWorkTree() { return silentRun('git rev-parse --is-inside-work-tree').status === 0; } -module.exports = { - topLevelDirectory, - insideWorkTree, -}; +export { topLevelDirectory, insideWorkTree }; diff --git a/packages/git-mob-core/src/git-mob-api/resolve-git-message-path.js b/packages/git-mob-core/src/git-mob-api/resolve-git-message-path.js index 50436e8..160fce3 100644 --- a/packages/git-mob-core/src/git-mob-api/resolve-git-message-path.js +++ b/packages/git-mob-core/src/git-mob-api/resolve-git-message-path.js @@ -2,7 +2,7 @@ const path = require('path'); const os = require('os'); const { config } = require('../commands'); -const { topLevelDirectory } = require('../git-rev-parse'); +const { topLevelDirectory } = require('./git-rev-parse'); function setCommitTemplate() { if (!config.has('commit.template')) { diff --git a/packages/git-mob-core/src/index.spec.ts b/packages/git-mob-core/src/index.spec.ts index d64c34a..44215b0 100644 --- a/packages/git-mob-core/src/index.spec.ts +++ b/packages/git-mob-core/src/index.spec.ts @@ -2,16 +2,24 @@ import { mob } from './commands'; import { Author } from './git-mob-api/author'; 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'; +import { + getGlobalCommitTemplate, + getLocalCommitTemplate, +} from './git-mob-api/git-config'; import { setCoAuthors, updateGitTemplate } from '.'; jest.mock('./commands'); jest.mock('./git-mob-api/git-authors'); jest.mock('./git-mob-api/git-message'); jest.mock('./git-mob-api/resolve-git-message-path'); +jest.mock('./git-mob-api/git-config'); const mockedGitAuthors = jest.mocked(gitAuthors); const mockedGitMessage = jest.mocked(gitMessage); const mockedMob = jest.mocked(mob); +const mockedGetGlobalCommitTemplate = jest.mocked(getGlobalCommitTemplate); +const mockedGetLocalCommitTemplate = jest.mocked(getLocalCommitTemplate); describe('Git Mob API', () => { function buildAuthors(keys: string[]) { @@ -33,8 +41,28 @@ describe('Git Mob API', () => { } afterEach(() => { - mockedMob.usingLocalTemplate.mockReset(); - mockedMob.usingGlobalTemplate.mockReset(); + mockedMob.removeGitMobSection.mockReset(); + mockedGetGlobalCommitTemplate.mockReset(); + mockedGetLocalCommitTemplate.mockReset(); + }); + + it('missing author to pick for list throws error', async () => { + const authorKeys = ['ab', 'cd']; + const mockWriteCoAuthors = jest.fn(async () => undefined); + const mockRemoveCoAuthors = jest.fn(async () => ''); + mockedGitAuthors.mockReturnValue(buildMockGitAuthors([...authorKeys, 'ef'])); + + mockedGitMessage.mockReturnValue({ + writeCoAuthors: mockWriteCoAuthors, + readCoAuthors: () => '', + removeCoAuthors: mockRemoveCoAuthors, + }); + + await expect(async () => { + await setCoAuthors([...authorKeys, 'rk']); + }).rejects.toThrow(AuthorNotFound); + + expect(mob.removeGitMobSection).not.toHaveBeenCalled(); }); it('apply co-authors to git config and git message', async () => { @@ -82,7 +110,7 @@ describe('Git Mob API', () => { const mockWriteCoAuthors = jest.fn(); const mockRemoveCoAuthors = jest.fn(); - mockedMob.usingLocalTemplate.mockReturnValue(true); + mockedGetLocalCommitTemplate.mockResolvedValueOnce('template/path'); mockedGitMessage.mockReturnValue({ writeCoAuthors: mockWriteCoAuthors, readCoAuthors: () => '', @@ -91,8 +119,8 @@ describe('Git Mob API', () => { await updateGitTemplate(authorList); - expect(mockedMob.usingLocalTemplate).toBeCalledTimes(1); - expect(mockedMob.getGlobalTemplate).toBeCalledTimes(1); + expect(mockedGetLocalCommitTemplate).toBeCalledTimes(1); + expect(mockedGetGlobalCommitTemplate).toBeCalledTimes(1); expect(mockWriteCoAuthors).toBeCalledTimes(2); expect(mockWriteCoAuthors).toBeCalledWith(authorList); expect(mockRemoveCoAuthors).not.toHaveBeenCalled(); diff --git a/packages/git-mob-core/src/index.ts b/packages/git-mob-core/src/index.ts index 37f4ad1..f824801 100644 --- a/packages/git-mob-core/src/index.ts +++ b/packages/git-mob-core/src/index.ts @@ -1,20 +1,33 @@ import { mob, config } from './commands'; import { Author } from './git-mob-api/author'; +import { AuthorNotFound } from './git-mob-api/errors/author-not-found'; import { gitAuthors } from './git-mob-api/git-authors'; import { gitMessage } from './git-mob-api/git-message'; +import { + localTemplate, + fetchFromGitHub, + getSetCoAuthors, +} from './git-mob-api/git-mob-config'; +import { + getLocalCommitTemplate, + getGlobalCommitTemplate, +} from './git-mob-api/git-config'; import { resolveGitMessagePath, setCommitTemplate, } from './git-mob-api/resolve-git-message-path'; +import { insideWorkTree, topLevelDirectory } from './git-mob-api/git-rev-parse'; +import { getConfig } from './git-mob-api/exec-command'; async function getAllAuthors() { const gitMobAuthors = gitAuthors(); return gitMobAuthors.toList(await gitMobAuthors.read()); } -async function setCoAuthors(keys: string[]) { - await solo(); +async function setCoAuthors(keys: string[]): Promise { const selectedAuthors = pickSelectedAuthors(keys, await getAllAuthors()); + await solo(); + for (const author of selectedAuthors) { mob.gitAddCoAuthor(author.toString()); } @@ -24,28 +37,40 @@ async function setCoAuthors(keys: string[]) { } async function updateGitTemplate(selectedAuthors?: Author[]) { - const usingLocal = mob.usingLocalTemplate(); - const gitTemplate = gitMessage( - resolveGitMessagePath(config.get('commit.template')) - ); + const [usingLocal, templatePath] = await Promise.all([ + getLocalCommitTemplate(), + getConfig('commit.template'), + ]); + + const gitTemplate = gitMessage(resolveGitMessagePath(templatePath)); if (selectedAuthors && selectedAuthors.length > 0) { if (usingLocal) { - await gitMessage(mob.getGlobalTemplate()).writeCoAuthors(selectedAuthors); + await gitMessage(await getGlobalCommitTemplate()).writeCoAuthors( + selectedAuthors + ); } return gitTemplate.writeCoAuthors(selectedAuthors); } if (usingLocal) { - await gitMessage(mob.getGlobalTemplate()).removeCoAuthors(); + await gitMessage(await getGlobalCommitTemplate()).removeCoAuthors(); } return gitTemplate.removeCoAuthors(); } function pickSelectedAuthors(keys: string[], authorMap: Author[]): Author[] { - return authorMap.filter(author => keys.includes(author.key)); + const selectedAuthors = []; + for (const key of keys) { + const author = authorMap.find(author => author.key === key); + + if (!author) throw new AuthorNotFound(key); + selectedAuthors.push(author); + } + + return selectedAuthors; } function getSelectedCoAuthors(allAuthors: Author[]) { @@ -71,8 +96,6 @@ function getPrimaryAuthor() { if (name && email) { return new Author('prime', name, email); } - - return null; } function setPrimaryAuthor(author: Author): void { @@ -92,6 +115,22 @@ export { updateGitTemplate, }; +export const gitMobConfig = { + localTemplate, + fetchFromGitHub, + getSetCoAuthors, +}; + +export const gitConfig = { + getLocalCommitTemplate, + getGlobalCommitTemplate, +}; + +export const gitRevParse = { + insideWorkTree, + topLevelDirectory, +}; + export { saveNewCoAuthors } from './git-mob-api/manage-authors/add-new-coauthor'; export { pathToCoAuthors } from './git-mob-api/git-authors'; export { fetchGitHubAuthors } from './git-mob-api/git-authors/fetch-github-authors'; diff --git a/packages/git-mob-core/src/silent-run.js b/packages/git-mob-core/src/silent-run.js index b6372af..ef9bdc4 100644 --- a/packages/git-mob-core/src/silent-run.js +++ b/packages/git-mob-core/src/silent-run.js @@ -17,17 +17,10 @@ const { getConfig } = require('./config-manager'); * @returns {ChildProcess.SpawnResult} object from child_process.spawnSync */ function silentRun(command) { - const cmdConfig = { shell: true }; + const cmdConfig = { shell: true, encoding: 'utf8' }; const processCwd = getConfig('processCwd'); if (processCwd) cmdConfig.cwd = processCwd; - return spawnSync(command, cmdOptions(cmdConfig)); -} - -function cmdOptions(extendOptions = {}) { - return { - ...extendOptions, - encoding: 'utf8', - }; + return spawnSync(command, cmdConfig); } exports.silentRun = silentRun; diff --git a/packages/git-mob/package.json b/packages/git-mob/package.json index 9b4f90e..6d74d0d 100644 --- a/packages/git-mob/package.json +++ b/packages/git-mob/package.json @@ -2,6 +2,7 @@ "name": "git-mob", "version": "2.4.0", "description": "CLI tool for adding co-authors to commits.", + "homepage": "https://github.com/rkotze/git-mob/blob/master/packages/git-mob/README.md", "scripts": { "build": "rimraf dist && bob", "test:w": "npm run build -- -w -t & env-cmd -f test-helpers/env.js ava --watch --serial --no-worker-threads", @@ -61,12 +62,13 @@ ], "dependencies": { "common-tags": "^1.8.0", - "git-mob-core": "^0.4.0", + "git-mob-core": "^0.5.0", "minimist": "^1.2.6", "update-notifier": "^5.1.0" }, "devDependencies": { "@ava/typescript": "^3.0.1", + "@types/common-tags": "^1.8.3", "@types/node": "^18.7.14", "@types/sinon": "^10.0.13", "ava": "^5.0.1", diff --git a/packages/git-mob/src/check-author.spec.js b/packages/git-mob/src/check-author.spec.ts similarity index 71% rename from packages/git-mob/src/check-author.spec.js rename to packages/git-mob/src/check-author.spec.ts index 9df3bf5..3f7015b 100644 --- a/packages/git-mob/src/check-author.spec.js +++ b/packages/git-mob/src/check-author.spec.ts @@ -1,15 +1,18 @@ import os from 'node:os'; import test from 'ava'; - +import type { Author } from 'git-mob-core'; import { configWarning } from './check-author'; test('does not print warning when config present', t => { - const actual = configWarning({ name: 'John Doe', email: 'jdoe@example.com' }); - t.is(actual, undefined); + const actual = configWarning({ + name: 'John Doe', + email: 'jdoe@example.com', + } as Author); + t.is(actual, ''); }); test('prints warning and missing config when one argument is missing', t => { - const actual = configWarning({ name: 'John Doe', email: '' }); + const actual = configWarning({ name: 'John Doe', email: '' } as Author); const expected = 'Warning: Missing information for the primary author. Set with:' + os.EOL + @@ -18,7 +21,7 @@ test('prints warning and missing config when one argument is missing', t => { }); test('prints warning and missing config when both arguments are missing', t => { - const actual = configWarning({ name: '', email: '' }); + const actual = configWarning({ name: '', email: '' } as Author); const expected = 'Warning: Missing information for the primary author. Set with:' + os.EOL + diff --git a/packages/git-mob/src/check-author.js b/packages/git-mob/src/check-author.ts similarity index 77% rename from packages/git-mob/src/check-author.js rename to packages/git-mob/src/check-author.ts index fbfbc86..ba0fd38 100644 --- a/packages/git-mob/src/check-author.js +++ b/packages/git-mob/src/check-author.ts @@ -1,7 +1,8 @@ import os from 'node:os'; +import type { Author } from 'git-mob-core'; -function configWarning({ name, email }) { - let result; +function configWarning({ name, email }: Author) { + let result = ''; if (name === '' || email === '') { result = 'Warning: Missing information for the primary author. Set with:'; } diff --git a/packages/git-mob/src/git-authors/author-base-format.ts b/packages/git-mob/src/git-authors/author-base-format.ts index 1d2dfe2..c528d5c 100644 --- a/packages/git-mob/src/git-authors/author-base-format.ts +++ b/packages/git-mob/src/git-authors/author-base-format.ts @@ -1,3 +1,5 @@ +import type { Author } from 'git-mob-core'; + export function authorBaseFormat({ name, email }: Author): string { return `${name} <${email}>`; } diff --git a/packages/git-mob/src/git-authors/compose-authors.ts b/packages/git-mob/src/git-authors/compose-authors.ts deleted file mode 100644 index 55e01be..0000000 --- a/packages/git-mob/src/git-authors/compose-authors.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { Author } from 'git-mob-core'; -import { fetchGitHubAuthors } from 'git-mob-core'; -import { saveAuthorList } from '../manage-authors/add-coauthor'; -import { mobConfig } from '../git-mob-commands'; -import { authorBaseFormat } from './author-base-format'; - -async function composeAuthors( - initials: string[], - coAuthorList: AuthorList, - getAuthors = fetchGitHubAuthors, - saveAuthors = saveAuthorList -): Promise { - const missing = findMissingAuthors(initials, coAuthorList); - if (missing.length > 0 && mobConfig.fetchFromGitHub()) { - const fetchedAuthors = await getAuthors(missing, 'git-mob-cli'); - const gitMobList = { - coauthors: { ...coAuthorList, ...transformToAuthorList(fetchedAuthors) }, - }; - await saveAuthors(gitMobList); - return buildFormatAuthorList(initials, gitMobList.coauthors); - } - - return buildFormatAuthorList(initials, coAuthorList); -} - -function findMissingAuthors( - initialList: string[], - coAuthorList: AuthorList -): string[] { - return initialList.filter(initials => !containsAuthor(initials, coAuthorList)); -} - -function containsAuthor(initials: string, coauthors: AuthorList): boolean { - return initials in coauthors; -} - -function transformToAuthorList(authors: Author[]): AuthorList { - const authorList: AuthorList = {}; - - for (const author of authors) { - authorList[author.key] = { - name: author.name, - email: author.email, - }; - } - - return authorList; -} - -function buildFormatAuthorList( - initialsList: string[], - coAuthorList: AuthorList -): string[] { - return initialsList.map(initials => { - if (!containsAuthor(initials, coAuthorList)) { - noAuthorFoundError(initials); - } - - return authorBaseFormat(coAuthorList[initials]); - }); -} - -function noAuthorFoundError(initials: string): Error { - throw new Error(`Author with initials "${initials}" not found!`); -} - -export { findMissingAuthors, composeAuthors }; diff --git a/packages/git-mob/src/git-authors/compose-authors.spec.ts b/packages/git-mob/src/git-authors/save-missing-authors.spec.ts similarity index 51% rename from packages/git-mob/src/git-authors/compose-authors.spec.ts rename to packages/git-mob/src/git-authors/save-missing-authors.spec.ts index 5dadb7b..1e71346 100644 --- a/packages/git-mob/src/git-authors/compose-authors.spec.ts +++ b/packages/git-mob/src/git-authors/save-missing-authors.spec.ts @@ -1,33 +1,18 @@ import test from 'ava'; import type { SinonSandbox, SinonStub } from 'sinon'; import { createSandbox, assert } from 'sinon'; +import { Author } from 'git-mob-core'; import { mobConfig } from '../git-mob-commands'; -import { composeAuthors, findMissingAuthors } from './compose-authors'; - -const authorsJson = { - coauthors: { - jd: { - name: 'Jane Doe', - email: 'jane@findmypast.com', - }, - fb: { - name: 'Frances Bar', - email: 'frances-bar@findmypast.com', - }, - }, -}; +import { saveMissingAuthors, findMissingAuthors } from './save-missing-authors'; + +const savedAuthors = [ + new Author('jd', 'Jane Doe', 'jane@findmypast.com'), + new Author('fb', 'Frances Bar', 'frances-bar@findmypast.com'), +]; const gitHubAuthors = [ - { - key: 'rkotze', - name: 'Richard', - email: 'rich@gitmob.com', - }, - { - key: 'dideler', - name: 'Denis', - email: 'denis@gitmob.com', - }, + new Author('rkotze', 'Richard', 'rich@gitmob.com'), + new Author('dideler', 'Denis', 'denis@gitmob.com'), ]; let sandbox: SinonSandbox; @@ -42,16 +27,31 @@ test.afterEach(() => { sandbox.restore(); }); +test('Search from GitHub not enabled', async t => { + const fetchAuthorsStub = sandbox.stub().resolves(gitHubAuthors); + sandbox.stub(mobConfig, 'fetchFromGitHub').returns(false); + + t.deepEqual( + await saveMissingAuthors( + ['rkotze', 'dideler', 'jd'], + savedAuthors, + fetchAuthorsStub, + saveCoauthorStub + ), + [] + ); +}); + test('Find missing author initials "rkotze" and "dideler" to an array', t => { const missingCoAuthor = findMissingAuthors( ['rkotze', 'dideler', 'jd'], - authorsJson.coauthors + savedAuthors ); t.deepEqual(missingCoAuthor, ['rkotze', 'dideler']); }); test('No missing author initials', t => { - const missingCoAuthor = findMissingAuthors(['jd', 'fb'], authorsJson.coauthors); + const missingCoAuthor = findMissingAuthors(['jd', 'fb'], savedAuthors); t.deepEqual(missingCoAuthor, []); }); @@ -59,9 +59,9 @@ test('Search GitHub for missing co-authors', async t => { const fetchAuthorsStub = sandbox.stub().resolves(gitHubAuthors); sandbox.stub(mobConfig, 'fetchFromGitHub').returns(true); - await composeAuthors( + await saveMissingAuthors( ['rkotze', 'dideler', 'jd'], - authorsJson.coauthors, + savedAuthors, fetchAuthorsStub, saveCoauthorStub ); @@ -74,9 +74,9 @@ test('Create author list from GitHub and co-author file', async t => { const fetchAuthorsStub = sandbox.stub().resolves(gitHubAuthors); sandbox.stub(mobConfig, 'fetchFromGitHub').returns(true); - const authorList = await composeAuthors( + const authorList = await saveMissingAuthors( ['rkotze', 'dideler', 'jd'], - authorsJson.coauthors, + savedAuthors, fetchAuthorsStub, saveCoauthorStub ); @@ -84,84 +84,35 @@ test('Create author list from GitHub and co-author file', async t => { const expectedAuthorList = [ 'Richard ', 'Denis ', - 'Jane Doe ', ]; t.deepEqual(authorList, expectedAuthorList); }); test('Save missing co-author', async t => { - const rkotzeAuthor = [ - { - key: 'rkotze', - name: 'Richard', - email: 'rich@gitmob.com', - }, - ]; + const rkotzeAuthor = [new Author('rkotze', 'Richard', 'rich@gitmob.com')]; const fetchAuthorsStub = sandbox.stub().resolves(rkotzeAuthor); sandbox.stub(mobConfig, 'fetchFromGitHub').returns(true); - await composeAuthors( + await saveMissingAuthors( ['rkotze', 'jd'], - authorsJson.coauthors, + savedAuthors, fetchAuthorsStub, saveCoauthorStub ); - const rkotzeAuthorList = { - rkotze: { - name: 'Richard', - email: 'rich@gitmob.com', - }, - }; + const rkotzeAuthorList = [new Author('rkotze', 'Richard', 'rich@gitmob.com')]; t.notThrows(() => { - assert.calledWith(saveCoauthorStub, { - coauthors: { - ...authorsJson.coauthors, - ...rkotzeAuthorList, - }, - }); + assert.calledWith(saveCoauthorStub, rkotzeAuthorList); }, 'Not called with GitMobCoauthors type'); }); -test('Create author list from co-author file only', async t => { - const fetchAuthorsStub = sandbox.stub().resolves([]); - - const authorList = await composeAuthors( - ['jd'], - authorsJson.coauthors, - fetchAuthorsStub, - saveCoauthorStub - ); - - const expectedAuthorList = ['Jane Doe ']; - - t.deepEqual(authorList, expectedAuthorList); -}); - test('Throw error if author not found', async t => { - const fetchAuthorsStub = sandbox.stub().resolves(gitHubAuthors); + const fetchAuthorsStub = sandbox.stub().rejects(); + sandbox.stub(mobConfig, 'fetchFromGitHub').returns(true); await t.throwsAsync(async () => - composeAuthors( - ['rkotze', 'dideler', 'james'], - authorsJson.coauthors, - fetchAuthorsStub - ) + saveMissingAuthors(['rkotze', 'dideler'], savedAuthors, fetchAuthorsStub) ); }); - -test('Throw error if author not found because fetch from GitHub is false', async t => { - const fetchAuthorsStub = sandbox.stub().resolves(gitHubAuthors); - - const error = await t.throwsAsync(async () => - composeAuthors( - ['rkotze', 'dideler', 'jd'], - authorsJson.coauthors, - fetchAuthorsStub - ) - ); - - t.regex(error ? error.message : '', /author with initials "rkotze" not found!/i); -}); diff --git a/packages/git-mob/src/git-authors/save-missing-authors.ts b/packages/git-mob/src/git-authors/save-missing-authors.ts new file mode 100644 index 0000000..9c61e8a --- /dev/null +++ b/packages/git-mob/src/git-authors/save-missing-authors.ts @@ -0,0 +1,36 @@ +import type { Author } from 'git-mob-core'; +import { fetchGitHubAuthors, saveNewCoAuthors } from 'git-mob-core'; +import { mobConfig } from '../git-mob-commands'; + +async function saveMissingAuthors( + initials: string[], + coAuthorList: Author[], + getAuthors = fetchGitHubAuthors, + saveAuthors = saveNewCoAuthors +): Promise { + if (!mobConfig.fetchFromGitHub()) { + return []; + } + + const missing = findMissingAuthors(initials, coAuthorList); + if (missing.length > 0) { + const fetchedAuthors: Author[] = await getAuthors(missing, 'git-mob-cli'); + await saveAuthors(fetchedAuthors); + return fetchedAuthors.map(author => author.toString()); + } + + return []; +} + +function findMissingAuthors( + initialList: string[], + coAuthorList: Author[] +): string[] { + return initialList.filter(initials => !containsAuthor(initials, coAuthorList)); +} + +function containsAuthor(initials: string, coauthors: Author[]): boolean { + return coauthors.some(author => author.key === initials); +} + +export { findMissingAuthors, saveMissingAuthors }; diff --git a/packages/git-mob/src/git-mob.js b/packages/git-mob/src/git-mob.js deleted file mode 100644 index 0b7fabb..0000000 --- a/packages/git-mob/src/git-mob.js +++ /dev/null @@ -1,164 +0,0 @@ -import os from 'node:os'; -import minimist from 'minimist'; -import { oneLine, stripIndents } from 'common-tags'; - -import { getAllAuthors, getPrimaryAuthor } from 'git-mob-core'; -import { config, revParse } from '../src/git-commands'; -import { gitAuthors } from '../src/git-authors'; -import { gitMessage, gitMessagePath, commitTemplatePath } from '../src/git-message'; -import { checkForUpdates, runHelp, runVersion, printList } from '../src/helpers'; -import { configWarning } from '../src/check-author'; -import { red, yellow } from '../src/colours'; -import { - getCoAuthors, - isCoAuthorSet, - resetMob, - addCoAuthor, - setGitAuthor, - mobConfig, -} from '../src/git-mob-commands'; -import { composeAuthors } from './git-authors/compose-authors'; - -checkForUpdates(); - -const argv = minimist(process.argv.slice(2), { - boolean: ['h', 'v', 'l', 'o'], - - alias: { - h: 'help', - v: 'version', - l: 'list', - o: 'override', - }, -}); - -execute(argv); - -async function execute(args) { - if (args.help) { - runHelp(); - process.exit(0); - } - - if (args.version) { - runVersion(); - process.exit(0); - } - - if (args.list) { - await listCoAuthors(); - process.exit(0); - } - - if (!revParse.insideWorkTree()) { - console.error('Error: not a Git repository'); - process.exit(1); - } - - if (args.override) { - setAuthor(args._); - } else { - runMob(args._); - } -} - -function runMob(args) { - if (args.length === 0) { - printMob(); - if (config.usingLocalTemplate() && isCoAuthorSet()) { - gitMessage(gitMessagePath()).writeCoAuthors(getCoAuthors().split(os.EOL)); - } - } else { - setMob(args); - } -} - -function printMob() { - const gitAuthor = getPrimaryAuthor(); - console.log(author(gitAuthor)); - - if (isCoAuthorSet()) { - console.log(getCoAuthors()); - } - - if (!mobConfig.useLocalTemplate() && config.usingLocalTemplate()) { - console.log( - yellow(stripIndents`Warning: Git Mob uses Git global config. - Using local commit.template could mean your template does not have selected co-authors appended after switching projects. - See: https://github.com/rkotze/git-mob/discussions/81`) - ); - } - - if (configWarning(gitAuthor)) { - console.warn(red(configWarning(gitAuthor))); - } -} - -async function listCoAuthors() { - try { - const coAuthors = await getAllAuthors(); - - printList(coAuthors); - } catch (error) { - console.error(red(`Error: ${error.message}`)); - process.exit(1); - } -} - -async function setMob(initials) { - try { - const instance = gitAuthors(); - const authorList = await instance.read(); - const coauthors = await composeAuthors(initials, authorList.coauthors); - - setCommitTemplate(); - resetMob(); - - for (const coauthor of coauthors) { - addCoAuthor(coauthor); - } - - gitMessage(gitMessagePath()).writeCoAuthors(coauthors); - - if (config.usingLocalTemplate() && config.usingGlobalTemplate()) { - gitMessage(config.getGlobalTemplate()).writeCoAuthors(coauthors); - } - - printMob(); - } catch (error) { - console.error(red(`Error: ${error.message}`)); - if (error.message.includes('not found!')) { - console.log( - yellow( - 'Run "git config --global git-mob-config.github-fetch true" to fetch GitHub authors.' - ) - ); - } - - process.exit(1); - } -} - -async function setAuthor(initials) { - try { - const instance = gitAuthors(); - const authorList = await instance.read(); - const authors = instance.author(initials.shift(), authorList); - - setGitAuthor(authors.name, authors.email); - runMob(initials); - } catch (error) { - console.error(red(`Error: ${error.message}`)); - process.exit(1); - } -} - -function author({ name, email }) { - return oneLine`${name} <${email}>`; -} - -function setCommitTemplate() { - if (!config.hasTemplatePath()) { - config.setTemplatePath(commitTemplatePath()); - } -} diff --git a/packages/git-mob/src/git-mob.spec.js b/packages/git-mob/src/git-mob.spec.ts similarity index 87% rename from packages/git-mob/src/git-mob.spec.js rename to packages/git-mob/src/git-mob.spec.ts index 0dc8714..bdab7ec 100644 --- a/packages/git-mob/src/git-mob.spec.js +++ b/packages/git-mob/src/git-mob.spec.ts @@ -1,5 +1,5 @@ import { EOL } from 'node:os'; -import test, { before, after, afterEach, skip } from 'ava'; +import test from 'ava'; import { stripIndent } from 'common-tags'; import { auto } from 'eol'; import { temporaryDirectory } from 'tempy'; @@ -20,11 +20,15 @@ import { tearDown, } from '../test-helpers'; +const { before, after, afterEach, skip } = test; + before('setup', () => { setup(); + setCoauthorsFile(); }); after.always('final cleanup', () => { + deleteCoauthorsFile(); deleteGitMessageFile(); tearDown(); }); @@ -45,12 +49,13 @@ test('-h prints help', t => { if (process.platform === 'win32') { // Windows tries to open a man page at git-doc/git-mob.html which errors. - skip('--help is intercepted by git launcher on Windows', () => {}); + skip('--help is intercepted by git launcher on Windows', () => null); } else { test('--help is intercepted by git launcher', t => { - const error = t.throws(() => { - exec('git mob --help', { silent: true }); - }); + const error = + t.throws(() => { + exec('git mob --help'); + }) || new Error('No error'); t.regex(error.message, /no manual entry for git-mob/i); }); @@ -69,7 +74,6 @@ test('--version prints version', t => { }); test('--list print a list of available co-authors', t => { - setCoauthorsFile(); const actual = exec('git mob --list').stdout.trimEnd(); const expected = [ 'jd, Jane Doe, jane@findmypast.com', @@ -78,7 +82,6 @@ test('--list print a list of available co-authors', t => { ].join(EOL); t.is(actual, expected); - deleteCoauthorsFile(); }); test('prints only primary author when there is no mob', t => { @@ -91,14 +94,14 @@ test('prints only primary author when there is no mob', t => { test('prints current mob', t => { addAuthor('John Doe', 'jdoe@example.com'); - addCoAuthor('Dennis Ideler', 'dideler@findmypast.com'); - addCoAuthor('Richard Kotze', 'rkotze@findmypast.com'); + addCoAuthor('Jane Doe', 'jane@findmypast.com'); + addCoAuthor('Elliot Alderson', 'ealderson@findmypast.com>'); const actual = exec('git mob').stdout.trimEnd(); const expected = stripIndent` John Doe - Dennis Ideler - Richard Kotze `; + Jane Doe + Elliot Alderson `; t.is(actual, expected); // setting co-authors outside the git mob lifecycle the commit.template @@ -134,14 +137,14 @@ test('hides warning if local git mob config template is used true', t => { test('update local commit template if using one', t => { addAuthor('John Doe', 'jdoe@example.com'); - addCoAuthor('Richard Kotze', 'rkotze@gitmob.com'); + addCoAuthor('Elliot Alderson', 'ealderson@findmypast.com'); exec('git config --local commit.template ".git/.gitmessage"'); exec('git mob').stdout.trimEnd(); const actualGitMessage = readGitMessageFile(); const expectedGitMessage = auto( - [EOL, EOL, 'Co-authored-by: Richard Kotze '].join('') + [EOL, EOL, 'Co-authored-by: Elliot Alderson '].join('') ); t.is(actualGitMessage, expectedGitMessage); @@ -149,7 +152,6 @@ test('update local commit template if using one', t => { }); test('sets mob when co-author initials found', t => { - setCoauthorsFile(); addAuthor('Billy the Kid', 'billy@example.com'); const actual = exec('git mob jd ea').stdout.trimEnd(); @@ -160,12 +162,10 @@ test('sets mob when co-author initials found', t => { `; t.is(actual, expected); - deleteCoauthorsFile(); removeCoAuthors(); }); test('sets mob and override author', t => { - setCoauthorsFile(); addAuthor('Billy the Kid', 'billy@example.com'); const actual = exec('git mob -o jd ea').stdout.trimEnd(); @@ -178,8 +178,18 @@ test('sets mob and override author', t => { removeCoAuthors(); }); +test('Incorrect override author key will show error', t => { + addAuthor('Billy the Kid', 'billy@example.com'); + + const error = + t.throws(() => { + exec('git mob -o kl ea'); + }) || new Error('No error'); + + t.regex(error.message, /error: kl author key not found!/i); +}); + test('overwrites old mob when setting a new mob', t => { - setCoauthorsFile(); setGitMessageFile(); addAuthor('John Doe', 'jdoe@example.com'); @@ -202,12 +212,10 @@ test('overwrites old mob when setting a new mob', t => { Co-authored-by: Elliot Alderson `); t.is(actualGitmessage, expectedGitmessage); - deleteCoauthorsFile(); removeCoAuthors(); }); test('appends co-authors to an existing commit template', t => { - setCoauthorsFile(); setGitMessageFile(); addAuthor('Thomas Anderson', 'neo@example.com'); @@ -225,13 +233,11 @@ test('appends co-authors to an existing commit template', t => { t.is(actualGitMessage, expectedGitMessage); unsetCommitTemplate(); - deleteCoauthorsFile(); removeCoAuthors(); }); test('appends co-authors to a new commit template', t => { deleteGitMessageFile(); - setCoauthorsFile(); addAuthor('Thomas Anderson', 'neo@example.com'); exec('git mob jd ea'); @@ -251,7 +257,6 @@ test('appends co-authors to a new commit template', t => { removeCoAuthors(); unsetCommitTemplate(); - deleteCoauthorsFile(); }); test('warns when used outside of a git repo', t => { @@ -259,9 +264,10 @@ test('warns when used outside of a git repo', t => { const temporaryDir = temporaryDirectory(); process.chdir(temporaryDir); - const error = t.throws(() => { - exec('git mob'); - }); + const error = + t.throws(() => { + exec('git mob'); + }) || new Error('No error'); t.regex(error.message, /not a git repository/i); diff --git a/packages/git-mob/src/git-mob.ts b/packages/git-mob/src/git-mob.ts new file mode 100644 index 0000000..76a8529 --- /dev/null +++ b/packages/git-mob/src/git-mob.ts @@ -0,0 +1,173 @@ +import os from 'node:os'; +import minimist from 'minimist'; +import { stripIndents } from 'common-tags'; + +import { + getAllAuthors, + getPrimaryAuthor, + getSelectedCoAuthors, + gitMobConfig, + gitConfig, + gitRevParse, + setCoAuthors, + setPrimaryAuthor, + updateGitTemplate, + Author, +} from 'git-mob-core'; +import { checkForUpdates, runHelp, runVersion, printList } from './helpers'; +import { configWarning } from './check-author'; +import { red, yellow } from './colours'; +import { saveMissingAuthors } from './git-authors/save-missing-authors'; + +checkForUpdates(); + +const argv = minimist(process.argv.slice(2), { + boolean: ['h', 'v', 'l', 'o'], + + alias: { + h: 'help', + v: 'version', + l: 'list', + o: 'override', + }, +}); + +execute(argv).catch(() => null); + +async function execute(args: minimist.ParsedArgs) { + if (args.help) { + runHelp(); + process.exit(0); + } + + if (args.version) { + runVersion(); + process.exit(0); + } + + if (args.list) { + await listCoAuthors(); + process.exit(0); + } + + if (!gitRevParse.insideWorkTree()) { + console.error(red('Error: not a Git repository')); + process.exit(1); + } + + if (args.override) { + const initial = args._.shift(); + if (initial) { + await setAuthor(initial); + } + + await runMob(args._); + } else { + await runMob(args._); + } +} + +async function runMob(args: string[]) { + if (args.length === 0) { + const gitAuthor = getPrimaryAuthor(); + const [authorList, useLocalTemplate, template] = await Promise.all([ + getAllAuthors(), + gitMobConfig.localTemplate(), + gitConfig.getLocalCommitTemplate(), + ]); + const selectedCoAuthors = getSelectedCoAuthors(authorList); + + printMob(gitAuthor, selectedCoAuthors, useLocalTemplate, template); + + if (template && selectedCoAuthors) { + await updateGitTemplate(selectedCoAuthors); + } + } else { + await setMob(args); + } +} + +function printMob( + gitAuthor: Author | undefined, + selectedCoAuthors: Author[], + useLocalTemplate: boolean, + template: string | undefined +) { + const theAuthor = gitAuthor || new Author('', '', ''); + const authorWarnConfig = configWarning(theAuthor); + if (authorWarnConfig) { + console.log(red(authorWarnConfig)); + process.exit(1); + } + + console.log(theAuthor.toString()); + + if (selectedCoAuthors && selectedCoAuthors.length > 0) { + console.log(selectedCoAuthors.join(os.EOL)); + } + + if (!useLocalTemplate && template) { + console.log( + yellow(stripIndents`Warning: Git Mob uses Git global config. + Using local commit.template could mean your template does not have selected co-authors appended after switching projects. + See: https://github.com/rkotze/git-mob/discussions/81`) + ); + } +} + +async function listCoAuthors() { + try { + const coAuthors = await getAllAuthors(); + + printList(coAuthors); + } catch (error: unknown) { + const authorListError = error as Error; + console.error(red(`listCoAuthors error: ${authorListError.message}`)); + process.exit(1); + } +} + +async function setMob(initials: string[]) { + try { + const authorList = await getAllAuthors(); + await saveMissingAuthors(initials, authorList); + const selectedCoAuthors = await setCoAuthors(initials); + + const [useLocalTemplate, template] = await Promise.all([ + gitMobConfig.localTemplate(), + gitConfig.getLocalCommitTemplate(), + ]); + + const gitAuthor = getPrimaryAuthor(); + printMob(gitAuthor, selectedCoAuthors, useLocalTemplate, template); + } catch (error: unknown) { + const setMobError = error as Error; + console.error(red(`setMob error: ${setMobError.message}`)); + if (setMobError.message.includes('not found!')) { + console.log( + yellow( + 'Run "git config --global git-mob-config.github-fetch true" to fetch GitHub authors.' + ) + ); + } + + process.exit(1); + } +} + +async function setAuthor(initial: string) { + try { + const authorList = await getAllAuthors(); + const author = authorList.find(author => author.key === initial); + + if (!author) { + throw new Error(`${initial} author key not found!`); + } + + setPrimaryAuthor(author); + } catch (error: unknown) { + const setAuthorError = error as Error; + console.error(red(`setAuthor error: ${setAuthorError.message}`)); + process.exit(1); + } +} diff --git a/packages/git-mob/test-helpers/env.js b/packages/git-mob/test-helpers/env.js index 83cab7e..b4b8c17 100644 --- a/packages/git-mob/test-helpers/env.js +++ b/packages/git-mob/test-helpers/env.js @@ -4,6 +4,7 @@ const testHelperPath = path.join(process.cwd(), '/test-helpers'); module.exports = { GITMOB_COAUTHORS_PATH: path.join(testHelperPath, '.git-coauthors'), GITMOB_MESSAGE_PATH: path.join(testHelperPath, '.gitmessage'), + GITMOB_GLOBAL_MESSAGE_PATH: path.join(testHelperPath, '.gitglobalmessage'), NO_UPDATE_NOTIFIER: true, HOME: testHelperPath, GITMOB_TEST_ENV_FOLDER: './test-env', diff --git a/packages/git-mob/test-helpers/index.js b/packages/git-mob/test-helpers/index.js index 2e13e28..f2d21a6 100644 --- a/packages/git-mob/test-helpers/index.js +++ b/packages/git-mob/test-helpers/index.js @@ -30,6 +30,11 @@ function addCoAuthor(name, email) { exec(`git config --global --add git-mob.co-author "${name} <${email}>"`); } +function globalCommitTemplate() { + const tempGlobal = path.join(process.cwd(), '.gitglobalmessage'); + exec(`git config --global --add commit.template ${tempGlobal}`); +} + function removeCoAuthors() { removeGitConfigSection('git-mob'); } @@ -65,12 +70,20 @@ function setGitMessageFile() { A commit body that goes into more detail. `; - fs.writeFileSync(process.env.GITMOB_MESSAGE_PATH, commitMessageTemplate); + writeNewFile(process.env.GITMOB_MESSAGE_PATH, commitMessageTemplate); } catch (error) { console.warn('Failed to create .gitmessage file.', error.message); } } +function writeNewFile(filePath, text) { + try { + fs.writeFileSync(filePath, text); + } catch (error) { + console.warn(`Failed to create ${filePath} file.`, error.message); + } +} + function readGitMessageFile(noFile = false) { if (noFile && !fs.existsSync(process.env.GITMOB_MESSAGE_PATH)) { return undefined; @@ -121,27 +134,22 @@ function readCoauthorsFile() { } function deleteCoauthorsFile() { - try { - if (fs.existsSync(process.env.GITMOB_COAUTHORS_PATH)) { - fs.unlinkSync(process.env.GITMOB_COAUTHORS_PATH); - } - } catch (error) { - console.warn( - 'Test helpers: Failed to delete .git-coauthors file.', - error.message - ); - } + deleteFile(process.env.GITMOB_COAUTHORS_PATH); } function deleteGitMessageFile() { const filePath = process.env.GITMOB_MESSAGE_PATH; + deleteFile(filePath); +} + +function deleteFile(filePath) { try { if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } } catch (error) { console.warn( - 'Test helpers: Failed to delete global .gitmessage file.', + `Test helpers: Failed to delete global ${filePath} file.`, error.message ); } @@ -151,7 +159,7 @@ function exec(command) { const spawnString = spawnSync(command, { encoding: 'utf8', shell: true }); if (spawnString.status !== 0) { - throw new Error(`GitMob handleResponse: "${command}" + throw new Error(`GitMob test helper: "${command}" stdout: ${spawnString.stdout} --- stderr: ${spawnString.stderr}`); @@ -214,12 +222,15 @@ function setup() { } catch (error) { console.log(error); } - + writeNewFile(process.env.GITMOB_GLOBAL_MESSAGE_PATH, ''); + globalCommitTemplate(); process.chdir(testDir); exec('git init -q'); } function tearDown() { + safelyRemoveGitConfigSection('commit'); + deleteFile(process.env.GITMOB_GLOBAL_MESSAGE_PATH); process.chdir(process.env.HOME); /* eslint n/no-unsupported-features/node-builtins: 0 */ fs.rmSync(testDir, { recursive: true });