Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use machine output for listing partially staged files #876

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 17 additions & 11 deletions lib/gitWorkflow.js
Expand Up @@ -19,19 +19,20 @@ const MERGE_HEAD = 'MERGE_HEAD'
const MERGE_MODE = 'MERGE_MODE'
const MERGE_MSG = 'MERGE_MSG'

// In git status, renames are presented as `from` -> `to`.
// In git status machine output, renames are presented as `to`NUL`from`
// When diffing, both need to be taken into account, but in some cases on the `to`.
const RENAME = / -> /
// eslint-disable-next-line no-control-regex
const RENAME = /\x00/

/**
* From list of files, split renames and flatten into two files `from` -> `to`.
* From list of files, split renames and flatten into two files `to`NUL`from`.
* @param {string[]} files
* @param {Boolean} [includeRenameFrom=true] Whether or not to include the `from` renamed file, which is no longer on disk
*/
const processRenames = (files, includeRenameFrom = true) =>
files.reduce((flattened, file) => {
if (RENAME.test(file)) {
const [from, to] = file.split(RENAME)
const [to, from] = file.split(RENAME)
if (includeRenameFrom) flattened.push(from)
flattened.push(to)
} else {
Expand Down Expand Up @@ -158,15 +159,20 @@ class GitWorkflow {
*/
async getPartiallyStagedFiles() {
debug('Getting partially staged files...')
const status = await this.execGit(['status', '--porcelain'])
const status = await this.execGit(['status', '-z'])
/**
* See https://git-scm.com/docs/git-status#_short_format
* Entries returned in machine format are separated by a NUL character.
* The first letter of each entry represents current index status,
* and second the working tree. Index and working tree status codes are
* separated from the file name by a space. If an entry includes a
* renamed file, the file names are separated by a NUL character
* (e.g. `to`\0`from`)
*/
const partiallyStaged = status
.split('\n')
// eslint-disable-next-line no-control-regex
.split(/\x00(?=[ AMDRCU?!]{2} |$)/)
.filter((line) => {
/**
* See https://git-scm.com/docs/git-status#_short_format
* The first letter of the line represents current index status,
* and second the working tree
*/
const [index, workingTree] = line
return index !== ' ' && workingTree !== ' ' && index !== '?' && workingTree !== '?'
})
Expand Down
51 changes: 51 additions & 0 deletions test/gitWorkflow.spec.js
Expand Up @@ -19,6 +19,8 @@ let tmpDir, cwd
const appendFile = async (filename, content, dir = cwd) =>
fs.appendFile(path.resolve(dir, filename), content)

const readFile = async (filename, dir = cwd) => fs.readFile(path.resolve(dir, filename))

/** Wrap execGit to always pass `gitOps` */
const execGit = async (args) => execGitBase(args, { cwd })

Expand Down Expand Up @@ -102,6 +104,41 @@ describe('gitWorkflow', () => {
})
})

describe('getPartiallyStagedFiles', () => {
it('should return unquoted files', async () => {
const gitWorkflow = new GitWorkflow({
gitDir: cwd,
gitConfigDir: path.resolve(cwd, './.git'),
})
await appendFile('file with spaces.txt', 'staged content')
await appendFile('file_without_spaces.txt', 'staged content')
await execGit(['add', 'file with spaces.txt'])
await execGit(['add', 'file_without_spaces.txt'])
await appendFile('file with spaces.txt', 'not staged content')
await appendFile('file_without_spaces.txt', 'not staged content')

expect(await gitWorkflow.getPartiallyStagedFiles()).toStrictEqual([
'file with spaces.txt',
'file_without_spaces.txt',
])
})
it('should include to and from for renamed files', async () => {
const gitWorkflow = new GitWorkflow({
gitDir: cwd,
gitConfigDir: path.resolve(cwd, './.git'),
})
await appendFile('original.txt', 'test content')
await execGit(['add', 'original.txt'])
await execGit(['commit', '-m "Add original.txt"'])
await appendFile('original.txt', 'additional content')
await execGit(['mv', 'original.txt', 'renamed.txt'])

expect(await gitWorkflow.getPartiallyStagedFiles()).toStrictEqual([
'renamed.txt\u0000original.txt',
])
})
})

describe('hideUnstagedChanges', () => {
it('should handle errors', async () => {
const gitWorkflow = new GitWorkflow({
Expand All @@ -127,6 +164,20 @@ describe('gitWorkflow', () => {
}
`)
})
it('should checkout renamed file when hiding changes', async () => {
const gitWorkflow = new GitWorkflow({
gitDir: cwd,
gitConfigDir: path.resolve(cwd, './.git'),
})
const origContent = await readFile('README.md')
await execGit(['mv', 'README.md', 'TEST.md'])
await appendFile('TEST.md', 'added content')

gitWorkflow.partiallyStagedFiles = await gitWorkflow.getPartiallyStagedFiles()
const ctx = getInitialState()
await gitWorkflow.hideUnstagedChanges(ctx)
expect(await readFile('TEST.md')).toStrictEqual(origContent)
})
})

describe('restoreMergeStatus', () => {
Expand Down