Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit cab6f0d

Browse files
authored
Merge pull request atom#1275 from atom/mkt-ku-filemodes-galore
Handle filemode changes
2 parents 941916a + e15a9ee commit cab6f0d

29 files changed

+1657
-502
lines changed

lib/controllers/file-patch-controller.js

Lines changed: 156 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {autobind} from 'core-decorators';
1010
import Switchboard from '../switchboard';
1111
import FilePatchView from '../views/file-patch-view';
1212
import ModelObserver from '../models/model-observer';
13-
import FilePatch from '../models/file-patch';
1413

1514
export default class FilePatchController extends React.Component {
1615
static propTypes = {
@@ -134,10 +133,13 @@ export default class FilePatchController extends React.Component {
134133
this.resolveFilePatchLoadedPromise();
135134
if (!this.destroyed) { this.setState({filePatch, isPartiallyStaged}); }
136135
} else {
137-
// TODO: Prevent flicker after staging/unstaging
138136
const oldFilePatch = this.state.filePatch;
139137
if (oldFilePatch) {
140-
filePatch = new FilePatch(oldFilePatch.getOldPath(), oldFilePatch.getNewPath(), oldFilePatch.getStatus(), []);
138+
filePatch = oldFilePatch.clone({
139+
oldFile: oldFilePatch.oldFile.clone({mode: null, symlink: null}),
140+
newFile: oldFilePatch.newFile.clone({mode: null, symlink: null}),
141+
patch: oldFilePatch.getPatch().clone({hunks: []}),
142+
});
141143
if (!this.destroyed) { this.setState({filePatch, isPartiallyStaged}); }
142144
}
143145
}
@@ -164,7 +166,18 @@ export default class FilePatchController extends React.Component {
164166
}
165167

166168
render() {
167-
const hunks = this.state.filePatch ? this.state.filePatch.getHunks() : [];
169+
const fp = this.state.filePatch;
170+
const hunks = fp ? fp.getHunks() : [];
171+
const executableModeChange = fp && fp.didChangeExecutableMode() ?
172+
{oldMode: fp.getOldMode(), newMode: fp.getNewMode()} :
173+
null;
174+
const symlinkChange = fp && fp.hasSymlink() ?
175+
{
176+
oldSymlink: fp.getOldSymlink(),
177+
newSymlink: fp.getNewSymlink(),
178+
typechange: fp.hasTypechange(),
179+
filePatchStatus: fp.getStatus(),
180+
} : null;
168181
const repository = this.repositoryObserver.getActiveModel();
169182
if (repository.isUndetermined() || repository.isLoading()) {
170183
return (
@@ -193,12 +206,17 @@ export default class FilePatchController extends React.Component {
193206
lineCount={this.lineCount}
194207
handleShowDiffClick={this.handleShowDiffClick}
195208
hunks={hunks}
209+
executableModeChange={executableModeChange}
210+
symlinkChange={symlinkChange}
196211
filePath={this.props.filePath}
197212
workingDirectoryPath={this.getWorkingDirectory()}
198213
stagingStatus={this.state.stagingStatus}
199214
isPartiallyStaged={this.state.isPartiallyStaged}
200215
attemptLineStageOperation={this.attemptLineStageOperation}
201216
attemptHunkStageOperation={this.attemptHunkStageOperation}
217+
attemptFileStageOperation={this.attemptFileStageOperation}
218+
attemptModeStageOperation={this.attemptModeStageOperation}
219+
attemptSymlinkStageOperation={this.attemptSymlinkStageOperation}
202220
didSurfaceFile={this.didSurfaceFile}
203221
didDiveIntoCorrespondingFilePatch={this.diveIntoCorrespondingFilePatch}
204222
switchboard={this.props.switchboard}
@@ -269,6 +287,98 @@ export default class FilePatchController extends React.Component {
269287
}
270288
}
271289

290+
async stageFile() {
291+
this.props.switchboard.didBeginStageOperation({stage: true, file: true});
292+
293+
await this.repositoryObserver.getActiveModel().stageFiles([this.props.filePath]);
294+
this.props.switchboard.didFinishStageOperation({stage: true, file: true});
295+
}
296+
297+
async unstageFile() {
298+
this.props.switchboard.didBeginStageOperation({unstage: true, file: true});
299+
300+
await this.repositoryObserver.getActiveModel().unstageFiles([this.props.filePath]);
301+
302+
this.props.switchboard.didFinishStageOperation({unstage: true, file: true});
303+
}
304+
305+
stageOrUnstageFile() {
306+
const stagingStatus = this.state.stagingStatus;
307+
if (stagingStatus === 'unstaged') {
308+
return this.stageFile();
309+
} else if (stagingStatus === 'staged') {
310+
return this.unstageFile();
311+
} else {
312+
throw new Error(`Unknown stagingStatus: ${stagingStatus}`);
313+
}
314+
}
315+
316+
async stageModeChange(mode) {
317+
this.props.switchboard.didBeginStageOperation({stage: true, mode: true});
318+
319+
await this.repositoryObserver.getActiveModel().stageFileModeChange(
320+
this.props.filePath, mode,
321+
);
322+
this.props.switchboard.didFinishStageOperation({stage: true, mode: true});
323+
}
324+
325+
async unstageModeChange(mode) {
326+
this.props.switchboard.didBeginStageOperation({unstage: true, mode: true});
327+
328+
await this.repositoryObserver.getActiveModel().stageFileModeChange(
329+
this.props.filePath, mode,
330+
);
331+
this.props.switchboard.didFinishStageOperation({unstage: true, mode: true});
332+
}
333+
334+
stageOrUnstageModeChange() {
335+
const stagingStatus = this.state.stagingStatus;
336+
const oldMode = this.state.filePatch.getOldMode();
337+
const newMode = this.state.filePatch.getNewMode();
338+
if (stagingStatus === 'unstaged') {
339+
return this.stageModeChange(newMode);
340+
} else if (stagingStatus === 'staged') {
341+
return this.unstageModeChange(oldMode);
342+
} else {
343+
throw new Error(`Unknown stagingStatus: ${stagingStatus}`);
344+
}
345+
}
346+
347+
async stageSymlinkChange() {
348+
this.props.switchboard.didBeginStageOperation({stage: true, symlink: true});
349+
350+
const filePatch = this.state.filePatch;
351+
if (filePatch.hasTypechange() && filePatch.getStatus() === 'added') {
352+
await this.repositoryObserver.getActiveModel().stageFileSymlinkChange(this.props.filePath);
353+
} else {
354+
await this.repositoryObserver.getActiveModel().stageFiles([this.props.filePath]);
355+
}
356+
this.props.switchboard.didFinishStageOperation({stage: true, symlink: true});
357+
}
358+
359+
async unstageSymlinkChange() {
360+
this.props.switchboard.didBeginStageOperation({unstage: true, symlink: true});
361+
362+
const filePatch = this.state.filePatch;
363+
if (filePatch.hasTypechange() && filePatch.getStatus() === 'deleted') {
364+
await this.repositoryObserver.getActiveModel().stageFileSymlinkChange(this.props.filePath);
365+
} else {
366+
await this.repositoryObserver.getActiveModel().unstageFiles([this.props.filePath]);
367+
}
368+
this.props.switchboard.didFinishStageOperation({unstage: true, symlink: true});
369+
}
370+
371+
stageOrUnstageSymlinkChange() {
372+
const stagingStatus = this.state.stagingStatus;
373+
if (stagingStatus === 'unstaged') {
374+
return this.stageSymlinkChange();
375+
} else if (stagingStatus === 'staged') {
376+
return this.unstageSymlinkChange();
377+
} else {
378+
throw new Error(`Unknown stagingStatus: ${stagingStatus}`);
379+
}
380+
}
381+
272382
@autobind
273383
attemptHunkStageOperation(hunk) {
274384
if (this.stagingOperationInProgress) {
@@ -283,6 +393,48 @@ export default class FilePatchController extends React.Component {
283393
this.stageOrUnstageHunk(hunk);
284394
}
285395

396+
@autobind
397+
attemptFileStageOperation() {
398+
if (this.stagingOperationInProgress) {
399+
return;
400+
}
401+
402+
this.stagingOperationInProgress = true;
403+
this.props.switchboard.getChangePatchPromise().then(() => {
404+
this.stagingOperationInProgress = false;
405+
});
406+
407+
this.stageOrUnstageFile();
408+
}
409+
410+
@autobind
411+
attemptModeStageOperation() {
412+
if (this.stagingOperationInProgress) {
413+
return;
414+
}
415+
416+
this.stagingOperationInProgress = true;
417+
this.props.switchboard.getChangePatchPromise().then(() => {
418+
this.stagingOperationInProgress = false;
419+
});
420+
421+
this.stageOrUnstageModeChange();
422+
}
423+
424+
@autobind
425+
attemptSymlinkStageOperation() {
426+
if (this.stagingOperationInProgress) {
427+
return;
428+
}
429+
430+
this.stagingOperationInProgress = true;
431+
this.props.switchboard.getChangePatchPromise().then(() => {
432+
this.stagingOperationInProgress = false;
433+
});
434+
435+
this.stageOrUnstageSymlinkChange();
436+
}
437+
286438
async stageLines(lines) {
287439
this.props.switchboard.didBeginStageOperation({stage: true, line: true});
288440

lib/git-shell-out-strategy.js

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import GitTempDir from './git-temp-dir';
1313
import AsyncQueue from './async-queue';
1414
import {
1515
getDugitePath, getAtomHelperPath,
16-
readFile, fileExists, fsStat, writeFile, isFileExecutable, isBinary,
16+
readFile, fileExists, fsStat, writeFile, isFileExecutable, isFileSymlink, isBinary, getRealPath,
1717
normalizeGitHelperPath, toNativePathSep, toGitPathSep,
1818
} from './helpers';
1919
import GitTimingsView from './views/git-timings-view';
@@ -188,7 +188,7 @@ export default class GitShellOutStrategy {
188188
env.GIT_TRACE_CURL = 'true';
189189
}
190190

191-
const opts = {env};
191+
let opts = {env};
192192

193193
if (stdin) {
194194
opts.stdin = stdin;
@@ -199,6 +199,11 @@ export default class GitShellOutStrategy {
199199
console.time(`git:${formattedArgs}`);
200200
}
201201
return new Promise(async (resolve, reject) => {
202+
if (options.beforeRun) {
203+
const newArgsOpts = await options.beforeRun({args, opts});
204+
args = newArgsOpts.args; // eslint-disable-line no-param-reassign
205+
opts = newArgsOpts.opts;
206+
}
202207
const {promise, cancel} = this.executeGitCommand(args, opts, timingMarker);
203208
let expectCancel = false;
204209
if (gitPromptServer) {
@@ -373,6 +378,25 @@ export default class GitShellOutStrategy {
373378
return this.exec(args, {writeOperation: true});
374379
}
375380

381+
stageFileModeChange(filename, newMode) {
382+
const indexReadPromise = this.exec(['ls-files', '-s', '--', filename]);
383+
return this.exec(['update-index', '--cacheinfo', `${newMode},<OID_TBD>,${filename}`], {
384+
writeOperation: true,
385+
beforeRun: async function determineArgs({args, opts}) {
386+
const index = await indexReadPromise;
387+
const oid = index.substr(7, 40);
388+
return {
389+
opts,
390+
args: ['update-index', '--cacheinfo', `${newMode},${oid},${filename}`],
391+
};
392+
},
393+
});
394+
}
395+
396+
stageFileSymlinkChange(filename) {
397+
return this.exec(['rm', '--cached', filename], {writeOperation: true});
398+
}
399+
376400
applyPatch(patch, {index} = {}) {
377401
const args = ['apply', '-'];
378402
if (index) { args.splice(1, 0, '--cached'); }
@@ -460,7 +484,7 @@ export default class GitShellOutStrategy {
460484
return output.trim().split(LINE_ENDING_REGEX).map(toNativePathSep);
461485
}
462486

463-
async getDiffForFilePath(filePath, {staged, baseCommit} = {}) {
487+
async getDiffsForFilePath(filePath, {staged, baseCommit} = {}) {
464488
let args = ['diff', '--no-prefix', '--no-ext-diff', '--no-renames', '--diff-filter=u'];
465489
if (staged) { args.push('--staged'); }
466490
if (baseCommit) { args.push(baseCommit); }
@@ -487,12 +511,26 @@ export default class GitShellOutStrategy {
487511
// add untracked file
488512
const absPath = path.join(this.workingDir, filePath);
489513
const executable = await isFileExecutable(absPath);
514+
const symlink = await isFileSymlink(absPath);
490515
const contents = await readFile(absPath);
491516
const binary = isBinary(contents);
492-
rawDiffs.push(buildAddedFilePatch(filePath, binary ? null : contents, executable));
517+
let mode;
518+
let realpath;
519+
if (executable) {
520+
mode = '100755';
521+
} else if (symlink) {
522+
mode = '120000';
523+
realpath = await getRealPath(absPath);
524+
} else {
525+
mode = '100644';
526+
}
527+
528+
rawDiffs.push(buildAddedFilePatch(filePath, binary ? null : contents, mode, realpath));
529+
}
530+
if (rawDiffs.length > 2) {
531+
throw new Error(`Expected between 0 and 2 diffs for ${filePath} but got ${rawDiffs.length}`);
493532
}
494-
if (rawDiffs.length > 1) { throw new Error(`Expected 0 or 1 diffs for ${filePath} but got ${rawDiffs.length}`); }
495-
return rawDiffs[0];
533+
return rawDiffs;
496534
}
497535

498536
/**
@@ -734,7 +772,14 @@ export default class GitShellOutStrategy {
734772
return output.slice(0, 6);
735773
} else {
736774
const executable = await isFileExecutable(path.join(this.workingDir, filePath));
737-
return executable ? '100755' : '100644';
775+
const symlink = await isFileSymlink(path.join(this.workingDir, filePath));
776+
if (executable) {
777+
return '100755';
778+
} else if (symlink) {
779+
return '120000';
780+
} else {
781+
return '100644';
782+
}
738783
}
739784
}
740785

@@ -743,11 +788,16 @@ export default class GitShellOutStrategy {
743788
}
744789
}
745790

746-
function buildAddedFilePatch(filePath, contents, executable) {
791+
function buildAddedFilePatch(filePath, contents, mode, realpath) {
747792
const hunks = [];
748793
if (contents) {
749794
const noNewLine = contents[contents.length - 1] !== '\n';
750-
const lines = contents.trim().split(LINE_ENDING_REGEX).map(line => `+${line}`);
795+
let lines;
796+
if (mode === '120000') {
797+
lines = [`+${toGitPathSep(realpath)}`, '\\ No newline at end of file'];
798+
} else {
799+
lines = contents.trim().split(LINE_ENDING_REGEX).map(line => `+${line}`);
800+
}
751801
if (noNewLine) { lines.push('\\ No newline at end of file'); }
752802
hunks.push({
753803
lines,
@@ -762,7 +812,7 @@ function buildAddedFilePatch(filePath, contents, executable) {
762812
oldPath: null,
763813
newPath: toNativePathSep(filePath),
764814
oldMode: null,
765-
newMode: executable ? '100755' : '100644',
815+
newMode: mode,
766816
status: 'added',
767817
hunks,
768818
};

lib/github-package.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,22 @@ export default class GithubPackage {
9696
yardstick.begin('stageLine');
9797
} else if (payload.stage && payload.hunk) {
9898
yardstick.begin('stageHunk');
99+
} else if (payload.stage && payload.file) {
100+
yardstick.begin('stageFile');
101+
} else if (payload.stage && payload.mode) {
102+
yardstick.begin('stageMode');
103+
} else if (payload.stage && payload.symlink) {
104+
yardstick.begin('stageSymlink');
99105
} else if (payload.unstage && payload.line) {
100106
yardstick.begin('unstageLine');
101107
} else if (payload.unstage && payload.hunk) {
102108
yardstick.begin('unstageHunk');
109+
} else if (payload.unstage && payload.file) {
110+
yardstick.begin('unstageFile');
111+
} else if (payload.unstage && payload.mode) {
112+
yardstick.begin('unstageMode');
113+
} else if (payload.unstage && payload.symlink) {
114+
yardstick.begin('unstageSymlink');
103115
}
104116
}),
105117
this.switchboard.onDidUpdateRepository(() => {

0 commit comments

Comments
 (0)