Show files as conflicted in git panel when discard undo creates merge conflicts, handle various edge cases #563
Changes from 18 commits
ca3687e
0763353
c35db7b
5daf3a5
84d451b
97c4d4d
bdc9de2
61eaf73
1d5364a
79a6487
b52b602
65858ee
4cb9d0c
352b9af
a27ed97
e9c46bf
22458df
862c0a5
d8686e1
8ee9dd2
79f901d
b950c38
b247137
cd91dcd
0440a0c
bc4477e
d29cd8a
73941ff
65c38b5
867f9b2
56f158c
0003447
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ import {CompositeDisposable} from 'atom'; | |
|
||
import {GitProcess} from 'git-kitchen-sink'; | ||
import {parse as parseDiff} from 'what-the-diff'; | ||
import dedent from 'dedent-js'; | ||
|
||
import GitPromptServer from './git-prompt-server'; | ||
import AsyncQueue from './async-queue'; | ||
|
@@ -243,7 +244,6 @@ export default class GitShellOutStrategy { | |
const args = ['diff', '--name-status', '--no-renames']; | ||
if (options.staged) { args.push('--staged'); } | ||
if (options.target) { args.push(options.target); } | ||
if (options.diffFilter === 'unmerged') { args.push('--diff-filter=U'); } | ||
const output = await this.exec(args); | ||
|
||
const statusMap = { | ||
|
@@ -508,7 +508,11 @@ export default class GitShellOutStrategy { | |
try { | ||
output = (await this.exec(['hash-object', '-w', filePath], {writeOperation: true})).trim(); | ||
} catch (e) { | ||
output = null; | ||
if (e.stdErr.match(/fatal: Cannot open .*: No such file or directory/)) { | ||
output = null; | ||
} else { | ||
throw e; | ||
} | ||
} | ||
} else if (stdin) { | ||
output = (await this.exec(['hash-object', '-w', '--stdin'], {stdin, writeOperation: true})).trim(); | ||
|
@@ -528,9 +532,9 @@ export default class GitShellOutStrategy { | |
return await this.exec(['cat-file', '-p', sha]); | ||
} | ||
|
||
async mergeFile(currentPath, basePath, otherPath, resultPath) { | ||
async mergeFile(oursPath, commonBasePath, theirsPath, resultPath) { | ||
const args = [ | ||
'merge-file', '-p', currentPath, basePath, otherPath, | ||
'merge-file', '-p', oursPath, commonBasePath, theirsPath, | ||
'-L', 'current', '-L', 'after discard', '-L', 'before discard', | ||
]; | ||
let output; | ||
|
@@ -546,7 +550,23 @@ export default class GitShellOutStrategy { | |
} | ||
} | ||
await writeFile(resultPath, output); | ||
return {filePath: currentPath, resultPath, conflict}; | ||
return {filePath: oursPath, resultPath, conflict}; | ||
} | ||
|
||
async updateIndex(filePath, commonBaseSha, oursSha, theirsSha) { | ||
const fileMode = await this.getFileMode(filePath); | ||
let indexInfo = dedent` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice. I was wondering if there was a library for this but I hadn't bothered to actually look for one yet 😁 |
||
0 0000000000000000000000000000000000000000\t${filePath} | ||
${fileMode} ${commonBaseSha} 1\t${filePath}\n | ||
`; | ||
if (oursSha) { indexInfo += `${fileMode} ${oursSha} 2\t${filePath}\n`; } | ||
if (theirsSha) { indexInfo += `${fileMode} ${theirsSha} 3\t${filePath}\n`; } | ||
return this.exec(['update-index', '--index-info'], {stdin: indexInfo}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should probably be a write op so it doesn't run in parallel with other operations. |
||
} | ||
|
||
async getFileMode(filePath) { | ||
const output = await this.exec(['ls-files', '--stage', '--', filePath]); | ||
return output.slice(0, 6); | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,13 +3,14 @@ import os from 'os'; | |
|
||
import {PartialFileDiscardHistory, WholeFileDiscardHistory} from './discard-history-stores'; | ||
|
||
import {getTempDir} from '../helpers'; | ||
import {getTempDir, copyFile} from '../helpers'; | ||
|
||
export default class DiscardHistory { | ||
constructor(createBlob, expandBlobToFile, mergeFile, {maxHistoryLength} = {}) { | ||
constructor(createBlob, expandBlobToFile, mergeFile, workdirPath, {maxHistoryLength} = {}) { | ||
this.createBlob = createBlob; | ||
this.expandBlobToFile = expandBlobToFile; | ||
this.mergeFile = mergeFile; | ||
this.workdirPath = workdirPath; | ||
this.partialFileHistory = new PartialFileDiscardHistory(maxHistoryLength); | ||
this.wholeFileHistory = new WholeFileDiscardHistory(maxHistoryLength); | ||
} | ||
|
@@ -114,25 +115,37 @@ export default class DiscardHistory { | |
async expandBlobsToFilesInTempFolder(snapshots) { | ||
const tempFolderPath = await getTempDir(path.join(os.tmpdir(), 'github-discard-history-')); | ||
const pathPromises = snapshots.map(async ({filePath, beforeSha, afterSha}) => { | ||
const otherPath = !beforeSha ? null : | ||
const theirsPath = !beforeSha ? null : | ||
await this.expandBlobToFile(path.join(tempFolderPath, `${filePath}-before-discard`), beforeSha); | ||
const basePath = !afterSha ? null : | ||
const commonBasePath = !afterSha ? null : | ||
await this.expandBlobToFile(path.join(tempFolderPath, `${filePath}-after-discard`), afterSha); | ||
const resultPath = path.join(tempFolderPath, `${filePath}-merge-result`); | ||
return {filePath, basePath, otherPath, resultPath}; | ||
const resultPath = path.join(tempFolderPath, `~${filePath}-merge-result`); | ||
return {filePath, commonBasePath, theirsPath, resultPath, theirsSha: beforeSha, commonBaseSha: afterSha}; | ||
}); | ||
return await Promise.all(pathPromises); | ||
} | ||
|
||
async mergeFiles(filePaths) { | ||
const mergeFilePromises = filePaths.map(({filePath, basePath, otherPath, resultPath}) => { | ||
if (otherPath && basePath) { | ||
return this.mergeFile(filePath, basePath, otherPath, resultPath); | ||
} else if (!otherPath && basePath) { // deleted file | ||
// TODO: handle this | ||
return null; | ||
const mergeFilePromises = filePaths.map(async (filePathInfo, i) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This logic is starting to feel a little hairy again. Probably okay to merge, but I continue to wonder if there are more extractions we can do, especially around object modeling. E.g. would it make sense to have a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We couldn't use |
||
const {filePath, commonBasePath, theirsPath, resultPath, theirsSha, commonBaseSha} = filePathInfo; | ||
let mergeResult; | ||
if (theirsPath && commonBasePath) { | ||
mergeResult = await this.mergeFile(filePath, commonBasePath, theirsPath, resultPath); | ||
} else if (!theirsPath && commonBasePath) { // deleted file | ||
const oursSha = await this.createBlob({filePath}); | ||
if (oursSha === commonBaseSha) { // no changes since discard, mark file to be deleted | ||
mergeResult = {filePath, resultPath: null, deleted: true, conflict: false}; | ||
} else { // changes since discard result in conflict | ||
await copyFile(path.join(this.workdirPath, filePath), resultPath); | ||
mergeResult = {filePath, resultPath, conflict: true}; | ||
} | ||
} else if (theirsPath && !commonBasePath) { // added file | ||
// TODO | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What will happen if this PR gets merged before this is implemented? |
||
} else { | ||
throw new Error('One of the following must be defined - theirsPath:' + | ||
`${theirsPath} or commonBasePath: ${commonBasePath}`); | ||
} | ||
return null; | ||
return {...mergeResult, theirsSha, commonBaseSha}; | ||
}); | ||
return await Promise.all(mergeFilePromises); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -74,6 +74,7 @@ export default class Repository { | |
this.createBlob, | ||
this.expandBlobToFile, | ||
this.mergeFile, | ||
this.workingDirectoryPath, | ||
{maxHistoryLength: 60}, | ||
); | ||
if (history) { this.discardHistory.updateHistory(history); } | ||
|
@@ -428,8 +429,12 @@ export default class Repository { | |
} | ||
|
||
@autobind | ||
mergeFile(currentPath, basePath, otherPath, resultPath) { | ||
return this.git.mergeFile(currentPath, basePath, otherPath, resultPath); | ||
mergeFile(oursPath, commonBasePath, theirsPath, resultPath) { | ||
return this.git.mergeFile(oursPath, commonBasePath, theirsPath, resultPath); | ||
} | ||
|
||
updateIndex(filePath, commonBaseSha, oursSha, theirsSha) { | ||
return this.git.updateIndex(filePath, commonBaseSha, oursSha, theirsSha); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a better method name than |
||
} | ||
|
||
async createDiscardHistoryBlob() { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,10 @@ export default class PushPullMenuView extends React.Component { | |
remoteName: React.PropTypes.string, | ||
aheadCount: React.PropTypes.number, | ||
behindCount: React.PropTypes.number, | ||
notificationManager: React.PropTypes.object, | ||
fetch: React.PropTypes.func, | ||
push: React.PropTypes.func, | ||
pull: React.PropTypes.func, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a note for me to remove this same change in #565 |
||
} | ||
|
||
static defaultProps = { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍