Skip to content

Commit

Permalink
fix: restore metadata about git merge before running tasks
Browse files Browse the repository at this point in the history
  • Loading branch information
iiroj committed Dec 16, 2019
1 parent 22ba124 commit f8ddfc2
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 70 deletions.
95 changes: 48 additions & 47 deletions lib/gitWorkflow.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,54 +42,70 @@ 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<Object>}
*/
async getBackupStash() {
const stashes = await this.execGit(['stash', 'list'])
const index = stashes.split('\n').findIndex(line => line.includes(STASH))
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<void>}
*/
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)
Expand All @@ -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<void>}
*/
async applyModifications() {
let modifiedFiles = await this.execGit(['ls-files', '--modified'])
Expand Down Expand Up @@ -165,40 +178,28 @@ class GitWorkflow {

/**
* Restore original HEAD state in case of errors
*
* @param {Object} [options]
* @returns {Promise<void>}
*/
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<void>}
*/
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!')
}
}
}

Expand Down
105 changes: 82 additions & 23 deletions test/runAll.unmocked.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down

0 comments on commit f8ddfc2

Please sign in to comment.