diff --git a/lib/gitWorkflow.js b/lib/gitWorkflow.js index 182d871c5..5a9c6f950 100644 --- a/lib/gitWorkflow.js +++ b/lib/gitWorkflow.js @@ -42,16 +42,13 @@ class GitWorkflow { * These three files hold state about an ongoing git merge * Resolve paths during constructor */ - this.mergeHeadFile = path.resolve(this.gitDir, '.git', MERGE_HEAD) - this.mergeModeFile = path.resolve(this.gitDir, '.git', MERGE_MODE) - this.mergeMsgFile = path.resolve(this.gitDir, '.git', MERGE_MSG) + this.mergeHeadFilename = path.resolve(this.gitDir, '.git', MERGE_HEAD) + this.mergeModeFilename = path.resolve(this.gitDir, '.git', MERGE_MODE) + this.mergeMsgFilename = path.resolve(this.gitDir, '.git', MERGE_MSG) } /** * Get name of backup stash - * - * @param {Object} [options] - * @returns {Promise} */ async getBackupStash() { const stashes = await this.execGit(['stash', 'list']) @@ -59,37 +56,56 @@ class GitWorkflow { return `stash@{${index}}` } + /** + * Save meta information about ongoing git merge + */ + async backupMergeStatus() { + debug('Detected current merge mode!') + debug('Backing up merge state...') + await Promise.all([ + readBufferFromFile(this.mergeHeadFilename).then(buffer => (this.mergeHeadBuffer = buffer)), + readBufferFromFile(this.mergeModeFilename).then(buffer => (this.mergeModeBuffer = buffer)), + readBufferFromFile(this.mergeMsgFilename).then(buffer => (this.mergeMsgBuffer = buffer)) + ]) + debug('Done backing up merge state!') + } + + /** + * Restore meta information about ongoing git merge + */ + async restoreMergeStatus() { + debug('Detected backup merge state!') + debug('Restoring merge state...') + await Promise.all([ + writeBufferToFile(this.mergeHeadFilename, this.mergeHeadBuffer), + writeBufferToFile(this.mergeModeFilename, this.mergeModeBuffer), + writeBufferToFile(this.mergeMsgFilename, this.mergeMsgBuffer) + ]) + debug('Done restoring merge state!') + } + /** * Create backup stashes, one of everything and one of only staged changes * Staged files are left in the index for running tasks - * - * @param {Object} [options] - * @returns {Promise} */ async stashBackup() { debug('Backing up original state...') - // Git stash loses metadata about a possible merge mode + // the `git stash` clears metadata about a possible git merge // Manually check and backup if necessary - if (await checkFile(this.mergeHeadFile)) { - debug('Detected current merge mode!') - debug('Backing up merge state...') - await Promise.all([ - readBufferFromFile(this.mergeHeadFile).then( - mergeHead => (this.mergeHeadBuffer = mergeHead) - ), - readBufferFromFile(this.mergeModeFile).then( - mergeMode => (this.mergeModeBuffer = mergeMode) - ), - readBufferFromFile(this.mergeMsgFile).then(mergeMsg => (this.mergeMsgBuffer = mergeMsg)) - ]) - debug('Done backing up merge state!') + if (await checkFile(this.mergeHeadFilename)) { + await this.backupMergeStatus() } // Save stash of entire original state, including unstaged and untracked changes. // `--keep-index leaves only staged files on disk, for tasks.` await this.execGit(['stash', 'save', '--quiet', '--include-untracked', '--keep-index', STASH]) + // Restore meta information about ongoing git merge + if (this.mergeHeadBuffer) { + await this.restoreMergeStatus() + } + // There is a bug in git =< 2.13.0 where `--keep-index` resurrects deleted files. // These files should be listed and deleted before proceeding. await cleanUntrackedFiles(this.execGit) @@ -112,9 +128,6 @@ class GitWorkflow { /** * Applies back task modifications, and unstaged changes hidden in the stash. * In case of a merge-conflict retry with 3-way merge. - * - * @param {Object} [options] - * @returns {Promise} */ async applyModifications() { let modifiedFiles = await this.execGit(['ls-files', '--modified']) @@ -165,40 +178,28 @@ class GitWorkflow { /** * Restore original HEAD state in case of errors - * - * @param {Object} [options] - * @returns {Promise} */ async restoreOriginalState() { debug('Restoring original state...') - const original = await this.getBackupStash() + const backupStash = await this.getBackupStash() await this.execGit(['reset', '--hard', 'HEAD']) - await this.execGit(['stash', 'apply', '--quiet', '--index', original]) + await this.execGit(['stash', 'apply', '--quiet', '--index', backupStash]) debug('Done restoring original state!') + + // Restore meta information about ongoing git merge + if (this.mergeHeadBuffer) { + await this.restoreMergeStatus() + } } /** * Drop the created stashes after everything has run - * - * @param {Object} [options] - * @returns {Promise} */ async dropBackup() { debug('Dropping backup stash...') - const original = await this.getBackupStash() - await this.execGit(['stash', 'drop', '--quiet', original]) + const backupStash = await this.getBackupStash() + await this.execGit(['stash', 'drop', '--quiet', backupStash]) debug('Done dropping backup stash!') - - if (this.mergeHeadBuffer) { - debug('Detected backup merge state!') - debug('Restoring merge state...') - await Promise.all([ - writeBufferToFile(this.mergeHeadFile, this.mergeHeadBuffer), - writeBufferToFile(this.mergeModeFile, this.mergeModeBuffer), - writeBufferToFile(this.mergeMsgFile, this.mergeMsgBuffer) - ]) - debug('Done restoring merge state!') - } } } diff --git a/test/runAll.unmocked.spec.js b/test/runAll.unmocked.spec.js index 6c14c310e..7b1c2fe06 100644 --- a/test/runAll.unmocked.spec.js +++ b/test/runAll.unmocked.spec.js @@ -369,17 +369,17 @@ describe('runAll', () => { // But local modifications are gone expect(await execGit(['diff'])).not.toEqual(diff) expect(await execGit(['diff'])).toMatchInlineSnapshot(` - "diff --git a/test.js b/test.js - index f80f875..1c5643c 100644 - --- a/test.js - +++ b/test.js - @@ -1,3 +1,3 @@ - module.exports = { - - 'foo': 'bar', - -} - + foo: \\"bar\\" - +};" - `) + "diff --git a/test.js b/test.js + index f80f875..1c5643c 100644 + --- a/test.js + +++ b/test.js + @@ -1,3 +1,3 @@ + module.exports = { + - 'foo': 'bar', + -} + + foo: \\"bar\\" + +};" + `) expect(await readFile('test.js')).not.toEqual(testJsFileUgly + appended) expect(await readFile('test.js')).toEqual(testJsFilePretty) @@ -433,13 +433,13 @@ describe('runAll', () => { } expect(await readFile('test.js')).toMatchInlineSnapshot(` - "<<<<<<< HEAD - module.exports = \\"foo\\"; - ======= - module.exports = \\"bar\\"; - >>>>>>> branch-b - " - `) + "<<<<<<< HEAD + module.exports = \\"foo\\"; + ======= + module.exports = \\"bar\\"; + >>>>>>> branch-b + " + `) // Fix conflict and commit using lint-staged await writeFile('test.js', fileInBranchB) @@ -453,15 +453,74 @@ describe('runAll', () => { // Nothing is wrong, so a new commit is created and file is pretty expect(await execGit(['rev-list', '--count', 'HEAD'])).toEqual('4') expect(await execGit(['log', '-1', '--pretty=%B'])).toMatchInlineSnapshot(` - "Merge branch 'branch-b' + "Merge branch 'branch-b' - # Conflicts: - # test.js - " - `) + # Conflicts: + # test.js + " + `) expect(await readFile('test.js')).toEqual(fileInBranchBFixed) }) + it('should handle merge conflict when task errors', async () => { + const fileInBranchA = `module.exports = "foo";\n` + const fileInBranchB = `module.exports = 'bar'\n` + const fileInBranchBFixed = `module.exports = "bar";\n` + + // Create one branch + await execGit(['checkout', '-b', 'branch-a']) + await appendFile('test.js', fileInBranchA) + await execGit(['add', '.']) + await gitCommit(fixJsConfig, ['-m commit a']) + expect(await readFile('test.js')).toEqual(fileInBranchA) + + await execGit(['checkout', 'master']) + + // Create another branch + await execGit(['checkout', '-b', 'branch-b']) + await appendFile('test.js', fileInBranchB) + await execGit(['add', '.']) + await gitCommit(fixJsConfig, ['-m commit b']) + expect(await readFile('test.js')).toEqual(fileInBranchBFixed) + + // Merge first branch + await execGit(['checkout', 'master']) + await execGit(['merge', 'branch-a']) + expect(await readFile('test.js')).toEqual(fileInBranchA) + expect(await execGit(['log', '-1', '--pretty=%B'])).toMatch('commit a') + + // Merge second branch, causing merge conflict + try { + await execGit(['merge', 'branch-b']) + } catch (error) { + expect(error.message).toMatch('Merge conflict in test.js') + } + + expect(await readFile('test.js')).toMatchInlineSnapshot(` + "<<<<<<< HEAD + module.exports = \\"foo\\"; + ======= + module.exports = \\"bar\\"; + >>>>>>> branch-b + " + `) + + // Fix conflict and commit using lint-staged + await writeFile('test.js', fileInBranchB) + expect(await readFile('test.js')).toEqual(fileInBranchB) + await execGit(['add', '.']) + + // Do not use `gitCommit` wrapper here + await expect( + runAll({ config: { '*.js': 'prettier --list-different' }, cwd, quiet: true }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Something went wrong"`) + + // Something went wrong, so runAll failed and merge is still going + expect(await execGit(['rev-list', '--count', 'HEAD'])).toEqual('2') + expect(await execGit(['status'])).toMatch('All conflicts fixed but you are still merging') + expect(await readFile('test.js')).toEqual(fileInBranchB) + }) + it('should keep untracked files', async () => { // Stage pretty file await appendFile('test.js', testJsFilePretty)