From f070a7b85d7cb29244d5d413d2901f371de81f3e Mon Sep 17 00:00:00 2001 From: Hiroki Osame Date: Mon, 20 Mar 2023 20:41:22 +0900 Subject: [PATCH 01/10] feat: preserve commit log BREAKING CHANGE: No longer creates orphaned commits by default --- src/index.ts | 68 +++++++++++++++++++++++++++++++++------------------- src/utils.ts | 4 +++- 2 files changed, 47 insertions(+), 25 deletions(-) diff --git a/src/index.ts b/src/index.ts index e1aca38..51f459c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ import task from 'tasuku'; import { cli } from 'cleye'; import packlist from 'npm-packlist'; import { name, version, description } from '../package.json'; -import { assertCleanTree, getCurrentBranchOrTagName, readJson } from './utils'; +import { assertCleanTree, getCurrentBranchOrTagName, readJson, gitStatusTracked } from './utils'; const { stringify } = JSON; @@ -65,14 +65,41 @@ const { stringify } = JSON; if (dry) { setStatus('Dry run'); } - + const localTemporaryBranch = `git-publish/${publishBranch}-${Date.now()}`; let success = false; // In the try-finally block in case it modifies the working tree // On failure, they will be reverted by the hard reset try { - let publishFiles: string[] = []; + const checkoutBranch = await task('Checking out branch', async ({ setWarning, setTitle }) => { + if (dry) { + setWarning(''); + return; + } + + const gitFetch = await execa('git', ['fetch', remote, publishBranch], { + reject: false, + }); + + if (gitFetch.failed) { + await execa('git', ['checkout', '-b', localTemporaryBranch]); + } else { + await execa('git', ['checkout', '-b', localTemporaryBranch, remote + '/' + publishBranch]); + } + + // Remove all files from Git + await execa('git', ['rm', '--cached', '-r', ':/'], { + reject: false, // Can fail if tree is empty: fatal: pathspec ':/' did not match any files + }); + + // Restore the files tree from the previous branch + await execa('git', ['restore', '--source', currentBranch, ':/']); + }); + + if (!dry) { + checkoutBranch.clear(); + } const runHooks = await task('Running hooks', async ({ setWarning, setTitle }) => { if (dry) { @@ -91,6 +118,7 @@ const { stringify } = JSON; runHooks.clear(); } + let publishFiles: string[]; const getPublishFiles = await task('Getting publish files', async ({ setWarning }) => { if (dry) { setWarning(''); @@ -161,22 +189,6 @@ const { stringify } = JSON; removeHooks.clear(); } - const checkoutBranch = await task(`Checking out branch ${stringify(publishBranch)}`, async ({ setWarning }) => { - if (dry) { - setWarning(''); - return; - } - - await execa('git', ['checkout', '--orphan', localTemporaryBranch]); - - // Unstage all files - await execa('git', ['reset']); - }); - - if (!dry) { - checkoutBranch.clear(); - } - const commit = await task('Commiting publish assets', async ({ setWarning }) => { if (dry) { setWarning(''); @@ -184,7 +196,14 @@ const { stringify } = JSON; } await execa('git', ['add', '-f', ...publishFiles]); - await execa('git', ['commit', '--no-verify', '-m', `Published branch ${stringify(currentBranch)}`]); + + const { stdout: trackedFiles } = await gitStatusTracked(); + if (trackedFiles.length === 0) { + console.warn('⚠️ No new changes found to commit.'); + } else { + // -a is passed in so it can stage deletions from `git restore` + await execa('git', ['commit', '--no-verify', '-am', `Published branch ${stringify(currentBranch)}`]); + } }); if (!dry) { @@ -192,15 +211,14 @@ const { stringify } = JSON; } const push = await task( - `Force pushing branch ${stringify(publishBranch)} to remote ${stringify(remote)}`, + `Pushing branch ${stringify(publishBranch)} to remote ${stringify(remote)}`, async ({ setWarning }) => { if (dry) { setWarning(''); return; } - await execa('git', ['push', '--no-verify', '-f', remote, `${localTemporaryBranch}:${publishBranch}`]); - + await execa('git', ['push', '--no-verify', remote, `${localTemporaryBranch}:${publishBranch}`]); success = true; }, ); @@ -221,7 +239,9 @@ const { stringify } = JSON; await execa('git', ['checkout', '-f', currentBranch]); // Delete local branch - await execa('git', ['branch', '-D', localTemporaryBranch]); + await execa('git', ['branch', '-D', localTemporaryBranch], { + reject: false, // Ignore failures (e.g. in case it didin't even succeed to create this branch) + }); }); revertBranch.clear(); diff --git a/src/utils.ts b/src/utils.ts index 4d5c967..b7b3617 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,8 +1,10 @@ import fs from 'fs'; import { execa } from 'execa'; +export const gitStatusTracked = () => execa('git', ['status', '--porcelain', '--untracked-files=no']); + export async function assertCleanTree() { - const { stdout } = await execa('git', ['status', '--porcelain', '--untracked-files=no']).catch((error) => { + const { stdout } = await gitStatusTracked().catch((error) => { if (error.stderr.includes('not a git repository')) { throw new Error('Not in a git repository'); } From f754f556ef715873bc9690774891c1b92353870d Mon Sep 17 00:00:00 2001 From: Hiroki Osame Date: Mon, 20 Mar 2023 20:44:43 +0900 Subject: [PATCH 02/10] lint --- src/index.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index 51f459c..33cefab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,9 @@ import task from 'tasuku'; import { cli } from 'cleye'; import packlist from 'npm-packlist'; import { name, version, description } from '../package.json'; -import { assertCleanTree, getCurrentBranchOrTagName, readJson, gitStatusTracked } from './utils'; +import { + assertCleanTree, getCurrentBranchOrTagName, readJson, gitStatusTracked, +} from './utils'; const { stringify } = JSON; @@ -65,14 +67,14 @@ const { stringify } = JSON; if (dry) { setStatus('Dry run'); } - + const localTemporaryBranch = `git-publish/${publishBranch}-${Date.now()}`; let success = false; // In the try-finally block in case it modifies the working tree // On failure, they will be reverted by the hard reset try { - const checkoutBranch = await task('Checking out branch', async ({ setWarning, setTitle }) => { + const checkoutBranch = await task('Checking out branch', async ({ setWarning }) => { if (dry) { setWarning(''); return; @@ -85,12 +87,13 @@ const { stringify } = JSON; if (gitFetch.failed) { await execa('git', ['checkout', '-b', localTemporaryBranch]); } else { - await execa('git', ['checkout', '-b', localTemporaryBranch, remote + '/' + publishBranch]); + await execa('git', ['checkout', '-b', localTemporaryBranch, `${remote}/${publishBranch}`]); } // Remove all files from Git await execa('git', ['rm', '--cached', '-r', ':/'], { - reject: false, // Can fail if tree is empty: fatal: pathspec ':/' did not match any files + // Can fail if tree is empty: fatal: pathspec ':/' did not match any files + reject: false, }); // Restore the files tree from the previous branch @@ -240,7 +243,8 @@ const { stringify } = JSON; // Delete local branch await execa('git', ['branch', '-D', localTemporaryBranch], { - reject: false, // Ignore failures (e.g. in case it didin't even succeed to create this branch) + // Ignore failures (e.g. in case it didin't even succeed to create this branch) + reject: false, }); }); From 00d6cc77090973573beff82c233721d09f85a88b Mon Sep 17 00:00:00 2001 From: Hiroki Osame Date: Mon, 20 Mar 2023 21:06:06 +0900 Subject: [PATCH 03/10] fetch depth 1 --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 33cefab..1d8cf7b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -80,14 +80,14 @@ const { stringify } = JSON; return; } - const gitFetch = await execa('git', ['fetch', remote, publishBranch], { + const gitFetch = await execa('git', ['fetch', '--depth=1', remote, `${publishBranch}:${localTemporaryBranch}`], { reject: false, }); if (gitFetch.failed) { await execa('git', ['checkout', '-b', localTemporaryBranch]); } else { - await execa('git', ['checkout', '-b', localTemporaryBranch, `${remote}/${publishBranch}`]); + await execa('git', ['checkout', localTemporaryBranch]); } // Remove all files from Git From 60a55f3d88aab8353edf1f5fe6b662442cbf7e3a Mon Sep 17 00:00:00 2001 From: Hiroki Osame Date: Mon, 20 Mar 2023 21:18:39 +0900 Subject: [PATCH 04/10] add fresh flag --- src/index.ts | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1d8cf7b..9fc1aa7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,6 +32,12 @@ const { stringify } = JSON; default: 'origin', }, + fresh: { + type: Boolean, + alias: 'f', + description: 'Publish without a commit history. Warning: Force-pushes to remote', + }, + dry: { type: Boolean, alias: 'd', @@ -56,6 +62,7 @@ const { stringify } = JSON; const { branch: publishBranch = `npm/${currentBranch}`, remote, + fresh, dry, } = argv.flags; @@ -80,17 +87,21 @@ const { stringify } = JSON; return; } - const gitFetch = await execa('git', ['fetch', '--depth=1', remote, `${publishBranch}:${localTemporaryBranch}`], { - reject: false, - }); - - if (gitFetch.failed) { - await execa('git', ['checkout', '-b', localTemporaryBranch]); + if (fresh) { + await execa('git', ['checkout', '--orphan', localTemporaryBranch]); } else { - await execa('git', ['checkout', localTemporaryBranch]); + const gitFetch = await execa('git', ['fetch', '--depth=1', remote, `${publishBranch}:${localTemporaryBranch}`], { + reject: false, + }); + + await execa('git', [ + 'checkout', + ...(gitFetch.failed ? ['-b'] : []), + localTemporaryBranch, + ]); } - // Remove all files from Git + // Remove all files from Git tree await execa('git', ['rm', '--cached', '-r', ':/'], { // Can fail if tree is empty: fatal: pathspec ':/' did not match any files reject: false, @@ -221,7 +232,13 @@ const { stringify } = JSON; return; } - await execa('git', ['push', '--no-verify', remote, `${localTemporaryBranch}:${publishBranch}`]); + await execa('git', [ + 'push', + ...(fresh ? ['--force'] : []), + '--no-verify', + remote, + `${localTemporaryBranch}:${publishBranch}`, + ]); success = true; }, ); From 7fa225d0940dbd704f57f4c3314df6127deb8918 Mon Sep 17 00:00:00 2001 From: Hiroki Osame Date: Mon, 20 Mar 2023 21:38:31 +0900 Subject: [PATCH 05/10] swap order of restore --- src/index.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 9fc1aa7..0f0ac34 100644 --- a/src/index.ts +++ b/src/index.ts @@ -101,14 +101,16 @@ const { stringify } = JSON; ]); } + // Checkout the files tree from the previous branch + // This also applies any file deletions from the source branch + await execa('git', ['restore', '--source', currentBranch, ':/']); + // Remove all files from Git tree + // This unstages all files to so only the publish files will be staged later await execa('git', ['rm', '--cached', '-r', ':/'], { // Can fail if tree is empty: fatal: pathspec ':/' did not match any files reject: false, }); - - // Restore the files tree from the previous branch - await execa('git', ['restore', '--source', currentBranch, ':/']); }); if (!dry) { @@ -132,6 +134,7 @@ const { stringify } = JSON; runHooks.clear(); } + // Move to commit let publishFiles: string[]; const getPublishFiles = await task('Getting publish files', async ({ setWarning }) => { if (dry) { From 7355214ceb9471d7775524e193d3e55f6e962742 Mon Sep 17 00:00:00 2001 From: Hiroki Osame Date: Mon, 20 Mar 2023 21:40:45 +0900 Subject: [PATCH 06/10] merge steps --- src/index.ts | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/src/index.ts b/src/index.ts index 0f0ac34..aeaecc6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -134,25 +134,6 @@ const { stringify } = JSON; runHooks.clear(); } - // Move to commit - let publishFiles: string[]; - const getPublishFiles = await task('Getting publish files', async ({ setWarning }) => { - if (dry) { - setWarning(''); - return; - } - - publishFiles = await packlist(); - - if (publishFiles.length === 0) { - throw new Error('No publish files found'); - } - }); - - if (!dry) { - getPublishFiles.clear(); - } - const removeHooks = await task('Removing "prepare" & "prepack" hooks', async ({ setWarning }) => { if (dry) { setWarning(''); @@ -212,6 +193,11 @@ const { stringify } = JSON; return; } + const publishFiles = await packlist(); + if (publishFiles.length === 0) { + throw new Error('No publish files found'); + } + await execa('git', ['add', '-f', ...publishFiles]); const { stdout: trackedFiles } = await gitStatusTracked(); From 9d1a36fdb4c87da63a8fb106fd675163dfa13147 Mon Sep 17 00:00:00 2001 From: Hiroki Osame Date: Mon, 20 Mar 2023 21:49:28 +0900 Subject: [PATCH 07/10] consolidate logic --- src/index.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index aeaecc6..1088b28 100644 --- a/src/index.ts +++ b/src/index.ts @@ -104,13 +104,6 @@ const { stringify } = JSON; // Checkout the files tree from the previous branch // This also applies any file deletions from the source branch await execa('git', ['restore', '--source', currentBranch, ':/']); - - // Remove all files from Git tree - // This unstages all files to so only the publish files will be staged later - await execa('git', ['rm', '--cached', '-r', ':/'], { - // Can fail if tree is empty: fatal: pathspec ':/' did not match any files - reject: false, - }); }); if (!dry) { @@ -198,6 +191,13 @@ const { stringify } = JSON; throw new Error('No publish files found'); } + // Remove all files from Git tree + // This removes all files from the branch so only the publish files will be added + await execa('git', ['rm', '--cached', '-r', ':/'], { + // Can fail if tree is empty: fatal: pathspec ':/' did not match any files + reject: false, + }); + await execa('git', ['add', '-f', ...publishFiles]); const { stdout: trackedFiles } = await gitStatusTracked(); From 1fb16b356628d5c11198ac9d1259e359636e8591 Mon Sep 17 00:00:00 2001 From: GitHub Actions <> Date: Mon, 20 Mar 2023 22:00:49 +0900 Subject: [PATCH 08/10] ci --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 578b6eb..b6f6e2b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,6 +14,9 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + repository: ${{ github.repository }} # For the tests to be able to publish current branch - name: Setup Node.js uses: actions/setup-node@v3 From fec1431e2c979b264a088b630e7804edf1300d2b Mon Sep 17 00:00:00 2001 From: GitHub Actions <> Date: Mon, 20 Mar 2023 22:07:27 +0900 Subject: [PATCH 09/10] ci --- tests/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/index.ts b/tests/index.ts index e396545..28f7315 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -100,7 +100,6 @@ describe('git-publish', ({ describe, test }) => { }); expect(gitPublishProcess.exitCode).toBe(0); - expect(gitPublishProcess.stderr).toBe(''); expect(gitPublishProcess.stdout).toMatch('✔'); }); }); From 0786e12c62cf2081a6afa9a63ae62b65ba5c1d0f Mon Sep 17 00:00:00 2001 From: GitHub Actions <> Date: Mon, 20 Mar 2023 22:11:56 +0900 Subject: [PATCH 10/10] docs --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8b152ff..62cf0c1 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Publish your npm package to a branch on the Git repository: npx git-publish ``` -> **⚠️ Warning:** This command will force-push to the remote branch `npm/`. Make sure there are no unsaved changes there. +This command will publish to the remote branch `npm/`. ### Global install @@ -61,6 +61,7 @@ git-publish | - | - | | `-b, --branch ` | The branch to publish the package to. Defaults to prefixing "npm/" to the current branch or tag name. | | `-r, --remote ` | The remote to push to. (default: `origin`) | +| `-f, --fresh` | Publish without a commit history. Warning: Force-pushes to remote | | `-d, --dry` | Dry run mode. Will not commit or push to the remote. | | `-h, --help` | Show help | | `--version` | Show version | @@ -85,8 +86,8 @@ Like `npm publish`, you can call the build command it in the [`prepack` script]( 1. Run [npm hooks](https://docs.npmjs.com/cli/v8/using-npm/scripts) `prepare` & `prepack` 2. Create a temporary branch by prefixing the current branch with the `npm/` namespace -3. Detect and commit the [npm publish files](https://github.com/npm/npm-packlist) -4. Force push the branch to remote +3. Detect and commit only the [npm publish files](https://github.com/npm/npm-packlist) +4. Push the branch to remote 5. Delete local branch from Step 2 6. Print the installation command for the branch