mirror of
https://github.com/desktop/desktop
synced 2024-09-19 16:12:20 +00:00
850 lines
29 KiB
TypeScript
850 lines
29 KiB
TypeScript
import * as path from 'path'
|
|
import * as FSE from 'fs-extra'
|
|
|
|
import { Repository } from '../../../src/models/repository'
|
|
import {
|
|
createCommit,
|
|
getCommits,
|
|
getCommit,
|
|
getChangedFiles,
|
|
getWorkingDirectoryDiff,
|
|
createMergeCommit,
|
|
} from '../../../src/lib/git'
|
|
|
|
import {
|
|
setupFixtureRepository,
|
|
setupEmptyRepository,
|
|
setupConflictedRepo,
|
|
setupConflictedRepoWithMultipleFiles,
|
|
} from '../../helpers/repositories'
|
|
|
|
import { GitProcess } from 'dugite'
|
|
import {
|
|
WorkingDirectoryFileChange,
|
|
AppFileStatusKind,
|
|
UnmergedEntrySummary,
|
|
GitStatusEntry,
|
|
isManualConflict,
|
|
} from '../../../src/models/status'
|
|
import {
|
|
DiffSelectionType,
|
|
DiffSelection,
|
|
ITextDiff,
|
|
DiffType,
|
|
} from '../../../src/models/diff'
|
|
import { getStatusOrThrow } from '../../helpers/status'
|
|
import { ManualConflictResolution } from '../../../src/models/manual-conflict-resolution'
|
|
import { isConflictedFile } from '../../../src/lib/status'
|
|
|
|
async function getTextDiff(
|
|
repo: Repository,
|
|
file: WorkingDirectoryFileChange
|
|
): Promise<ITextDiff> {
|
|
const diff = await getWorkingDirectoryDiff(repo, file)
|
|
expect(diff.kind === DiffType.Text)
|
|
return diff as ITextDiff
|
|
}
|
|
|
|
describe('git/commit', () => {
|
|
let repository: Repository
|
|
|
|
beforeEach(async () => {
|
|
const testRepoPath = await setupFixtureRepository('test-repo')
|
|
repository = new Repository(testRepoPath, -1, null, false)
|
|
})
|
|
|
|
describe('createCommit normal', () => {
|
|
it('commits the given files', async () => {
|
|
await FSE.writeFile(path.join(repository.path, 'README.md'), 'Hi world\n')
|
|
|
|
let status = await getStatusOrThrow(repository)
|
|
let files = status.workingDirectory.files
|
|
expect(files.length).toEqual(1)
|
|
|
|
const sha = await createCommit(repository, 'Special commit', files)
|
|
expect(sha).toHaveLength(7)
|
|
|
|
status = await getStatusOrThrow(repository)
|
|
files = status.workingDirectory.files
|
|
expect(files.length).toEqual(0)
|
|
|
|
const commits = await getCommits(repository, 'HEAD', 100)
|
|
expect(commits.length).toEqual(6)
|
|
expect(commits[0].summary).toEqual('Special commit')
|
|
expect(commits[0].sha.substring(0, 7)).toEqual(sha)
|
|
})
|
|
|
|
it('commit does not strip commentary by default', async () => {
|
|
await FSE.writeFile(path.join(repository.path, 'README.md'), 'Hi world\n')
|
|
|
|
const status = await getStatusOrThrow(repository)
|
|
const files = status.workingDirectory.files
|
|
expect(files.length).toEqual(1)
|
|
|
|
const message = `Special commit
|
|
|
|
# this is a comment`
|
|
|
|
const sha = await createCommit(repository, message, files)
|
|
expect(sha).toHaveLength(7)
|
|
|
|
const commit = await getCommit(repository, 'HEAD')
|
|
expect(commit).not.toBeNull()
|
|
expect(commit!.summary).toEqual('Special commit')
|
|
expect(commit!.body).toEqual('# this is a comment\n')
|
|
expect(commit!.shortSha).toEqual(sha)
|
|
})
|
|
|
|
it('can commit for empty repository', async () => {
|
|
const repo = await setupEmptyRepository()
|
|
|
|
await FSE.writeFile(path.join(repo.path, 'foo'), 'foo\n')
|
|
await FSE.writeFile(path.join(repo.path, 'bar'), 'bar\n')
|
|
|
|
const status = await getStatusOrThrow(repo)
|
|
const files = status.workingDirectory.files
|
|
|
|
expect(files.length).toEqual(2)
|
|
|
|
const allChanges = [
|
|
files[0].withIncludeAll(true),
|
|
files[1].withIncludeAll(true),
|
|
]
|
|
|
|
const sha = await createCommit(
|
|
repo,
|
|
'added two files\n\nthis is a description',
|
|
allChanges
|
|
)
|
|
expect(sha).toEqual('(root-commit)')
|
|
|
|
const statusAfter = await getStatusOrThrow(repo)
|
|
|
|
expect(statusAfter.workingDirectory.files.length).toEqual(0)
|
|
|
|
const history = await getCommits(repo, 'HEAD', 2)
|
|
|
|
expect(history.length).toEqual(1)
|
|
expect(history[0].summary).toEqual('added two files')
|
|
expect(history[0].body).toEqual('this is a description\n')
|
|
})
|
|
|
|
it('can commit renames', async () => {
|
|
const repo = await setupEmptyRepository()
|
|
|
|
await FSE.writeFile(path.join(repo.path, 'foo'), 'foo\n')
|
|
|
|
await GitProcess.exec(['add', 'foo'], repo.path)
|
|
await GitProcess.exec(['commit', '-m', 'Initial commit'], repo.path)
|
|
await GitProcess.exec(['mv', 'foo', 'bar'], repo.path)
|
|
|
|
const status = await getStatusOrThrow(repo)
|
|
const files = status.workingDirectory.files
|
|
|
|
expect(files.length).toEqual(1)
|
|
|
|
const sha = await createCommit(repo, 'renamed a file', [
|
|
files[0].withIncludeAll(true),
|
|
])
|
|
expect(sha).toHaveLength(7)
|
|
|
|
const statusAfter = await getStatusOrThrow(repo)
|
|
|
|
expect(statusAfter.workingDirectory.files.length).toEqual(0)
|
|
})
|
|
})
|
|
|
|
describe('createCommit partials', () => {
|
|
beforeEach(async () => {
|
|
const testRepoPath = await setupFixtureRepository('repo-with-changes')
|
|
repository = new Repository(testRepoPath, -1, null, false)
|
|
})
|
|
|
|
it('can commit some lines from new file', async () => {
|
|
const previousTip = (await getCommits(repository, 'HEAD', 1))[0]
|
|
|
|
const newFileName = 'new-file.md'
|
|
|
|
// select first five lines of file
|
|
const selection = DiffSelection.fromInitialSelection(
|
|
DiffSelectionType.None
|
|
).withRangeSelection(0, 5, true)
|
|
|
|
const file = new WorkingDirectoryFileChange(
|
|
newFileName,
|
|
{ kind: AppFileStatusKind.New },
|
|
selection
|
|
)
|
|
|
|
// commit just this change, ignore everything else
|
|
const sha = await createCommit(repository, 'title', [file])
|
|
expect(sha).toHaveLength(7)
|
|
|
|
// verify that the HEAD of the repository has moved
|
|
const newTip = (await getCommits(repository, 'HEAD', 1))[0]
|
|
expect(newTip.sha).not.toEqual(previousTip.sha)
|
|
expect(newTip.summary).toEqual('title')
|
|
expect(newTip.shortSha).toEqual(sha)
|
|
|
|
// verify that the contents of this new commit are just the new file
|
|
const changesetData = await getChangedFiles(repository, newTip.sha)
|
|
expect(changesetData.files.length).toEqual(1)
|
|
expect(changesetData.files[0].path).toEqual(newFileName)
|
|
|
|
// verify that changes remain for this new file
|
|
const status = await getStatusOrThrow(repository)
|
|
expect(status.workingDirectory.files.length).toEqual(4)
|
|
|
|
// verify that the file is now tracked
|
|
const fileChange = status!.workingDirectory.files.find(
|
|
f => f.path === newFileName
|
|
)
|
|
expect(fileChange).not.toBeUndefined()
|
|
expect(fileChange!.status.kind).toEqual(AppFileStatusKind.Modified)
|
|
})
|
|
|
|
it('can commit second hunk from modified file', async () => {
|
|
const previousTip = (await getCommits(repository, 'HEAD', 1))[0]
|
|
|
|
const modifiedFile = 'modified-file.md'
|
|
|
|
const unselectedFile = DiffSelection.fromInitialSelection(
|
|
DiffSelectionType.None
|
|
)
|
|
const file = new WorkingDirectoryFileChange(
|
|
modifiedFile,
|
|
{ kind: AppFileStatusKind.Modified },
|
|
unselectedFile
|
|
)
|
|
|
|
const diff = await getTextDiff(repository, file)
|
|
|
|
const selection = DiffSelection.fromInitialSelection(
|
|
DiffSelectionType.All
|
|
).withRangeSelection(
|
|
diff.hunks[0].unifiedDiffStart,
|
|
diff.hunks[0].unifiedDiffEnd - diff.hunks[0].unifiedDiffStart,
|
|
false
|
|
)
|
|
|
|
const updatedFile = file.withSelection(selection)
|
|
|
|
// commit just this change, ignore everything else
|
|
const sha = await createCommit(repository, 'title', [updatedFile])
|
|
expect(sha).toHaveLength(7)
|
|
|
|
// verify that the HEAD of the repository has moved
|
|
const newTip = (await getCommits(repository, 'HEAD', 1))[0]
|
|
expect(newTip.sha).not.toEqual(previousTip.sha)
|
|
expect(newTip.summary).toEqual('title')
|
|
|
|
// verify that the contents of this new commit are just the modified file
|
|
const changesetData = await getChangedFiles(repository, newTip.sha)
|
|
expect(changesetData.files.length).toEqual(1)
|
|
expect(changesetData.files[0].path).toEqual(modifiedFile)
|
|
|
|
// verify that changes remain for this modified file
|
|
const status = await getStatusOrThrow(repository)
|
|
expect(status.workingDirectory.files.length).toEqual(4)
|
|
|
|
// verify that the file is still marked as modified
|
|
const fileChange = status.workingDirectory.files.find(
|
|
f => f.path === modifiedFile
|
|
)
|
|
expect(fileChange).not.toBeUndefined()
|
|
expect(fileChange!.status.kind).toEqual(AppFileStatusKind.Modified)
|
|
})
|
|
|
|
it('can commit single delete from modified file', async () => {
|
|
const previousTip = (await getCommits(repository, 'HEAD', 1))[0]
|
|
|
|
const fileName = 'modified-file.md'
|
|
|
|
const unselectedFile = DiffSelection.fromInitialSelection(
|
|
DiffSelectionType.None
|
|
)
|
|
const modifiedFile = new WorkingDirectoryFileChange(
|
|
fileName,
|
|
{ kind: AppFileStatusKind.Modified },
|
|
unselectedFile
|
|
)
|
|
|
|
const diff = await getTextDiff(repository, modifiedFile)
|
|
|
|
const secondRemovedLine = diff.hunks[0].unifiedDiffStart + 5
|
|
|
|
const selection = DiffSelection.fromInitialSelection(
|
|
DiffSelectionType.None
|
|
).withRangeSelection(secondRemovedLine, 1, true)
|
|
|
|
const file = new WorkingDirectoryFileChange(
|
|
fileName,
|
|
{ kind: AppFileStatusKind.Modified },
|
|
selection
|
|
)
|
|
|
|
// commit just this change, ignore everything else
|
|
const sha = await createCommit(repository, 'title', [file])
|
|
expect(sha).toHaveLength(7)
|
|
|
|
// verify that the HEAD of the repository has moved
|
|
const newTip = (await getCommits(repository, 'HEAD', 1))[0]
|
|
expect(newTip.sha).not.toEqual(previousTip.sha)
|
|
expect(newTip.summary).toEqual('title')
|
|
expect(newTip.shortSha).toEqual(sha)
|
|
|
|
// verify that the contents of this new commit are just the modified file
|
|
const changesetData = await getChangedFiles(repository, newTip.sha)
|
|
expect(changesetData.files.length).toEqual(1)
|
|
expect(changesetData.files[0].path).toEqual(fileName)
|
|
})
|
|
|
|
it('can commit multiple hunks from modified file', async () => {
|
|
const previousTip = (await getCommits(repository, 'HEAD', 1))[0]
|
|
|
|
const modifiedFile = 'modified-file.md'
|
|
|
|
const unselectedFile = DiffSelection.fromInitialSelection(
|
|
DiffSelectionType.None
|
|
)
|
|
const file = new WorkingDirectoryFileChange(
|
|
modifiedFile,
|
|
{ kind: AppFileStatusKind.Modified },
|
|
unselectedFile
|
|
)
|
|
|
|
const diff = await getTextDiff(repository, file)
|
|
|
|
const selection = DiffSelection.fromInitialSelection(
|
|
DiffSelectionType.All
|
|
).withRangeSelection(
|
|
diff.hunks[1].unifiedDiffStart,
|
|
diff.hunks[1].unifiedDiffEnd - diff.hunks[1].unifiedDiffStart,
|
|
false
|
|
)
|
|
|
|
const updatedFile = new WorkingDirectoryFileChange(
|
|
modifiedFile,
|
|
{ kind: AppFileStatusKind.Modified },
|
|
selection
|
|
)
|
|
|
|
// commit just this change, ignore everything else
|
|
const sha = await createCommit(repository, 'title', [updatedFile])
|
|
expect(sha).toHaveLength(7)
|
|
|
|
// verify that the HEAD of the repository has moved
|
|
const newTip = (await getCommits(repository, 'HEAD', 1))[0]
|
|
expect(newTip.sha).not.toEqual(previousTip.sha)
|
|
expect(newTip.summary).toEqual('title')
|
|
expect(newTip.shortSha).toEqual(sha)
|
|
|
|
// verify that the contents of this new commit are just the modified file
|
|
const changesetData = await getChangedFiles(repository, newTip.sha)
|
|
expect(changesetData.files.length).toEqual(1)
|
|
expect(changesetData.files[0].path).toEqual(modifiedFile)
|
|
|
|
// verify that changes remain for this modified file
|
|
const status = await getStatusOrThrow(repository)
|
|
expect(status.workingDirectory.files.length).toEqual(4)
|
|
|
|
// verify that the file is still marked as modified
|
|
const fileChange = status.workingDirectory.files.find(
|
|
f => f.path === modifiedFile
|
|
)
|
|
expect(fileChange).not.toBeUndefined()
|
|
expect(fileChange!.status.kind).toEqual(AppFileStatusKind.Modified)
|
|
})
|
|
|
|
it('can commit some lines from deleted file', async () => {
|
|
const previousTip = (await getCommits(repository, 'HEAD', 1))[0]
|
|
|
|
const deletedFile = 'deleted-file.md'
|
|
|
|
const selection = DiffSelection.fromInitialSelection(
|
|
DiffSelectionType.None
|
|
).withRangeSelection(0, 5, true)
|
|
|
|
const file = new WorkingDirectoryFileChange(
|
|
deletedFile,
|
|
{ kind: AppFileStatusKind.Deleted },
|
|
selection
|
|
)
|
|
|
|
// commit just this change, ignore everything else
|
|
const sha = await createCommit(repository, 'title', [file])
|
|
expect(sha).toHaveLength(7)
|
|
|
|
// verify that the HEAD of the repository has moved
|
|
const newTip = (await getCommits(repository, 'HEAD', 1))[0]
|
|
expect(newTip.sha).not.toEqual(previousTip.sha)
|
|
expect(newTip.summary).toEqual('title')
|
|
expect(newTip.sha.substring(0, 7)).toEqual(sha)
|
|
|
|
// verify that the contents of this new commit are just the new file
|
|
const changesetData = await getChangedFiles(repository, newTip.sha)
|
|
expect(changesetData.files.length).toEqual(1)
|
|
expect(changesetData.files[0].path).toEqual(deletedFile)
|
|
|
|
// verify that changes remain for this new file
|
|
const status = await getStatusOrThrow(repository)
|
|
expect(status.workingDirectory.files.length).toEqual(4)
|
|
|
|
// verify that the file is now tracked
|
|
const fileChange = status.workingDirectory.files.find(
|
|
f => f.path === deletedFile
|
|
)
|
|
expect(fileChange).not.toBeUndefined()
|
|
expect(fileChange!.status.kind).toEqual(AppFileStatusKind.Deleted)
|
|
})
|
|
|
|
it('can commit renames with modifications', async () => {
|
|
const repo = await setupEmptyRepository()
|
|
|
|
await FSE.writeFile(path.join(repo.path, 'foo'), 'foo\n')
|
|
|
|
await GitProcess.exec(['add', 'foo'], repo.path)
|
|
await GitProcess.exec(['commit', '-m', 'Initial commit'], repo.path)
|
|
await GitProcess.exec(['mv', 'foo', 'bar'], repo.path)
|
|
|
|
await FSE.writeFile(path.join(repo.path, 'bar'), 'bar\n')
|
|
|
|
const status = await getStatusOrThrow(repo)
|
|
const files = status.workingDirectory.files
|
|
|
|
expect(files.length).toEqual(1)
|
|
|
|
const sha = await createCommit(repo, 'renamed a file', [
|
|
files[0].withIncludeAll(true),
|
|
])
|
|
expect(sha).toHaveLength(7)
|
|
|
|
const statusAfter = await getStatusOrThrow(repo)
|
|
|
|
expect(statusAfter.workingDirectory.files.length).toEqual(0)
|
|
expect(statusAfter.currentTip!.substring(0, 7)).toEqual(sha)
|
|
})
|
|
|
|
// The scenario here is that the user has staged a rename (probably using git mv)
|
|
// and then added some lines to the newly renamed file and they only want to
|
|
// commit one of these lines.
|
|
it('can commit renames with partially selected modifications', async () => {
|
|
const repo = await setupEmptyRepository()
|
|
|
|
await FSE.writeFile(path.join(repo.path, 'foo'), 'line1\n')
|
|
|
|
await GitProcess.exec(['add', 'foo'], repo.path)
|
|
await GitProcess.exec(['commit', '-m', 'Initial commit'], repo.path)
|
|
await GitProcess.exec(['mv', 'foo', 'bar'], repo.path)
|
|
|
|
await FSE.writeFile(path.join(repo.path, 'bar'), 'line1\nline2\nline3\n')
|
|
|
|
const status = await getStatusOrThrow(repo)
|
|
const files = status.workingDirectory.files
|
|
|
|
expect(files.length).toEqual(1)
|
|
expect(files[0].path).toContain('bar')
|
|
expect(files[0].status.kind).toEqual(AppFileStatusKind.Renamed)
|
|
|
|
const selection = files[0].selection
|
|
.withSelectNone()
|
|
.withLineSelection(2, true)
|
|
|
|
const partiallySelectedFile = files[0].withSelection(selection)
|
|
|
|
const sha = await createCommit(repo, 'renamed a file', [
|
|
partiallySelectedFile,
|
|
])
|
|
expect(sha).toHaveLength(7)
|
|
|
|
const statusAfter = await getStatusOrThrow(repo)
|
|
|
|
expect(statusAfter.workingDirectory.files.length).toEqual(1)
|
|
|
|
const diff = await getTextDiff(
|
|
repo,
|
|
statusAfter.workingDirectory.files[0]
|
|
)
|
|
|
|
expect(diff.hunks.length).toEqual(1)
|
|
expect(diff.hunks[0].lines.length).toEqual(4)
|
|
expect(diff.hunks[0].lines[3].text).toEqual('+line3')
|
|
})
|
|
})
|
|
|
|
describe('createCommit with a merge conflict', () => {
|
|
it('creates a merge commit', async () => {
|
|
const repo = await setupConflictedRepo()
|
|
const filePath = path.join(repo.path, 'foo')
|
|
|
|
const inMerge = await FSE.pathExists(
|
|
path.join(repo.path, '.git', 'MERGE_HEAD')
|
|
)
|
|
expect(inMerge).toEqual(true)
|
|
|
|
await FSE.writeFile(filePath, 'b1b2')
|
|
|
|
const status = await getStatusOrThrow(repo)
|
|
const files = status.workingDirectory.files
|
|
|
|
expect(files.length).toEqual(1)
|
|
expect(files[0].path).toEqual('foo')
|
|
|
|
expect(files[0].status).toEqual({
|
|
kind: AppFileStatusKind.Conflicted,
|
|
entry: {
|
|
kind: 'conflicted',
|
|
action: UnmergedEntrySummary.BothModified,
|
|
them: GitStatusEntry.UpdatedButUnmerged,
|
|
us: GitStatusEntry.UpdatedButUnmerged,
|
|
},
|
|
conflictMarkerCount: 0,
|
|
})
|
|
|
|
const selection = files[0].selection.withSelectAll()
|
|
const selectedFile = files[0].withSelection(selection)
|
|
const sha = await createCommit(repo, 'Merge commit!', [selectedFile])
|
|
expect(sha).toHaveLength(7)
|
|
|
|
const commits = await getCommits(repo, 'HEAD', 5)
|
|
expect(commits[0].parentSHAs.length).toEqual(2)
|
|
expect(commits[0]!.shortSha).toEqual(sha)
|
|
})
|
|
})
|
|
|
|
describe('createMergeCommit', () => {
|
|
describe('with a simple merge conflict', () => {
|
|
let repository: Repository
|
|
describe('with a merge conflict', () => {
|
|
beforeEach(async () => {
|
|
repository = await setupConflictedRepo()
|
|
})
|
|
it('creates a merge commit', async () => {
|
|
const status = await getStatusOrThrow(repository)
|
|
const trackedFiles = status.workingDirectory.files.filter(
|
|
f => f.status.kind !== AppFileStatusKind.Untracked
|
|
)
|
|
const sha = await createMergeCommit(repository, trackedFiles)
|
|
const newStatus = await getStatusOrThrow(repository)
|
|
expect(sha).toHaveLength(7)
|
|
expect(newStatus.workingDirectory.files).toHaveLength(0)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('with a merge conflict and manual resolutions', () => {
|
|
let repository: Repository
|
|
beforeEach(async () => {
|
|
repository = await setupConflictedRepoWithMultipleFiles()
|
|
})
|
|
it('keeps files chosen to be added and commits', async () => {
|
|
const status = await getStatusOrThrow(repository)
|
|
const trackedFiles = status.workingDirectory.files.filter(
|
|
f => f.status.kind !== AppFileStatusKind.Untracked
|
|
)
|
|
const manualResolutions = new Map([
|
|
['bar', ManualConflictResolution.ours],
|
|
])
|
|
const sha = await createMergeCommit(
|
|
repository,
|
|
trackedFiles,
|
|
manualResolutions
|
|
)
|
|
expect(
|
|
await FSE.pathExists(path.join(repository.path, 'bar'))
|
|
).toBeTrue()
|
|
const newStatus = await getStatusOrThrow(repository)
|
|
expect(sha).toHaveLength(7)
|
|
expect(newStatus.workingDirectory.files).toHaveLength(1)
|
|
})
|
|
|
|
it('deletes files chosen to be removed and commits', async () => {
|
|
const status = await getStatusOrThrow(repository)
|
|
const trackedFiles = status.workingDirectory.files.filter(
|
|
f => f.status.kind !== AppFileStatusKind.Untracked
|
|
)
|
|
const manualResolutions = new Map([
|
|
['bar', ManualConflictResolution.theirs],
|
|
])
|
|
const sha = await createMergeCommit(
|
|
repository,
|
|
trackedFiles,
|
|
manualResolutions
|
|
)
|
|
expect(
|
|
await FSE.pathExists(path.join(repository.path, 'bar'))
|
|
).toBeFalse()
|
|
const newStatus = await getStatusOrThrow(repository)
|
|
expect(sha).toHaveLength(7)
|
|
expect(newStatus.workingDirectory.files).toHaveLength(1)
|
|
})
|
|
|
|
it('checks out our content for file added in both branches', async () => {
|
|
const status = await getStatusOrThrow(repository)
|
|
const trackedFiles = status.workingDirectory.files.filter(
|
|
f => f.status.kind !== AppFileStatusKind.Untracked
|
|
)
|
|
const manualResolutions = new Map([
|
|
['baz', ManualConflictResolution.ours],
|
|
])
|
|
const sha = await createMergeCommit(
|
|
repository,
|
|
trackedFiles,
|
|
manualResolutions
|
|
)
|
|
expect(
|
|
await FSE.readFile(path.join(repository.path, 'baz'), 'utf8')
|
|
).toEqual('b2')
|
|
const newStatus = await getStatusOrThrow(repository)
|
|
expect(sha).toHaveLength(7)
|
|
expect(newStatus.workingDirectory.files).toHaveLength(1)
|
|
})
|
|
|
|
it('checks out their content for file added in both branches', async () => {
|
|
const status = await getStatusOrThrow(repository)
|
|
const trackedFiles = status.workingDirectory.files.filter(
|
|
f => f.status.kind !== AppFileStatusKind.Untracked
|
|
)
|
|
const manualResolutions = new Map([
|
|
['baz', ManualConflictResolution.theirs],
|
|
])
|
|
const sha = await createMergeCommit(
|
|
repository,
|
|
trackedFiles,
|
|
manualResolutions
|
|
)
|
|
expect(
|
|
await FSE.readFile(path.join(repository.path, 'baz'), 'utf8')
|
|
).toEqual('b1')
|
|
const newStatus = await getStatusOrThrow(repository)
|
|
expect(sha).toHaveLength(7)
|
|
expect(newStatus.workingDirectory.files).toHaveLength(1)
|
|
})
|
|
|
|
describe('binary file conflicts', () => {
|
|
let fileName: string
|
|
let fileContentsOurs: string, fileContentsTheirs: string
|
|
beforeEach(async () => {
|
|
const repoPath = await setupFixtureRepository(
|
|
'detect-conflict-in-binary-file'
|
|
)
|
|
repository = new Repository(repoPath, -1, null, false)
|
|
fileName = 'my-cool-image.png'
|
|
|
|
await GitProcess.exec(['checkout', 'master'], repoPath)
|
|
|
|
fileContentsTheirs = await FSE.readFile(
|
|
path.join(repoPath, fileName),
|
|
'utf8'
|
|
)
|
|
|
|
await GitProcess.exec(['checkout', 'make-a-change'], repoPath)
|
|
|
|
fileContentsOurs = await FSE.readFile(
|
|
path.join(repoPath, fileName),
|
|
'utf8'
|
|
)
|
|
})
|
|
|
|
it('chooses `their` version of a file and commits', async () => {
|
|
const repo = repository
|
|
|
|
await GitProcess.exec(['merge', 'master'], repo.path)
|
|
|
|
const status = await getStatusOrThrow(repo)
|
|
const files = status.workingDirectory.files
|
|
expect(files).toHaveLength(1)
|
|
|
|
const file = files[0]
|
|
expect(file.status.kind).toBe(AppFileStatusKind.Conflicted)
|
|
expect(
|
|
isConflictedFile(file.status) && isManualConflict(file.status)
|
|
).toBe(true)
|
|
|
|
const trackedFiles = files.filter(
|
|
f => f.status.kind !== AppFileStatusKind.Untracked
|
|
)
|
|
|
|
const manualResolutions = new Map([
|
|
[file.path, ManualConflictResolution.theirs],
|
|
])
|
|
await createMergeCommit(repository, trackedFiles, manualResolutions)
|
|
|
|
const fileContents = await FSE.readFile(
|
|
path.join(repository.path, file.path),
|
|
'utf8'
|
|
)
|
|
|
|
expect(fileContents).not.toStrictEqual(fileContentsOurs)
|
|
expect(fileContents).toStrictEqual(fileContentsTheirs)
|
|
})
|
|
|
|
it('chooses `our` version of a file and commits', async () => {
|
|
const repo = repository
|
|
|
|
await GitProcess.exec(['merge', 'master'], repo.path)
|
|
|
|
const status = await getStatusOrThrow(repo)
|
|
const files = status.workingDirectory.files
|
|
expect(files).toHaveLength(1)
|
|
|
|
const file = files[0]
|
|
expect(file.status.kind).toBe(AppFileStatusKind.Conflicted)
|
|
expect(
|
|
isConflictedFile(file.status) && isManualConflict(file.status)
|
|
).toBe(true)
|
|
|
|
const trackedFiles = files.filter(
|
|
f => f.status.kind !== AppFileStatusKind.Untracked
|
|
)
|
|
|
|
const manualResolutions = new Map([
|
|
[file.path, ManualConflictResolution.ours],
|
|
])
|
|
await createMergeCommit(repository, trackedFiles, manualResolutions)
|
|
|
|
const fileContents = await FSE.readFile(
|
|
path.join(repository.path, file.path),
|
|
'utf8'
|
|
)
|
|
|
|
expect(fileContents).toStrictEqual(fileContentsOurs)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('with no changes', () => {
|
|
let repository: Repository
|
|
beforeEach(async () => {
|
|
repository = new Repository(
|
|
await setupFixtureRepository('test-repo'),
|
|
-1,
|
|
null,
|
|
false
|
|
)
|
|
})
|
|
it('throws an error', async () => {
|
|
const status = await getStatusOrThrow(repository)
|
|
expect(
|
|
createMergeCommit(repository, status.workingDirectory.files)
|
|
).rejects.toThrow('There are no changes to commit.')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('index corner cases', () => {
|
|
it('can commit when staged new file is then deleted', async () => {
|
|
let status,
|
|
files = null
|
|
|
|
const repo = await setupEmptyRepository()
|
|
|
|
const firstPath = path.join(repo.path, 'first')
|
|
const secondPath = path.join(repo.path, 'second')
|
|
|
|
await FSE.writeFile(firstPath, 'line1\n')
|
|
await FSE.writeFile(secondPath, 'line2\n')
|
|
|
|
await GitProcess.exec(['add', '.'], repo.path)
|
|
|
|
await FSE.unlink(firstPath)
|
|
|
|
status = await getStatusOrThrow(repo)
|
|
files = status.workingDirectory.files
|
|
|
|
expect(files.length).toEqual(1)
|
|
expect(files[0].path).toContain('second')
|
|
expect(files[0].status.kind).toEqual(AppFileStatusKind.New)
|
|
|
|
const toCommit = status.workingDirectory.withIncludeAllFiles(true)
|
|
|
|
const sha = await createCommit(repo, 'commit everything', toCommit.files)
|
|
expect(sha).toEqual('(root-commit)')
|
|
|
|
status = await getStatusOrThrow(repo)
|
|
files = status.workingDirectory.files
|
|
expect(files).toHaveLength(0)
|
|
|
|
const commit = await getCommit(repo, 'HEAD')
|
|
expect(commit).not.toBeNull()
|
|
expect(commit!.summary).toEqual('commit everything')
|
|
})
|
|
|
|
it('can commit when a delete is staged and the untracked file exists', async () => {
|
|
let status,
|
|
files = null
|
|
|
|
const repo = await setupEmptyRepository()
|
|
|
|
const firstPath = path.join(repo.path, 'first')
|
|
await FSE.writeFile(firstPath, 'line1\n')
|
|
|
|
await GitProcess.exec(['add', 'first'], repo.path)
|
|
await GitProcess.exec(['commit', '-am', 'commit first file'], repo.path)
|
|
await GitProcess.exec(['rm', '--cached', 'first'], repo.path)
|
|
|
|
// if the text is now different, everything is fine
|
|
await FSE.writeFile(firstPath, 'line2\n')
|
|
|
|
status = await getStatusOrThrow(repo)
|
|
files = status.workingDirectory.files
|
|
|
|
expect(files.length).toEqual(1)
|
|
expect(files[0].path).toContain('first')
|
|
expect(files[0].status.kind).toEqual(AppFileStatusKind.Untracked)
|
|
|
|
const toCommit = status!.workingDirectory.withIncludeAllFiles(true)
|
|
|
|
const sha = await createCommit(repo, 'commit again!', toCommit.files)
|
|
expect(sha).toHaveLength(7)
|
|
|
|
status = await getStatusOrThrow(repo)
|
|
files = status.workingDirectory.files
|
|
expect(files).toHaveLength(0)
|
|
|
|
const commit = await getCommit(repo, 'HEAD')
|
|
expect(commit).not.toBeNull()
|
|
expect(commit!.summary).toEqual('commit again!')
|
|
expect(commit!.shortSha).toEqual(sha)
|
|
})
|
|
|
|
it('file is deleted in index', async () => {
|
|
const repo = await setupEmptyRepository()
|
|
await FSE.writeFile(path.join(repo.path, 'secret'), 'contents\n')
|
|
await FSE.writeFile(path.join(repo.path, '.gitignore'), '')
|
|
|
|
// Setup repo to reproduce bug
|
|
await GitProcess.exec(['add', '.'], repo.path)
|
|
await GitProcess.exec(['commit', '-m', 'Initial commit'], repo.path)
|
|
|
|
// Make changes that should remain secret
|
|
await FSE.writeFile(path.join(repo.path, 'secret'), 'Somethign secret\n')
|
|
|
|
// Ignore it
|
|
await FSE.writeFile(path.join(repo.path, '.gitignore'), 'secret')
|
|
|
|
// Remove from index to mark as deleted
|
|
await GitProcess.exec(['rm', '--cached', 'secret'], repo.path)
|
|
|
|
// Make sure that file is marked as deleted
|
|
const beforeCommit = await getStatusOrThrow(repo)
|
|
const files = beforeCommit.workingDirectory.files
|
|
expect(files.length).toBe(2)
|
|
expect(files[1].status.kind).toBe(AppFileStatusKind.Deleted)
|
|
|
|
// Commit changes
|
|
await createCommit(repo!, 'FAIL commit', files)
|
|
const afterCommit = await getStatusOrThrow(repo)
|
|
expect(beforeCommit.currentTip).not.toBe(afterCommit.currentTip)
|
|
|
|
// Verify the file was delete in repo
|
|
const changesetData = await getChangedFiles(repo, afterCommit.currentTip!)
|
|
expect(changesetData.files.length).toBe(2)
|
|
expect(changesetData.files[0].status.kind).toBe(
|
|
AppFileStatusKind.Modified
|
|
)
|
|
expect(changesetData.files[1].status.kind).toBe(AppFileStatusKind.Deleted)
|
|
})
|
|
})
|
|
})
|