Skip to content

Commit ea58162

Browse files
Mario Parejaazz
authored andcommitted
feat: safer handling of partially staged files (with fix) (#33)
* feat: safer handling of partially staged files (#29) + Partially staged files are not re-staged + Non-zero exit code upon reformatting partially staged file + Update README * Fixes #30 handling of partially staged files The git command checking for unstaged changes should not have been including a revision.
1 parent 3287e8f commit ea58162

File tree

6 files changed

+76
-7
lines changed

6 files changed

+76
-7
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ In `package.json`'s `"scripts"` section, add:
7474

7575
Pre-commit mode. Under this flag only staged files will be formatted, and they will be re-staged after formatting.
7676

77+
Partially staged files will not be re-staged after formatting and pretty-quick will exit with a non-zero exit code. The intent is to abort the git commit and allow the user to amend their selective staging to include formatting fixes.
78+
7779
### `--branch`
7880

7981
When not in `staged` pre-commit mode, use this flag to compare changes with the specified branch. Defaults to `master` (git) / `default` (hg) branch.

bin/pretty-quick.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const prettyQuick = require('..').default;
99

1010
const args = mri(process.argv.slice(2));
1111

12+
let success = true;
1213
prettyQuick(
1314
process.cwd(),
1415
Object.assign({}, args, {
@@ -28,10 +29,23 @@ prettyQuick(
2829
);
2930
},
3031

32+
onPartiallyStagedFile: file => {
33+
console.log(`✗ Found ${chalk.bold('partially')} staged file ${file}.`);
34+
success = false;
35+
},
36+
3137
onWriteFile: file => {
3238
console.log(`✍️ Fixing up ${chalk.bold(file)}.`);
3339
},
3440
})
3541
);
3642

37-
console.log('✅ Everything is awesome!');
43+
if (success) {
44+
console.log('✅ Everything is awesome!');
45+
} else {
46+
console.log(
47+
'✗ Partially staged files were fixed up.' +
48+
` ${chalk.bold('Please update stage before committing')}.`
49+
);
50+
process.exit(1); // ensure git hooks abort
51+
}

src/__tests__/scm-git.test.js

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ afterEach(() => {
1111
jest.clearAllMocks();
1212
});
1313

14-
const mockGitFs = () => {
14+
const mockGitFs = (additionalUnstaged = '') => {
1515
mock({
1616
'/.git': {},
17+
'/raz.js': 'raz()',
1718
'/foo.js': 'foo()',
1819
'/bar.md': '# foo',
1920
});
@@ -26,8 +27,8 @@ const mockGitFs = () => {
2627
return { stdout: '' };
2728
case 'diff':
2829
return args[2] === '--cached'
29-
? { stdout: './foo.js\n' }
30-
: { stdout: './foo.js\n' + './bar.md\n' };
30+
? { stdout: './raz.js\n' }
31+
: { stdout: './foo.js\n' + './bar.md\n' + additionalUnstaged };
3132
case 'add':
3233
return { stdout: '' };
3334
default:
@@ -80,6 +81,20 @@ describe('with git', () => {
8081
]);
8182
});
8283

84+
test('with --staged calls diff without revision', () => {
85+
mock({
86+
'/.git': {},
87+
});
88+
89+
prettyQuick('root', { since: 'banana', staged: true });
90+
91+
expect(execa.sync).toHaveBeenCalledWith(
92+
'git',
93+
['diff', '--name-only', '--diff-filter=ACMRTUB'],
94+
{ cwd: '/' }
95+
);
96+
});
97+
8398
test('calls `git diff --name-only` with revision', () => {
8499
mock({
85100
'/.git': {},
@@ -151,19 +166,33 @@ describe('with git', () => {
151166
expect(fs.readFileSync('/bar.md', 'utf8')).toEqual('formatted:# foo');
152167
});
153168

154-
test('with --staged stages staged files', () => {
169+
test('with --staged stages fully-staged files', () => {
155170
mockGitFs();
156171

157172
prettyQuick('root', { since: 'banana', staged: true });
158173

159-
expect(execa.sync).toHaveBeenCalledWith('git', ['add', './foo.js'], {
174+
expect(execa.sync).toHaveBeenCalledWith('git', ['add', './raz.js'], {
175+
cwd: '/',
176+
});
177+
expect(execa.sync).not.toHaveBeenCalledWith('git', ['add', './foo.md'], {
160178
cwd: '/',
161179
});
162180
expect(execa.sync).not.toHaveBeenCalledWith('git', ['add', './bar.md'], {
163181
cwd: '/',
164182
});
165183
});
166184

185+
test('with --staged does not stage previously partially staged files AND aborts commit', () => {
186+
const additionalUnstaged = './raz.js\n'; // raz.js is partly staged and partly not staged
187+
mockGitFs(additionalUnstaged);
188+
189+
prettyQuick('root', { since: 'banana', staged: true });
190+
191+
expect(execa.sync).not.toHaveBeenCalledWith('git', ['add', './raz.js'], {
192+
cwd: '/',
193+
});
194+
});
195+
167196
test('without --staged does NOT stage changed files', () => {
168197
mockGitFs();
169198

src/index.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default (
1212
branch,
1313
onFoundSinceRevision,
1414
onFoundChangedFiles,
15+
onPartiallyStagedFile,
1516
onWriteFile,
1617
} = {}
1718
) => {
@@ -30,13 +31,28 @@ export default (
3031
.filter(isSupportedExtension)
3132
.filter(createIgnorer(directory));
3233

34+
const unstagedFiles = staged
35+
? scm
36+
.getUnstagedChangedFiles(directory, revision)
37+
.filter(isSupportedExtension)
38+
.filter(createIgnorer(directory))
39+
: [];
40+
41+
const wasFullyStaged = f => unstagedFiles.indexOf(f) < 0;
42+
3343
onFoundChangedFiles && onFoundChangedFiles(changedFiles);
3444

3545
formatFiles(directory, changedFiles, {
3646
config,
3747
onWriteFile: file => {
3848
onWriteFile && onWriteFile(file);
39-
staged && scm.stageFile(directory, file);
49+
if (staged) {
50+
if (wasFullyStaged(file)) {
51+
scm.stageFile(directory, file);
52+
} else {
53+
onPartiallyStagedFile && onPartiallyStagedFile(file);
54+
}
55+
}
4056
},
4157
});
4258
};

src/scms/git.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ export const getChangedFiles = (directory, revision, staged) => {
6161
].filter(Boolean);
6262
};
6363

64+
export const getUnstagedChangedFiles = directory => {
65+
return getChangedFiles(directory, null, false);
66+
};
67+
6468
export const stageFile = (directory, file) => {
6569
runGit(directory, ['add', file]);
6670
};

src/scms/hg.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ export const getChangedFiles = (directory, revision) => {
3535
].filter(Boolean);
3636
};
3737

38+
export const getUnstagedChangedFiles = () => {
39+
return [];
40+
};
41+
3842
export const stageFile = (directory, file) => {
3943
runHg(directory, ['add', file]);
4044
};

0 commit comments

Comments
 (0)