Skip to content

Commit

Permalink
feat: preserve commit history and add --fresh flag (#5)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: By default the published branch preserves the git history. The old behavior is moved to the `--fresh` flag.
  • Loading branch information
privatenumber committed Mar 20, 2023
1 parent a5c098b commit 2a0111b
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 39 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<current branch>`. Make sure there are no unsaved changes there.
This command will publish to the remote branch `npm/<current branch>`.


### Global install
Expand All @@ -61,6 +61,7 @@ git-publish
| - | - |
| `-b, --branch <branch name>` | The branch to publish the package to. Defaults to prefixing "npm/" to the current branch or tag name. |
| `-r, --remote <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 |
Expand All @@ -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

Expand Down
98 changes: 64 additions & 34 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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',
Expand All @@ -54,6 +62,7 @@ const { stringify } = JSON;
const {
branch: publishBranch = `npm/${currentBranch}`,
remote,
fresh,
dry,
} = argv.flags;

Expand All @@ -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 }) => {
Expand Down Expand Up @@ -161,46 +180,54 @@ 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) {
commit.clear();
}

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;
},
);
Expand All @@ -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();
Expand Down
4 changes: 3 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -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');
}
Expand Down
1 change: 0 additions & 1 deletion tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ describe('git-publish', ({ describe, test }) => {
});

expect(gitPublishProcess.exitCode).toBe(0);
expect(gitPublishProcess.stderr).toBe('');
expect(gitPublishProcess.stdout).toMatch('✔');
});
});

0 comments on commit 2a0111b

Please sign in to comment.