Skip to content

Commit

Permalink
Fix remote history check - check if git fetch needs to be run (#685)
Browse files Browse the repository at this point in the history
  • Loading branch information
tommy-mitchell committed Apr 6, 2023
1 parent a6ce792 commit a5d4c3d
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 66 deletions.
34 changes: 26 additions & 8 deletions source/git-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,21 +133,39 @@ export const verifyWorkingTreeIsClean = async () => {
}
};

export const isRemoteHistoryClean = async () => {
let history;
try { // Gracefully handle no remote set up.
const {stdout} = await execa('git', ['rev-list', '--count', '--left-only', '@{u}...HEAD']);
history = stdout;
} catch {}

if (history && history !== '0') {
const hasRemote = async () => {
try {
await execa('git', ['rev-parse', '@{u}']);
} catch { // Has no remote if command fails
return false;
}

return true;
};

const hasUnfetchedChangesFromRemote = async () => {
const {stdout: possibleNewChanges} = await execa('git', ['fetch', '--dry-run']);

// There are no unfetched changes if output is empty.
return !possibleNewChanges || possibleNewChanges === '';
};

const isRemoteHistoryClean = async () => {
const {stdout: history} = await execa('git', ['rev-list', '--count', '--left-only', '@{u}...HEAD']);

// Remote history is clean if there are 0 revisions.
return history === '0';
};

export const verifyRemoteHistoryIsClean = async () => {
if (!(await hasRemote())) {
return;
}

if (!(await hasUnfetchedChangesFromRemote())) {
throw new Error('Remote history differs. Please run `git fetch` and pull changes.');
}

if (!(await isRemoteHistoryClean())) {
throw new Error('Remote history differs. Please pull changes.');
}
Expand Down
44 changes: 23 additions & 21 deletions test/_utils.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,36 @@
import esmock from 'esmock';
import sinon from 'sinon';
import {execa} from 'execa';
import {SilentRenderer} from './fixtures/listr-renderer.js';

export const _stubExeca = source => async (t, commands) => esmock(source, {}, {
execa: {
async execa(...args) {
const results = await Promise.all(commands.map(async result => {
const argsMatch = await t.try(tt => {
const [command, ...commandArgs] = result.command.split(' ');
tt.deepEqual(args, [command, commandArgs]);
});
const makeExecaStub = commands => {
const stub = sinon.stub();

if (argsMatch.passed) {
argsMatch.discard();
for (const result of commands) {
const [command, ...commandArgs] = result.command.split(' ');

if (!result.exitCode || result.exitCode === 0) {
return result;
}
// Command passes if the exit code is 0, or if there's no exit code and no stderr.
const passes = result.exitCode === 0 || (!result.exitCode && !result.stderr);

throw result;
}
if (passes) {
stub.withArgs(command, commandArgs).resolves(result);
} else {
stub.withArgs(command, commandArgs).rejects(Object.assign(new Error(), result)); // eslint-disable-line unicorn/error-message
}
}

return stub;
};

argsMatch.discard();
}));
export const _stubExeca = source => async commands => {
const execaStub = makeExecaStub(commands);

const result = results.filter(Boolean).at(0);
return result ?? execa(...args);
return esmock(source, {}, {
execa: {
execa: async (...args) => execaStub.resolves(execa(...args))(...args),
},
},
});
});
};

export const run = async listr => {
listr.setRenderer(SilentRenderer);
Expand Down
106 changes: 83 additions & 23 deletions test/git-tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ test.afterEach(() => {
});

test.serial('should fail when release branch is not specified, current branch is not the release branch, and publishing from any branch not permitted', async t => {
const gitTasks = await stubExeca(t, [{
const gitTasks = await stubExeca([{
command: 'git symbolic-ref --short HEAD',
exitCode: 0,
stdout: 'feature',
}]);

Expand All @@ -30,9 +29,8 @@ test.serial('should fail when release branch is not specified, current branch is
});

test.serial('should fail when current branch is not the specified release branch and publishing from any branch not permitted', async t => {
const gitTasks = await stubExeca(t, [{
const gitTasks = await stubExeca([{
command: 'git symbolic-ref --short HEAD',
exitCode: 0,
stdout: 'feature',
}]);

Expand All @@ -45,21 +43,26 @@ test.serial('should fail when current branch is not the specified release branch
});

test.serial('should not fail when current branch not master and publishing from any branch permitted', async t => {
const gitTasks = await stubExeca(t, [
const gitTasks = await stubExeca([
{
command: 'git symbolic-ref --short HEAD',
exitCode: 0,
stdout: 'feature',
},
{
command: 'git status --porcelain',
exitCode: 0,
stdout: '',
},
{
command: 'git rev-list --count --left-only @{u}...HEAD',
command: 'git rev-parse @{u}',
exitCode: 0,
stdout: '',
},
{
command: 'git fetch --dry-run',
exitCode: 0,
},
{
command: 'git rev-list --count --left-only @{u}...HEAD',
stdout: '0',
},
]);

Expand All @@ -71,15 +74,13 @@ test.serial('should not fail when current branch not master and publishing from
});

test.serial('should fail when local working tree modified', async t => {
const gitTasks = await stubExeca(t, [
const gitTasks = await stubExeca([
{
command: 'git symbolic-ref --short HEAD',
exitCode: 0,
stdout: 'master',
},
{
command: 'git status --porcelain',
exitCode: 0,
stdout: 'M source/git-tasks.js',
},
]);
Expand All @@ -92,22 +93,48 @@ test.serial('should fail when local working tree modified', async t => {
assertTaskFailed(t, 'Check local working tree');
});

test.serial('should fail when remote history differs', async t => {
const gitTasks = await stubExeca(t, [
test.serial('should not fail when no remote set up', async t => {
const gitTasks = await stubExeca([
{
command: 'git symbolic-ref --short HEAD',
exitCode: 0,
stdout: 'master',
},
{
command: 'git status --porcelain',
exitCode: 0,
stdout: '',
},
{
command: 'git rev-list --count --left-only @{u}...HEAD',
command: 'git rev-parse @{u}',
stderr: 'fatal: no upstream configured for branch \'master\'',
},
]);

await t.notThrowsAsync(
run(gitTasks({branch: 'master'})),
);
});

test.serial('should fail when remote history differs and changes are fetched', async t => {
const gitTasks = await stubExeca([
{
command: 'git symbolic-ref --short HEAD',
stdout: 'master',
},
{
command: 'git status --porcelain',
stdout: '',
},
{
command: 'git rev-parse @{u}',
exitCode: 0,
},
{
command: 'git fetch --dry-run',
exitCode: 0,
stdout: '1',
},
{
command: 'git rev-list --count --left-only @{u}...HEAD',
stdout: '1', // Has unpulled changes
},
]);

Expand All @@ -119,23 +146,56 @@ test.serial('should fail when remote history differs', async t => {
assertTaskFailed(t, 'Check remote history');
});

test.serial('checks should pass when publishing from master, working tree is clean and remote history not different', async t => {
const gitTasks = await stubExeca(t, [
test.serial('should fail when remote has unfetched changes', async t => {
const gitTasks = await stubExeca([
{
command: 'git symbolic-ref --short HEAD',
exitCode: 0,
stdout: 'master',
},
{
command: 'git status --porcelain',
exitCode: 0,
stdout: '',
},
{
command: 'git rev-list --count --left-only @{u}...HEAD',
command: 'git rev-parse @{u}',
exitCode: 0,
},
{
command: 'git fetch --dry-run',
stdout: 'From https://github.com/sindresorhus/np', // Has unfetched changes
},
]);

await t.throwsAsync(
run(gitTasks({branch: 'master'})),
{message: 'Remote history differs. Please run `git fetch` and pull changes.'},
);

assertTaskFailed(t, 'Check remote history');
});

test.serial('checks should pass when publishing from master, working tree is clean and remote history not different', async t => {
const gitTasks = await stubExeca([
{
command: 'git symbolic-ref --short HEAD',
stdout: 'master',
},
{
command: 'git status --porcelain',
stdout: '',
},
{
command: 'git rev-parse @{u}',
exitCode: 0,
},
{
command: 'git fetch --dry-run',
exitCode: 0,
},
{
command: 'git rev-list --count --left-only @{u}...HEAD',
stdout: '0',
},
]);

await t.notThrowsAsync(
Expand Down

0 comments on commit a5d4c3d

Please sign in to comment.