From 2a0111ba1476a6c68006d1194c8e6ab904eba6ef Mon Sep 17 00:00:00 2001 From: hiroki osame Date: Mon, 20 Mar 2023 09:12:28 -0400 Subject: [PATCH] feat: preserve commit history and add `--fresh` flag (#5) BREAKING CHANGE: By default the published branch preserves the git history. The old behavior is moved to the `--fresh` flag. --- .github/workflows/test.yml | 3 ++ README.md | 7 +-- src/index.ts | 98 +++++++++++++++++++++++++------------- src/utils.ts | 4 +- tests/index.ts | 1 - 5 files changed, 74 insertions(+), 39 deletions(-) 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 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 diff --git a/src/index.ts b/src/index.ts index e1aca38..1088b28 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 } from './utils'; +import { + assertCleanTree, getCurrentBranchOrTagName, readJson, gitStatusTracked, +} from './utils'; const { stringify } = JSON; @@ -30,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', @@ -54,6 +62,7 @@ const { stringify } = JSON; const { branch: publishBranch = `npm/${currentBranch}`, remote, + fresh, dry, } = argv.flags; @@ -72,40 +81,50 @@ const { stringify } = JSON; // 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 runHooks = await task('Running hooks', async ({ setWarning, setTitle }) => { + const checkoutBranch = await task('Checking out branch', async ({ setWarning }) => { if (dry) { setWarning(''); return; } - setTitle('Running hook "prepare"'); - await execa('npm', ['run', '--if-present', 'prepare']); + if (fresh) { + await execa('git', ['checkout', '--orphan', localTemporaryBranch]); + } else { + const gitFetch = await execa('git', ['fetch', '--depth=1', remote, `${publishBranch}:${localTemporaryBranch}`], { + reject: false, + }); + + await execa('git', [ + 'checkout', + ...(gitFetch.failed ? ['-b'] : []), + localTemporaryBranch, + ]); + } - setTitle('Running hook "prepack"'); - await execa('npm', ['run', '--if-present', 'prepack']); + // Checkout the files tree from the previous branch + // This also applies any file deletions from the source branch + await execa('git', ['restore', '--source', currentBranch, ':/']); }); if (!dry) { - runHooks.clear(); + checkoutBranch.clear(); } - const getPublishFiles = await task('Getting publish files', async ({ setWarning }) => { + const runHooks = await task('Running hooks', async ({ setWarning, setTitle }) => { if (dry) { setWarning(''); return; } - publishFiles = await packlist(); + setTitle('Running hook "prepare"'); + await execa('npm', ['run', '--if-present', 'prepare']); - if (publishFiles.length === 0) { - throw new Error('No publish files found'); - } + setTitle('Running hook "prepack"'); + await execa('npm', ['run', '--if-present', 'prepack']); }); if (!dry) { - getPublishFiles.clear(); + runHooks.clear(); } const removeHooks = await task('Removing "prepare" & "prepack" hooks', async ({ setWarning }) => { @@ -161,30 +180,33 @@ const { stringify } = JSON; removeHooks.clear(); } - const checkoutBranch = await task(`Checking out branch ${stringify(publishBranch)}`, async ({ setWarning }) => { + const commit = await task('Commiting publish assets', async ({ setWarning }) => { if (dry) { setWarning(''); return; } - await execa('git', ['checkout', '--orphan', localTemporaryBranch]); + const publishFiles = await packlist(); + if (publishFiles.length === 0) { + throw new Error('No publish files found'); + } - // Unstage all files - await execa('git', ['reset']); - }); + // 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, + }); - if (!dry) { - checkoutBranch.clear(); - } + await execa('git', ['add', '-f', ...publishFiles]); - const commit = await task('Commiting publish assets', async ({ setWarning }) => { - if (dry) { - setWarning(''); - return; + 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)}`]); } - - await execa('git', ['add', '-f', ...publishFiles]); - await execa('git', ['commit', '--no-verify', '-m', `Published branch ${stringify(currentBranch)}`]); }); if (!dry) { @@ -192,15 +214,20 @@ 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', + ...(fresh ? ['--force'] : []), + '--no-verify', + remote, + `${localTemporaryBranch}:${publishBranch}`, + ]); success = true; }, ); @@ -221,7 +248,10 @@ const { stringify } = JSON; await execa('git', ['checkout', '-f', currentBranch]); // Delete local branch - await execa('git', ['branch', '-D', localTemporaryBranch]); + await execa('git', ['branch', '-D', localTemporaryBranch], { + // Ignore failures (e.g. in case it didin't even succeed to create this branch) + reject: false, + }); }); 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'); } 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('✔'); }); });