From d4dee1926c63d8899f5dfd0e82d2292cc8e9f788 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 24 Apr 2017 11:04:45 -0400 Subject: [PATCH 01/55] Move Cache after the actual Present state --- lib/models/repository-states/present.js | 54 ++++++++++++------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 603a902af5..a06ebd1571 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -11,33 +11,6 @@ import Branch from '../branch'; import Remote from '../remote'; import Commit from '../commit'; -class Cache { - constructor() { - this.storage = new Map(); - } - - getOrSet(key, operation) { - const existing = this.storage.get(key); - if (existing !== undefined) { - return existing; - } - - const created = operation(); - this.storage.set(key, created); - return created; - } - - invalidate(...keys) { - for (let i = 0; i < keys.length; i++) { - this.storage.delete(keys[i]); - } - } - - clear() { - this.storage.clear(); - } -} - /** * Decorator for an async method that invalidates the cache after execution (regardless of success or failure.) */ @@ -503,3 +476,30 @@ function buildFilePatchesFromRawDiffs(rawDiffs) { return new FilePatch(patch.oldPath, patch.newPath, patch.status, hunks); }); } + +class Cache { + constructor() { + this.storage = new Map(); + } + + getOrSet(key, operation) { + const existing = this.storage.get(key); + if (existing !== undefined) { + return existing; + } + + const created = operation(); + this.storage.set(key, created); + return created; + } + + invalidate(...keys) { + for (let i = 0; i < keys.length; i++) { + this.storage.delete(keys[i]); + } + } + + clear() { + this.storage.clear(); + } +} From 99988345448269cf07b9c886e488804e17b61aec Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 24 Apr 2017 11:21:00 -0400 Subject: [PATCH 02/55] Use Keys constants and functions as cache keys --- lib/models/repository-states/present.js | 78 +++++++++++++++++-------- 1 file changed, 55 insertions(+), 23 deletions(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index a06ebd1571..b17adf7de7 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -257,13 +257,13 @@ export default class Present extends State { // Index queries getStatusesForChangedFiles() { - return this.cache.getOrSet('changed-files', () => { + return this.cache.getOrSet(Keys.changedFiles, () => { return this.git().getStatusesForChangedFiles(); }); } getStagedChangesSinceParentCommit() { - return this.cache.getOrSet('staged-changes-since-parent-commit', async () => { + return this.cache.getOrSet(Keys.stagedChangesSinceParentCommit, async () => { try { const stagedFiles = await this.git().diffFileStatus({staged: true, target: 'HEAD~'}); return Object.keys(stagedFiles).map(filePath => ({filePath, status: stagedFiles[filePath]})); @@ -278,22 +278,13 @@ export default class Present extends State { } isPartiallyStaged(fileName) { - return this.cache.getOrSet(`is-partially-staged:${fileName}`, () => { + return this.cache.getOrSet(Keys.isPartiallyStaged(fileName), () => { return this.git().isPartiallyStaged(fileName); }); } getFilePatchForPath(filePath, {staged, amending} = {staged: false, amending: false}) { - let desc = ''; - if (staged && amending) { - desc = 'p'; - } else if (staged) { - desc = 's'; - } else { - desc = 'u'; - } - - return this.cache.getOrSet(`file-patch:${desc}:${filePath}`, async () => { + return this.cache.getOrSet(Keys.filePatch(filePath, {staged, amending}), async () => { const options = {staged, amending}; if (amending) { options.baseCommit = 'HEAD~'; @@ -310,7 +301,7 @@ export default class Present extends State { } readFileFromIndex(filePath) { - return this.cache.getOrSet(`index:${filePath}`, () => { + return this.cache.getOrSet(Keys.index(filePath), () => { return this.git().readFileFromIndex(filePath); }); } @@ -318,7 +309,7 @@ export default class Present extends State { // Commit access getLastCommit() { - return this.cache.getOrSet('last-commit', async () => { + return this.cache.getOrSet(Keys.lastCommit, async () => { const {sha, message, unbornRef} = await this.git().getHeadCommit(); return unbornRef ? Commit.createUnborn() : new Commit(sha, message); }); @@ -327,14 +318,14 @@ export default class Present extends State { // Branches getBranches() { - return this.cache.getOrSet('branches', async () => { + return this.cache.getOrSet(Keys.branches, async () => { const branchNames = await this.git().getBranches(); return branchNames.map(branchName => new Branch(branchName)); }); } getCurrentBranch() { - return this.cache.getOrSet('current-branch', async () => { + return this.cache.getOrSet(Keys.currentBranch, async () => { const {name, isDetached} = await this.git().getCurrentBranch(); return isDetached ? Branch.createDetached(name) : new Branch(name); }); @@ -353,27 +344,26 @@ export default class Present extends State { // Remotes getRemotes() { - return this.cache.getOrSet('remotes', async () => { + return this.cache.getOrSet(Keys.remotes, async () => { const remotesInfo = await this.git().getRemotes(); return remotesInfo.map(({name, url}) => new Remote(name, url)); }); } getAheadCount(branchName) { - return this.cache.getOrSet(`ahead-count:${branchName}`, () => { + return this.cache.getOrSet(Keys.aheadCount(branchName), () => { return this.git().getAheadCount(branchName); }); } getBehindCount(branchName) { - return this.cache.getOrSet(`behind-count:${branchName}`, () => { + return this.cache.getOrSet(Keys.behindCount(branchName), () => { return this.git().getBehindCount(branchName); }); } getConfig(option, {local} = {local: false}) { - const desc = local ? 'l' : ''; - return this.cache.getOrSet(`config:${desc}:${option}`, () => { + return this.cache.getOrSet(Keys.config(option, {local}), () => { return this.git().getConfig(option, {local}); }); } @@ -381,7 +371,7 @@ export default class Present extends State { // Direct blob access getBlobContents(sha) { - return this.cache.getOrSet(`blob:${sha}`, () => { + return this.cache.getOrSet(Keys.blob(sha), () => { return this.git().getBlobContents(sha); }); } @@ -503,3 +493,45 @@ class Cache { this.storage.clear(); } } + +const Keys = { + changedFiles: 'changed-files', + + stagedChangesSinceParentCommit: 'staged-changes-since-parent-commit', + + isPartiallyStaged: fileName => `is-partially-staged:${fileName}`, + + filePatch: (fileName, {staged, amending}) => { + let opKey = ''; + if (staged && amending) { + opKey = 'a'; + } else if (staged) { + opKey = 's'; + } else { + opKey = 'u'; + } + + return `file-patch:${opKey}:${fileName}`; + }, + + index: fileName => `index:${fileName}`, + + lastCommit: 'last-commit', + + branches: 'branches', + + currentBranch: 'current-branch', + + remotes: 'remotes', + + aheadCount: branchName => `ahead-count:${branchName}`, + + behindCount: branchName => `beind-count:${branchName}`, + + config: (option, {local}) => { + const opKey = local ? 'l' : ''; + return `config:${opKey}:${option}`; + }, + + blob: sha => `blob:${sha}`, +}; From 9d237794d183cbe46d4fab263ade4d3026393b30 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 24 Apr 2017 11:47:20 -0400 Subject: [PATCH 03/55] Pending tests for using and invalidating the Repository cache --- test/models/repository.test.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 075d80c070..c3651cea46 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -230,6 +230,10 @@ describe('Repository', function() { repo.refresh(); assert.deepEqual(await repo.getStagedChangesSinceParentCommit(), []); }); + + it('selectively invalidates the cache on stage'); + + it('selectively invalidates the cache on unstage'); }); describe('getFilePatchForPath', function() { @@ -264,6 +268,8 @@ describe('Repository', function() { assert.notEqual(await repo.getFilePatchForPath('a.txt'), filePatchA); assert.deepEqual(await repo.getFilePatchForPath('a.txt'), filePatchA); }); + + it('uses cached data if available'); }); describe('applyPatchToIndex', function() { @@ -298,6 +304,8 @@ describe('Repository', function() { assert.deepEqual(unstagedChanges, ['subdir-1/a.txt']); assert.deepEqual(stagedChanges, []); }); + + it('selectively invalidates the cache'); }); describe('commit', function() { @@ -407,6 +415,10 @@ describe('Repository', function() { assert.deepEqual((await repo.getLastCommit()).getMessage(), 'Make a commit'); }); + + it('clears the stored resolution progress'); + + it('selectively invalidates the cache'); }); describe('fetch(branchName)', function() { @@ -427,6 +439,8 @@ describe('Repository', function() { assert.equal(remoteHead.message, 'third commit'); assert.equal(localHead.message, 'second commit'); }); + + it('selectively invalidates the cache'); }); describe('pull()', function() { @@ -447,6 +461,8 @@ describe('Repository', function() { assert.equal(remoteHead.message, 'third commit'); assert.equal(localHead.message, 'third commit'); }); + + it('selectively invalidates the cache'); }); describe('push()', function() { @@ -477,6 +493,8 @@ describe('Repository', function() { assert.equal(remoteHead.message, 'fifth commit'); assert.deepEqual(remoteHead, localRemoteHead); }); + + it('selectively invalidates the cache'); }); describe('getAheadCount(branchName) and getBehindCount(branchName)', function() { @@ -495,6 +513,8 @@ describe('Repository', function() { assert.equal(await localRepo.getBehindCount('master'), 1); assert.equal(await localRepo.getAheadCount('master'), 2); }); + + it('uses cached data if available'); }); describe('getRemoteForBranch(branchName)', function() { @@ -520,6 +540,8 @@ describe('Repository', function() { const remote2 = await localRepo.getRemoteForBranch('master'); assert.isFalse(remote2.isPresent()); }); + + it('uses cached data if available'); }); describe('merge conflicts', function() { @@ -592,6 +614,8 @@ describe('Repository', function() { const mergeConflicts = await repo.getMergeConflicts(); assert.deepEqual(mergeConflicts, []); }); + + it('uses cached data if available'); }); describe('stageFiles([path])', function() { @@ -738,6 +762,10 @@ describe('Repository', function() { assert.deepEqual(await repo.getUnstagedChanges(), unstagedChanges); }); }); + + it('clears the stored resolution progress'); + + it('selectively invalidates the cache'); }); }); @@ -787,6 +815,8 @@ describe('Repository', function() { repo.refresh(); assert.deepEqual(await repo.getUnstagedChanges(), []); }); + + it('selectively invalidates the cache'); }); describe('maintaining discard history across repository instances', function() { From 71b259bffaf621cbd301262226bc414866d75db3 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 24 Apr 2017 14:18:13 -0400 Subject: [PATCH 04/55] Test cache invalidation for a file staging event --- lib/models/repository-states/present.js | 46 +++++++++++++++++---- test/models/repository.test.js | 55 ++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 8 deletions(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index b17adf7de7..ee42f0f0a2 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -11,20 +11,24 @@ import Branch from '../branch'; import Remote from '../remote'; import Commit from '../commit'; +const ALL = Symbol('all'); + /** - * Decorator for an async method that invalidates the cache after execution (regardless of success or failure.) + * Decorator for an async method that invalidates the cache after execution (regardless of success or failure). + * Optionally parameterized by a function that accepts the same arguments as the function that returns the list of cache + * keys to invalidate. */ -function invalidate() { +function invalidate(spec = () => ALL) { return function(target, name, descriptor) { const original = descriptor.value; descriptor.value = function(...args) { return original.apply(this, args).then( result => { - this.refresh(); + this.acceptInvalidation(spec, args); return result; }, err => { - this.refresh(); + this.acceptInvalidation(spec, args); return Promise.reject(err); }, ); @@ -61,6 +65,16 @@ export default class Present extends State { return true; } + acceptInvalidation(spec, args) { + const keys = spec(args); + if (keys !== ALL) { + this.cache.invalidate(keys); + } else { + this.cache.clear(); + } + this.didUpdate(); + } + refresh() { this.cache.clear(); this.didUpdate(); @@ -84,7 +98,13 @@ export default class Present extends State { // Staging and unstaging - @invalidate() + @invalidate(paths => [ + Keys.changedFiles, + Keys.stagedChangesSinceParentCommit, + ...paths.map(Keys.isPartiallyStaged), + ...Keys.allFilePatches(paths), + ...paths.map(Keys.index), + ]) stageFiles(paths) { return this.git().stageFiles(paths); } @@ -483,7 +503,7 @@ class Cache { return created; } - invalidate(...keys) { + invalidate(keys) { for (let i = 0; i < keys.length; i++) { this.storage.delete(keys[i]); } @@ -526,7 +546,7 @@ const Keys = { aheadCount: branchName => `ahead-count:${branchName}`, - behindCount: branchName => `beind-count:${branchName}`, + behindCount: branchName => `behind-count:${branchName}`, config: (option, {local}) => { const opKey = local ? 'l' : ''; @@ -534,4 +554,16 @@ const Keys = { }, blob: sha => `blob:${sha}`, + + allFilePatches: fileNames => { + const keys = []; + for (let i = 0; i < fileNames.length; i++) { + keys.push( + Keys.filePatch(fileNames[i], {staged: false}), + Keys.filePatch(fileNames[i], {staged: true}), + Keys.filePatch(fileNames[i], {staged: true, amending: true}), + ); + } + return keys; + }, }; diff --git a/test/models/repository.test.js b/test/models/repository.test.js index c3651cea46..aa864837d8 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -9,7 +9,15 @@ import {expectedDelegates} from '../../lib/models/repository-states'; import {cloneRepository, assertDeepPropertyVals, setUpLocalAndRemoteRepositories, getHeadCommitOnRemote, assertEqualSortedArraysByKey} from '../helpers'; +const PRIMER = Symbol('cachePrimer'); + describe('Repository', function() { + function primeCache(repo, ...keys) { + for (const key of keys) { + repo.state.cache.getOrSet(key, () => Promise.resolve(PRIMER)); + } + } + it('delegates all state methods', function() { const missing = expectedDelegates.filter(delegateName => { return Repository.prototype[delegateName] === undefined; @@ -231,7 +239,52 @@ describe('Repository', function() { assert.deepEqual(await repo.getStagedChangesSinceParentCommit(), []); }); - it('selectively invalidates the cache on stage'); + it('selectively invalidates the cache on stage', async function() { + const workingDirPath = await cloneRepository('three-files'); + const changedFileName = path.join('subdir-1', 'a.txt'); + const unchangedFileName = 'b.txt'; + fs.writeFileSync(path.join(workingDirPath, changedFileName), 'wat', 'utf8'); + const repo = new Repository(workingDirPath); + await repo.getLoadPromise(); + + primeCache(repo, + 'changed-files', + `is-partially-staged:${changedFileName}`, + `file-patch:u:${changedFileName}`, `file-patch:s:${changedFileName}`, + `index:${changedFileName}`, + `is-partially-staged:${unchangedFileName}`, + `file-patch:u:${unchangedFileName}`, `file-patch:s:${unchangedFileName}`, + `index:${unchangedFileName}`, + 'last-commit', 'branches', 'current-branch', 'remotes', 'ahead-count:master', 'behind-count:master', + ); + + await repo.stageFiles([changedFileName]); + + const uninvalidated = await Promise.all([ + repo.isPartiallyStaged(unchangedFileName), + repo.getFilePatchForPath(unchangedFileName), + repo.getFilePatchForPath(unchangedFileName, {staged: true}), + repo.readFileFromIndex(unchangedFileName), + repo.getLastCommit(), + repo.getBranches(), + repo.getCurrentBranch(), + repo.getRemotes(), + repo.getAheadCount('master'), + repo.getBehindCount('master'), + ]); + + assert.isTrue(uninvalidated.every(result => result === PRIMER)); + + const invalidated = await Promise.all([ + repo.getStatusesForChangedFiles(), + repo.isPartiallyStaged(changedFileName), + repo.getFilePatchForPath(changedFileName), + repo.getFilePatchForPath(changedFileName, {staged: true}), + repo.readFileFromIndex(changedFileName), + ]); + + assert.isTrue(invalidated.every(result => result !== PRIMER)); + }); it('selectively invalidates the cache on unstage'); }); From 29243d1b71ad0bed3ae6bb004d299d00ee47977f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 24 Apr 2017 15:57:53 -0400 Subject: [PATCH 05/55] Populate the cache invalidation decorations --- lib/models/repository-states/present.js | 149 ++++++++++++++++++------ test/models/repository.test.js | 52 ++++++++- 2 files changed, 159 insertions(+), 42 deletions(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index ee42f0f0a2..cb604126ce 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -98,34 +98,28 @@ export default class Present extends State { // Staging and unstaging - @invalidate(paths => [ - Keys.changedFiles, - Keys.stagedChangesSinceParentCommit, - ...paths.map(Keys.isPartiallyStaged), - ...Keys.allFilePatches(paths), - ...paths.map(Keys.index), - ]) + @invalidate(paths => Keys.cacheOperationKeys(paths)) stageFiles(paths) { return this.git().stageFiles(paths); } - @invalidate() + @invalidate(paths => Keys.cacheOperationKeys(paths)) unstageFiles(paths) { return this.git().unstageFiles(paths); } - @invalidate() + @invalidate(paths => Keys.cacheOperationKeys(paths)) stageFilesFromParentCommit(paths) { return this.git().unstageFiles(paths, 'HEAD~'); } - @invalidate() + @invalidate(paths => Keys.cacheOperationKeys(paths)) applyPatchToIndex(filePatch) { const patchStr = filePatch.getHeaderString() + filePatch.toString(); return this.git().applyPatch(patchStr, {index: true}); } - @invalidate() + @invalidate(paths => Keys.cacheOperationKeys(paths)) applyPatchToWorkdir(filePatch) { const patchStr = filePatch.getHeaderString() + filePatch.toString(); return this.git().applyPatch(patchStr); @@ -133,53 +127,76 @@ export default class Present extends State { // Committing - @invalidate() + @invalidate(() => Keys.headOperationKeys()) commit(message, options) { return this.git().commit(formatCommitMessage(message), options); } // Merging - @invalidate() + @invalidate(() => Keys.headOperationKeys()) merge(branchName) { return this.git().merge(branchName); } - @invalidate() + @invalidate(() => [ + Keys.changedFiles, + Keys.stagedChangesSinceParentCommit, + Keys.allPartiallyStaged, + Keys.allFilePatches, + Keys.allIndices, + ]) abortMerge() { return this.git().abortMerge(); } - @invalidate() + @invalidate((side, paths) => [ + Keys.changedFiles, + Keys.stagedChangesSinceParentCommit, + ...paths.map(Keys.isPartiallyStaged), + ...Keys.allFilePatchOps(paths), + ...paths.map(Keys.index), + ]) checkoutSide(side, paths) { return this.git().checkoutSide(side, paths); } - @invalidate() mergeFile(oursPath, commonBasePath, theirsPath, resultPath) { return this.git().mergeFile(oursPath, commonBasePath, theirsPath, resultPath); } - @invalidate() + @invalidate(filePath => [ + Keys.changedFiles, + Keys.stagedChangesSinceParentCommit, + ...Keys.allFilePatchOps([filePath]), + Keys.index(filePath), + ]) writeMergeConflictToIndex(filePath, commonBaseSha, oursSha, theirsSha) { return this.git().writeMergeConflictToIndex(filePath, commonBaseSha, oursSha, theirsSha); } // Checkout - @invalidate() + @invalidate(() => Keys.headOperationKeys) checkout(revision, options = {}) { return this.git().checkout(revision, options); } - @invalidate() + @invalidate(paths => [ + Keys.changedFiles, + Keys.stagedChangesSinceParentCommit, + ...Keys.allFilePatchOps(paths), + ]) checkoutPathsAtRevision(paths, revision = 'HEAD') { return this.git().checkoutFiles(paths, revision); } // Remote interactions - @invalidate() + @invalidate(branchName => [ + Keys.aheadCount(branchName), + Keys.behindCount(branchName), + ]) async fetch(branchName) { const remote = await this.getRemoteForBranch(branchName); if (!remote.isPresent()) { @@ -188,7 +205,7 @@ export default class Present extends State { await this.git().fetch(remote.getName(), branchName); } - @invalidate() + @invalidate(() => Keys.headOperationKeys) async pull(branchName) { const remote = await this.getRemoteForBranch(branchName); if (!remote.isPresent()) { @@ -197,7 +214,18 @@ export default class Present extends State { await this.git().pull(remote.getName(), branchName); } - @invalidate() + @invalidate((branchName, options = {}) => { + const keys = [ + Keys.aheadCount(branchName), + Keys.behindCount(branchName), + ]; + + if (options.setUpstream) { + keys.push(Keys.config(`branch.${branchName}.remote`)); + } + + return keys; + }) async push(branchName, options) { const remote = await this.getRemoteForBranch(branchName); return this.git().push(remote.getNameOr('origin'), branchName, options); @@ -205,14 +233,15 @@ export default class Present extends State { // Configuration - @invalidate() + @invalidate(() => [ + Keys.allConfigs, + ]) setConfig(option, value, options) { return this.git().setConfig(option, value, options); } // Direct blob interactions - @invalidate() createBlob(options) { return this.git().createBlob(options); } @@ -261,7 +290,12 @@ export default class Present extends State { return this.saveDiscardHistory(); } - @invalidate() + @invalidate(paths => [ + Keys.changedFiles, + Keys.stagedChangesSinceParentCommit, + ...paths.map(Keys.isPartiallyStaged), + ...paths.map(filePath => Keys.filePatch(filePath, {staged: false})), + ]) async discardWorkDirChangesForPaths(paths) { const untrackedFiles = await this.git().getUntrackedFiles(); const [filesToRemove, filesToCheckout] = partition(paths, f => untrackedFiles.includes(f)); @@ -504,8 +538,22 @@ class Cache { } invalidate(keys) { + const keyPrefixes = []; for (let i = 0; i < keys.length; i++) { - this.storage.delete(keys[i]); + if (keys[i].startsWith('*')) { + keyPrefixes.push(keys[i]); + } else { + this.storage.delete(keys[i]); + } + } + + if (keyPrefixes.length > 0) { + const keyPattern = new RegExp(`^(?:${keyPrefixes.map(prefix => prefix.substring(1)).join('|')})`); + for (const existingKey of this.storage.keys()) { + if (keyPattern.test(existingKey)) { + this.storage.delete(existingKey); + } + } } } @@ -514,12 +562,14 @@ class Cache { } } + const Keys = { changedFiles: 'changed-files', stagedChangesSinceParentCommit: 'staged-changes-since-parent-commit', isPartiallyStaged: fileName => `is-partially-staged:${fileName}`, + allPartiallyStaged: '*is-partially-staged:', filePatch: (fileName, {staged, amending}) => { let opKey = ''; @@ -533,8 +583,21 @@ const Keys = { return `file-patch:${opKey}:${fileName}`; }, + allFilePatchOps: fileNames => { + const keys = []; + for (let i = 0; i < fileNames.length; i++) { + keys.push( + Keys.filePatch(fileNames[i], {staged: false}), + Keys.filePatch(fileNames[i], {staged: true}), + Keys.filePatch(fileNames[i], {staged: true, amending: true}), + ); + } + return keys; + }, + allFilePatches: '*file-patch:', index: fileName => `index:${fileName}`, + allIndices: '*index:', lastCommit: 'last-commit', @@ -545,25 +608,37 @@ const Keys = { remotes: 'remotes', aheadCount: branchName => `ahead-count:${branchName}`, + allAheadCounts: '*ahead-count:', behindCount: branchName => `behind-count:${branchName}`, + allBehindCounts: '*behind-count:', config: (option, {local}) => { const opKey = local ? 'l' : ''; return `config:${opKey}:${option}`; }, + allConfigs: '*config:', blob: sha => `blob:${sha}`, - allFilePatches: fileNames => { - const keys = []; - for (let i = 0; i < fileNames.length; i++) { - keys.push( - Keys.filePatch(fileNames[i], {staged: false}), - Keys.filePatch(fileNames[i], {staged: true}), - Keys.filePatch(fileNames[i], {staged: true, amending: true}), - ); - } - return keys; - }, + // Common collections of keys and patterns for use with @invalidate(). + + cacheOperationKeys: fileNames => [ + Keys.changedFiles, + Keys.stagedChangesSinceParentCommit, + ...fileNames.map(Keys.isPartiallyStaged), + ...Keys.allFilePatchOps(fileNames), + ...fileNames.map(Keys.index), + ], + + headOperationKeys: () => [ + Keys.changedFiles, + Keys.stagedChangesSinceParentCommit, + Keys.allPartiallyStaged, + Keys.allFilePatches, + Keys.allIndices, + Keys.lastCommit, + Keys.allAheadCounts, + Keys.allBehindCounts, + ], }; diff --git a/test/models/repository.test.js b/test/models/repository.test.js index aa864837d8..283681f8dd 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -286,7 +286,53 @@ describe('Repository', function() { assert.isTrue(invalidated.every(result => result !== PRIMER)); }); - it('selectively invalidates the cache on unstage'); + it('selectively invalidates the cache on unstage', async function() { + const workingDirPath = await cloneRepository('three-files'); + const changedFileName = path.join('subdir-1', 'a.txt'); + const unchangedFileName = 'b.txt'; + fs.writeFileSync(path.join(workingDirPath, changedFileName), 'wat', 'utf8'); + const repo = new Repository(workingDirPath); + await repo.getLoadPromise(); + await repo.stageFiles([changedFileName]); + + primeCache(repo, + 'changed-files', + `is-partially-staged:${changedFileName}`, + `file-patch:u:${changedFileName}`, `file-patch:s:${changedFileName}`, + `index:${changedFileName}`, + `is-partially-staged:${unchangedFileName}`, + `file-patch:u:${unchangedFileName}`, `file-patch:s:${unchangedFileName}`, + `index:${unchangedFileName}`, + 'last-commit', 'branches', 'current-branch', 'remotes', 'ahead-count:master', 'behind-count:master', + ); + + await repo.unstageFiles([changedFileName]); + + const uninvalidated = await Promise.all([ + repo.isPartiallyStaged(unchangedFileName), + repo.getFilePatchForPath(unchangedFileName), + repo.getFilePatchForPath(unchangedFileName, {staged: true}), + repo.readFileFromIndex(unchangedFileName), + repo.getLastCommit(), + repo.getBranches(), + repo.getCurrentBranch(), + repo.getRemotes(), + repo.getAheadCount('master'), + repo.getBehindCount('master'), + ]); + + assert.isTrue(uninvalidated.every(result => result === PRIMER)); + + const invalidated = await Promise.all([ + repo.getStatusesForChangedFiles(), + repo.isPartiallyStaged(changedFileName), + repo.getFilePatchForPath(changedFileName), + repo.getFilePatchForPath(changedFileName, {staged: true}), + repo.readFileFromIndex(changedFileName), + ]); + + assert.isTrue(invalidated.every(result => result !== PRIMER)); + }); }); describe('getFilePatchForPath', function() { @@ -321,8 +367,6 @@ describe('Repository', function() { assert.notEqual(await repo.getFilePatchForPath('a.txt'), filePatchA); assert.deepEqual(await repo.getFilePatchForPath('a.txt'), filePatchA); }); - - it('uses cached data if available'); }); describe('applyPatchToIndex', function() { @@ -357,8 +401,6 @@ describe('Repository', function() { assert.deepEqual(unstagedChanges, ['subdir-1/a.txt']); assert.deepEqual(stagedChanges, []); }); - - it('selectively invalidates the cache'); }); describe('commit', function() { From 7123b1ed20b1929bc4b93027797637d3590734c9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 24 Apr 2017 16:27:11 -0400 Subject: [PATCH 06/55] Invalidate correct keys for FilePatch operations --- lib/models/repository-states/present.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index cb604126ce..7835c54007 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -66,7 +66,7 @@ export default class Present extends State { } acceptInvalidation(spec, args) { - const keys = spec(args); + const keys = spec(...args); if (keys !== ALL) { this.cache.invalidate(keys); } else { @@ -113,13 +113,19 @@ export default class Present extends State { return this.git().unstageFiles(paths, 'HEAD~'); } - @invalidate(paths => Keys.cacheOperationKeys(paths)) + @invalidate(filePatch => [ + ...Keys.cacheOperationKeys([filePatch.getOldPath()]), + ...Keys.cacheOperationKeys([filePatch.getNewPath()]), + ]) applyPatchToIndex(filePatch) { const patchStr = filePatch.getHeaderString() + filePatch.toString(); return this.git().applyPatch(patchStr, {index: true}); } - @invalidate(paths => Keys.cacheOperationKeys(paths)) + @invalidate(filePatch => [ + ...Keys.cacheOperationKeys([filePatch.getOldPath()]), + ...Keys.cacheOperationKeys([filePatch.getNewPath()]), + ]) applyPatchToWorkdir(filePatch) { const patchStr = filePatch.getHeaderString() + filePatch.toString(); return this.git().applyPatch(patchStr); From ead66fa2dd0135fe83e352dc7b1a2067fc77620d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 25 Apr 2017 09:36:02 -0400 Subject: [PATCH 07/55] Accept fs events within .git/refs/heads --- lib/models/file-system-change-observer.js | 2 +- lib/models/workspace-change-observer.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/models/file-system-change-observer.js b/lib/models/file-system-change-observer.js index 1a9624119d..2368d41d2f 100644 --- a/lib/models/file-system-change-observer.js +++ b/lib/models/file-system-change-observer.js @@ -57,7 +57,7 @@ export default class FileSystemChangeObserver { const isNonGitFile = event => !event.directory.split(path.sep).includes('.git') && event.file !== '.git'; const isWatchedGitFile = event => { return ['config', 'index', 'HEAD', 'MERGE_HEAD'].includes(event.file || event.newFile) || - event.directory.includes(path.join('.git', 'refs', 'remotes')); + event.directory.includes(path.join('.git', 'refs')); }; const filteredEvents = events.filter(e => isNonGitFile(e) || isWatchedGitFile(e)); if (filteredEvents.length) { diff --git a/lib/models/workspace-change-observer.js b/lib/models/workspace-change-observer.js index b920d0e74e..f94265d2c8 100644 --- a/lib/models/workspace-change-observer.js +++ b/lib/models/workspace-change-observer.js @@ -1,3 +1,4 @@ +import path from 'path'; import {CompositeDisposable, Disposable, Emitter} from 'event-kit'; import nsfw from 'nsfw'; @@ -74,7 +75,8 @@ export default class WorkspaceChangeObserver { gitDirectoryPath, events => { const filteredEvents = events.filter(e => { - return ['config', 'index', 'HEAD', 'MERGE_HEAD'].includes(e.file || e.newFile); + return ['config', 'index', 'HEAD', 'MERGE_HEAD'].includes(e.file || e.newFile) || + event.directory.includes(path.join('.git', 'refs')); }); if (filteredEvents.length) { this.logger.showEvents(filteredEvents); From b849ec6d41b609b32ba0812a36a3290507d7359b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 25 Apr 2017 09:36:15 -0400 Subject: [PATCH 08/55] Use didChange() methods --- lib/models/file-system-change-observer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/models/file-system-change-observer.js b/lib/models/file-system-change-observer.js index 2368d41d2f..23571314e9 100644 --- a/lib/models/file-system-change-observer.js +++ b/lib/models/file-system-change-observer.js @@ -62,11 +62,11 @@ export default class FileSystemChangeObserver { const filteredEvents = events.filter(e => isNonGitFile(e) || isWatchedGitFile(e)); if (filteredEvents.length) { this.logger.showEvents(filteredEvents); - this.emitter.emit('did-change'); + this.didChange(filteredEvents); const workdirOrHeadEvent = filteredEvents.find(e => !['config', 'index'].includes(e.file || e.newFile)); if (workdirOrHeadEvent) { this.logger.showWorkdirOrHeadEvents(); - this.emitter.emit('did-change-workdir-or-head'); + this.didChangeWorkdirOrHead(); } } }, From 5b56a7ab243165c071972ba159dcf86cc183a900 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 25 Apr 2017 09:36:32 -0400 Subject: [PATCH 09/55] observeFilesystemChange accepts an argument --- lib/models/repository-states/state.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js index 95a8dd743c..f4bb702de8 100644 --- a/lib/models/repository-states/state.js +++ b/lib/models/repository-states/state.js @@ -130,7 +130,7 @@ export default class State { } @shouldDelegate - observeFilesystemChange() { + observeFilesystemChange(events) { this.repository.refresh(); } From 6e8f4937dc678b9cacb69981714b8bfdf051e938 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 25 Apr 2017 09:36:54 -0400 Subject: [PATCH 10/55] First pass at cache invalidation from filesystem events --- lib/models/repository-states/present.js | 55 +++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 7835c54007..23e741cdf5 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -75,6 +75,61 @@ export default class Present extends State { this.didUpdate(); } + observeFilesystemChange(events) { + const keys = new Set(); + for (let i = 0; i < events.length; i++) { + const event = events[i]; + + const fullPath = path.join(event.directory, event.file || event.newFile); + + const endsWith = (...segments) => fullPath.endsWith(path.join(...segments)); + const includes = (...segments) => fullPath.includes(path.join(...segments)); + + if (endsWith('.git', 'index')) { + keys.add(Keys.changedFiles); + keys.add(Keys.stagedChangesSinceParentCommit); + keys.add(Keys.allPartiallyStaged); + keys.add(Keys.allFilePatches); + keys.add(Keys.allIndices); + continue; + } + + if (endsWith('.git', 'HEAD')) { + keys.add(Keys.lastCommit); + keys.add(Keys.currentBranch); + keys.add(Keys.allAheadCounts); + keys.add(Keys.allBehindCounts); + continue; + } + + if (includes('.git', 'refs', 'heads')) { + keys.add(Keys.branches); + continue; + } + + if (includes('.git', 'refs', 'remotes')) { + keys.add(Keys.remotes); + continue; + } + + if (endsWith('.git', 'config')) { + keys.add(Keys.allConfigs); + keys.add(Keys.allAheadCounts); + keys.add(Keys.allBehindCounts); + continue; + } + + // File change within the working directory + const relativePath = path.relative(this.workdir(), fullPath); + keys.add(Keys.changedFiles); + keys.add(Keys.isPartiallyStaged(relativePath)); + keys.add(Keys.filePatch(relativePath, {staged: false, amending: false})); + } + + this.cache.invalidate(Array.from(keys)); + this.didUpdate(); + } + refresh() { this.cache.clear(); this.didUpdate(); From eeda89ca34cd3776b824db78e0a16a5ff898dbdd Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 25 Apr 2017 11:51:43 -0400 Subject: [PATCH 11/55] Fixture repository with both multiple files and commits --- test/fixtures/repo-multi-commits-files/a.txt | 1 + test/fixtures/repo-multi-commits-files/b.txt | 2 + test/fixtures/repo-multi-commits-files/c.txt | 2 + .../dot-git/COMMIT_EDITMSG | 1 + .../repo-multi-commits-files/dot-git/HEAD | 1 + .../repo-multi-commits-files/dot-git/MERGE_RR | 0 .../repo-multi-commits-files/dot-git/config | 7 + .../dot-git/description | 1 + .../dot-git/hooks/applypatch-msg.sample | 15 ++ .../dot-git/hooks/commit-msg.sample | 24 +++ .../dot-git/hooks/post-update.sample | 8 + .../dot-git/hooks/pre-applypatch.sample | 14 ++ .../dot-git/hooks/pre-commit.sample | 49 +++++ .../dot-git/hooks/pre-push.sample | 53 ++++++ .../dot-git/hooks/pre-rebase.sample | 169 ++++++++++++++++++ .../dot-git/hooks/prepare-commit-msg.sample | 36 ++++ .../dot-git/hooks/update.sample | 128 +++++++++++++ .../repo-multi-commits-files/dot-git/index | Bin 0 -> 554 bytes .../dot-git/info/exclude | 6 + .../dot-git/logs/HEAD | 6 + .../dot-git/logs/refs/heads/master | 6 + .../10/0b0dec8c53a40e4de7714b2c612dad5fad9985 | Bin 0 -> 19 bytes .../17/e8f63575fbe35334c35f33c30d72134b791491 | Bin 0 -> 24 bytes .../25/7cc5642cb1a054f08cc83f2d943e56fd3ebe99 | Bin 0 -> 19 bytes .../30/6c2d32d8fb911cb59da3bc2d600db0c7842fd9 | Bin 0 -> 157 bytes .../36/dbd4548ee626e65fb4efc4ac0951d2e7d13ef9 | Bin 0 -> 137 bytes .../45/2fd1b53795bdc838e89448f2954a2c1125ac08 | Bin 0 -> 824 bytes .../57/16ca5987cbf97d6bb54920bea6adde242d87e6 | Bin 0 -> 19 bytes .../5b/0f8e2fae7d9a5bda8a5a8e08b8d9ffac4ea5fb | 1 + .../66/d11860af6d28eb38349ef83de475597cb0e8b4 | 3 + .../6a/ec5997172db87e793a8ed284cd37c377c32d59 | Bin 0 -> 138 bytes .../75/284a7422bfe59a00dada1941ec74d3c030a458 | Bin 0 -> 24 bytes .../76/018072e09c5d31c8c6e3113b8aa0fe625195ca | Bin 0 -> 19 bytes .../8f/d7c1d85c8c3c78ad9c8a21c328b1905df638c7 | Bin 0 -> 829 bytes .../96/b005089c39ca199ba16a660f50903756e2abf7 | Bin 0 -> 27 bytes .../9e/6cfd1e2160f22c7c45259b720f0cc62c748c7c | Bin 0 -> 156 bytes .../a2/6e7c03dba9e3e17b0091a95eff67e663b6093a | Bin 0 -> 102 bytes .../b1/5a256b50a5de488c3af81a168c2eb3cf8056ab | Bin 0 -> 24 bytes .../bd/ee5a3a30f52329c051e63964dbb4294a9cec7b | Bin 0 -> 27 bytes .../bf/951bbda0483fbdbaa50e9db9c51ffb7d2f78f1 | Bin 0 -> 76 bytes .../c0/5164eba3a17e452dd77dd67c93032295860891 | Bin 0 -> 27 bytes .../ea/696dc1e354b6014bc5cc6e87c282e23a7d8605 | Bin 0 -> 137 bytes .../ea/e629e0574add4da0f67056fd3ec128c18b2c47 | Bin 0 -> 102 bytes .../f3/d6e209f77d0152610976d0bab77573c92c252c | Bin 0 -> 138 bytes .../f4/4d312c2f07c2b69ae48bdc2d1e2f125df388d1 | 2 + .../fe/0631213ec1a4a90900d82883fd88b8d8622026 | Bin 0 -> 102 bytes .../dot-git/refs/heads/master | 1 + .../repo-multi-commits-files/subdir-1/a.txt | 1 + .../repo-multi-commits-files/subdir-1/b.txt | 2 + .../repo-multi-commits-files/subdir-1/c.txt | 2 + 50 files changed, 541 insertions(+) create mode 100644 test/fixtures/repo-multi-commits-files/a.txt create mode 100644 test/fixtures/repo-multi-commits-files/b.txt create mode 100644 test/fixtures/repo-multi-commits-files/c.txt create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/COMMIT_EDITMSG create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/HEAD create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/MERGE_RR create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/config create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/description create mode 100755 test/fixtures/repo-multi-commits-files/dot-git/hooks/applypatch-msg.sample create mode 100755 test/fixtures/repo-multi-commits-files/dot-git/hooks/commit-msg.sample create mode 100755 test/fixtures/repo-multi-commits-files/dot-git/hooks/post-update.sample create mode 100755 test/fixtures/repo-multi-commits-files/dot-git/hooks/pre-applypatch.sample create mode 100755 test/fixtures/repo-multi-commits-files/dot-git/hooks/pre-commit.sample create mode 100755 test/fixtures/repo-multi-commits-files/dot-git/hooks/pre-push.sample create mode 100755 test/fixtures/repo-multi-commits-files/dot-git/hooks/pre-rebase.sample create mode 100755 test/fixtures/repo-multi-commits-files/dot-git/hooks/prepare-commit-msg.sample create mode 100755 test/fixtures/repo-multi-commits-files/dot-git/hooks/update.sample create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/index create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/info/exclude create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/logs/HEAD create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/logs/refs/heads/master create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/10/0b0dec8c53a40e4de7714b2c612dad5fad9985 create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/17/e8f63575fbe35334c35f33c30d72134b791491 create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/25/7cc5642cb1a054f08cc83f2d943e56fd3ebe99 create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/30/6c2d32d8fb911cb59da3bc2d600db0c7842fd9 create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/36/dbd4548ee626e65fb4efc4ac0951d2e7d13ef9 create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/45/2fd1b53795bdc838e89448f2954a2c1125ac08 create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/57/16ca5987cbf97d6bb54920bea6adde242d87e6 create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/5b/0f8e2fae7d9a5bda8a5a8e08b8d9ffac4ea5fb create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/66/d11860af6d28eb38349ef83de475597cb0e8b4 create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/6a/ec5997172db87e793a8ed284cd37c377c32d59 create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/75/284a7422bfe59a00dada1941ec74d3c030a458 create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/76/018072e09c5d31c8c6e3113b8aa0fe625195ca create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/8f/d7c1d85c8c3c78ad9c8a21c328b1905df638c7 create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/96/b005089c39ca199ba16a660f50903756e2abf7 create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/9e/6cfd1e2160f22c7c45259b720f0cc62c748c7c create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/a2/6e7c03dba9e3e17b0091a95eff67e663b6093a create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/b1/5a256b50a5de488c3af81a168c2eb3cf8056ab create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/bd/ee5a3a30f52329c051e63964dbb4294a9cec7b create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/bf/951bbda0483fbdbaa50e9db9c51ffb7d2f78f1 create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/c0/5164eba3a17e452dd77dd67c93032295860891 create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/ea/696dc1e354b6014bc5cc6e87c282e23a7d8605 create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/ea/e629e0574add4da0f67056fd3ec128c18b2c47 create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/f3/d6e209f77d0152610976d0bab77573c92c252c create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/f4/4d312c2f07c2b69ae48bdc2d1e2f125df388d1 create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/objects/fe/0631213ec1a4a90900d82883fd88b8d8622026 create mode 100644 test/fixtures/repo-multi-commits-files/dot-git/refs/heads/master create mode 100644 test/fixtures/repo-multi-commits-files/subdir-1/a.txt create mode 100644 test/fixtures/repo-multi-commits-files/subdir-1/b.txt create mode 100644 test/fixtures/repo-multi-commits-files/subdir-1/c.txt diff --git a/test/fixtures/repo-multi-commits-files/a.txt b/test/fixtures/repo-multi-commits-files/a.txt new file mode 100644 index 0000000000..257cc5642c --- /dev/null +++ b/test/fixtures/repo-multi-commits-files/a.txt @@ -0,0 +1 @@ +foo diff --git a/test/fixtures/repo-multi-commits-files/b.txt b/test/fixtures/repo-multi-commits-files/b.txt new file mode 100644 index 0000000000..75284a7422 --- /dev/null +++ b/test/fixtures/repo-multi-commits-files/b.txt @@ -0,0 +1,2 @@ +bar +bar 2 diff --git a/test/fixtures/repo-multi-commits-files/c.txt b/test/fixtures/repo-multi-commits-files/c.txt new file mode 100644 index 0000000000..17e8f63575 --- /dev/null +++ b/test/fixtures/repo-multi-commits-files/c.txt @@ -0,0 +1,2 @@ +baz +baz 2 diff --git a/test/fixtures/repo-multi-commits-files/dot-git/COMMIT_EDITMSG b/test/fixtures/repo-multi-commits-files/dot-git/COMMIT_EDITMSG new file mode 100644 index 0000000000..72818841d4 --- /dev/null +++ b/test/fixtures/repo-multi-commits-files/dot-git/COMMIT_EDITMSG @@ -0,0 +1 @@ +Another commit diff --git a/test/fixtures/repo-multi-commits-files/dot-git/HEAD b/test/fixtures/repo-multi-commits-files/dot-git/HEAD new file mode 100644 index 0000000000..cb089cd89a --- /dev/null +++ b/test/fixtures/repo-multi-commits-files/dot-git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/test/fixtures/repo-multi-commits-files/dot-git/MERGE_RR b/test/fixtures/repo-multi-commits-files/dot-git/MERGE_RR new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/fixtures/repo-multi-commits-files/dot-git/config b/test/fixtures/repo-multi-commits-files/dot-git/config new file mode 100644 index 0000000000..6c9406b7d9 --- /dev/null +++ b/test/fixtures/repo-multi-commits-files/dot-git/config @@ -0,0 +1,7 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true + ignorecase = true + precomposeunicode = true diff --git a/test/fixtures/repo-multi-commits-files/dot-git/description b/test/fixtures/repo-multi-commits-files/dot-git/description new file mode 100644 index 0000000000..498b267a8c --- /dev/null +++ b/test/fixtures/repo-multi-commits-files/dot-git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/test/fixtures/repo-multi-commits-files/dot-git/hooks/applypatch-msg.sample b/test/fixtures/repo-multi-commits-files/dot-git/hooks/applypatch-msg.sample new file mode 100755 index 0000000000..a5d7b84a67 --- /dev/null +++ b/test/fixtures/repo-multi-commits-files/dot-git/hooks/applypatch-msg.sample @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} +: diff --git a/test/fixtures/repo-multi-commits-files/dot-git/hooks/commit-msg.sample b/test/fixtures/repo-multi-commits-files/dot-git/hooks/commit-msg.sample new file mode 100755 index 0000000000..b58d1184a9 --- /dev/null +++ b/test/fixtures/repo-multi-commits-files/dot-git/hooks/commit-msg.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff --git a/test/fixtures/repo-multi-commits-files/dot-git/hooks/post-update.sample b/test/fixtures/repo-multi-commits-files/dot-git/hooks/post-update.sample new file mode 100755 index 0000000000..ec17ec1939 --- /dev/null +++ b/test/fixtures/repo-multi-commits-files/dot-git/hooks/post-update.sample @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff --git a/test/fixtures/repo-multi-commits-files/dot-git/hooks/pre-applypatch.sample b/test/fixtures/repo-multi-commits-files/dot-git/hooks/pre-applypatch.sample new file mode 100755 index 0000000000..4142082bcb --- /dev/null +++ b/test/fixtures/repo-multi-commits-files/dot-git/hooks/pre-applypatch.sample @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +precommit="$(git rev-parse --git-path hooks/pre-commit)" +test -x "$precommit" && exec "$precommit" ${1+"$@"} +: diff --git a/test/fixtures/repo-multi-commits-files/dot-git/hooks/pre-commit.sample b/test/fixtures/repo-multi-commits-files/dot-git/hooks/pre-commit.sample new file mode 100755 index 0000000000..68d62d5446 --- /dev/null +++ b/test/fixtures/repo-multi-commits-files/dot-git/hooks/pre-commit.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff --git a/test/fixtures/repo-multi-commits-files/dot-git/hooks/pre-push.sample b/test/fixtures/repo-multi-commits-files/dot-git/hooks/pre-push.sample new file mode 100755 index 0000000000..6187dbf439 --- /dev/null +++ b/test/fixtures/repo-multi-commits-files/dot-git/hooks/pre-push.sample @@ -0,0 +1,53 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +z40=0000000000000000000000000000000000000000 + +while read local_ref local_sha remote_ref remote_sha +do + if [ "$local_sha" = $z40 ] + then + # Handle delete + : + else + if [ "$remote_sha" = $z40 ] + then + # New branch, examine all commits + range="$local_sha" + else + # Update to existing branch, examine new commits + range="$remote_sha..$local_sha" + fi + + # Check for WIP commit + commit=`git rev-list -n 1 --grep '^WIP' "$range"` + if [ -n "$commit" ] + then + echo >&2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/test/fixtures/repo-multi-commits-files/dot-git/hooks/pre-rebase.sample b/test/fixtures/repo-multi-commits-files/dot-git/hooks/pre-rebase.sample new file mode 100755 index 0000000000..9773ed4cb2 --- /dev/null +++ b/test/fixtures/repo-multi-commits-files/dot-git/hooks/pre-rebase.sample @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up-to-date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +exit 0 + +################################################################ + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". diff --git a/test/fixtures/repo-multi-commits-files/dot-git/hooks/prepare-commit-msg.sample b/test/fixtures/repo-multi-commits-files/dot-git/hooks/prepare-commit-msg.sample new file mode 100755 index 0000000000..f093a02ec4 --- /dev/null +++ b/test/fixtures/repo-multi-commits-files/dot-git/hooks/prepare-commit-msg.sample @@ -0,0 +1,36 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first comments out the +# "Conflicts:" part of a merge commit. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +case "$2,$3" in + merge,) + /usr/bin/perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;; + +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$1" ;; + + *) ;; +esac + +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" diff --git a/test/fixtures/repo-multi-commits-files/dot-git/hooks/update.sample b/test/fixtures/repo-multi-commits-files/dot-git/hooks/update.sample new file mode 100755 index 0000000000..d84758373d --- /dev/null +++ b/test/fixtures/repo-multi-commits-files/dot-git/hooks/update.sample @@ -0,0 +1,128 @@ +#!/bin/sh +# +# An example hook script to blocks unannotated tags from entering. +# Called by "git receive-pack" with arguments: refname sha1-old sha1-new +# +# To enable this hook, rename this file to "update". +# +# Config +# ------ +# hooks.allowunannotated +# This boolean sets whether unannotated tags will be allowed into the +# repository. By default they won't be. +# hooks.allowdeletetag +# This boolean sets whether deleting tags will be allowed in the +# repository. By default they won't be. +# hooks.allowmodifytag +# This boolean sets whether a tag may be modified after creation. By default +# it won't be. +# hooks.allowdeletebranch +# This boolean sets whether deleting branches will be allowed in the +# repository. By default they won't be. +# hooks.denycreatebranch +# This boolean sets whether remotely creating branches will be denied +# in the repository. By default this is allowed. +# + +# --- Command line +refname="$1" +oldrev="$2" +newrev="$3" + +# --- Safety check +if [ -z "$GIT_DIR" ]; then + echo "Don't run this script from the command line." >&2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --bool hooks.allowunannotated) +allowdeletebranch=$(git config --bool hooks.allowdeletebranch) +denycreatebranch=$(git config --bool hooks.denycreatebranch) +allowdeletetag=$(git config --bool hooks.allowdeletetag) +allowmodifytag=$(git config --bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero="0000000000000000000000000000000000000000" +if [ "$newrev" = "$zero" ]; then + newrev_type=delete +else + newrev_type=$(git cat-file -t $newrev) +fi + +case "$refname","$newrev_type" in + refs/tags/*,commit) + # un-annotated tag + short_refname=${refname##refs/tags/} + if [ "$allowunannotated" != "true" ]; then + echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 diff --git a/test/fixtures/repo-multi-commits-files/dot-git/index b/test/fixtures/repo-multi-commits-files/dot-git/index new file mode 100644 index 0000000000000000000000000000000000000000..218cea2ac9663a7ad425adddfa126b35efaddd70 GIT binary patch literal 554 zcmZ?q402{*U|<4bwut|ECxJ8rjAmqDU|~!|oWMECyE2$^}X$R?_2%(cedKjSQTt_p9t5n0QL}~xiSq!&sNjknM zxqQH2NdyCH5;k*wA(?Xn-5l{3-%Lw?KMpoI9B+J>w@BE#Qe+|nYcjGqXCUs$1Je-q zTu?+a2fM%cic6DHGK+K#_2C{w_%B%+;jRlxXy)-A2uyjscwwEZ?)BPhHItc@rnYfR zWI&h)_b8Hia!BSWqno$)U6hrcZg0`_n)9X_$vn7+LxNmgf#z5-m?;=? zWxa` 1466116715 -0600 commit (initial): Initial commit +f44d312c2f07c2b69ae48bdc2d1e2f125df388d1 9e6cfd1e2160f22c7c45259b720f0cc62c748c7c Antonio Scandurra 1466159114 +0200 commit (amend): Initial commit +9e6cfd1e2160f22c7c45259b720f0cc62c748c7c 306c2d32d8fb911cb59da3bc2d600db0c7842fd9 Antonio Scandurra 1466604506 +0200 commit (amend): Initial commit +306c2d32d8fb911cb59da3bc2d600db0c7842fd9 66d11860af6d28eb38349ef83de475597cb0e8b4 Antonio Scandurra 1466604645 +0200 commit (amend): Initial commit +66d11860af6d28eb38349ef83de475597cb0e8b4 452fd1b53795bdc838e89448f2954a2c1125ac08 Ash Wilson 1493132091 -0400 commit: Another commit +452fd1b53795bdc838e89448f2954a2c1125ac08 8fd7c1d85c8c3c78ad9c8a21c328b1905df638c7 Ash Wilson 1493135077 -0400 commit (amend): Another commit diff --git a/test/fixtures/repo-multi-commits-files/dot-git/logs/refs/heads/master b/test/fixtures/repo-multi-commits-files/dot-git/logs/refs/heads/master new file mode 100644 index 0000000000..a8615335ff --- /dev/null +++ b/test/fixtures/repo-multi-commits-files/dot-git/logs/refs/heads/master @@ -0,0 +1,6 @@ +0000000000000000000000000000000000000000 f44d312c2f07c2b69ae48bdc2d1e2f125df388d1 Nathan Sobo 1466116715 -0600 commit (initial): Initial commit +f44d312c2f07c2b69ae48bdc2d1e2f125df388d1 9e6cfd1e2160f22c7c45259b720f0cc62c748c7c Antonio Scandurra 1466159114 +0200 commit (amend): Initial commit +9e6cfd1e2160f22c7c45259b720f0cc62c748c7c 306c2d32d8fb911cb59da3bc2d600db0c7842fd9 Antonio Scandurra 1466604506 +0200 commit (amend): Initial commit +306c2d32d8fb911cb59da3bc2d600db0c7842fd9 66d11860af6d28eb38349ef83de475597cb0e8b4 Antonio Scandurra 1466604645 +0200 commit (amend): Initial commit +66d11860af6d28eb38349ef83de475597cb0e8b4 452fd1b53795bdc838e89448f2954a2c1125ac08 Ash Wilson 1493132091 -0400 commit: Another commit +452fd1b53795bdc838e89448f2954a2c1125ac08 8fd7c1d85c8c3c78ad9c8a21c328b1905df638c7 Ash Wilson 1493135077 -0400 commit (amend): Another commit diff --git a/test/fixtures/repo-multi-commits-files/dot-git/objects/10/0b0dec8c53a40e4de7714b2c612dad5fad9985 b/test/fixtures/repo-multi-commits-files/dot-git/objects/10/0b0dec8c53a40e4de7714b2c612dad5fad9985 new file mode 100644 index 0000000000000000000000000000000000000000..cd502a84d6c8ae9fa760013c2ba6bf767ae88122 GIT binary patch literal 19 acmb!)WjzMtc2-6JYjX!z literal 0 HcmV?d00001 diff --git a/test/fixtures/repo-multi-commits-files/dot-git/objects/25/7cc5642cb1a054f08cc83f2d943e56fd3ebe99 b/test/fixtures/repo-multi-commits-files/dot-git/objects/25/7cc5642cb1a054f08cc83f2d943e56fd3ebe99 new file mode 100644 index 0000000000000000000000000000000000000000..bdcf704c9e663f3a11b3146b1b455bc2581b4761 GIT binary patch literal 19 acmb+ckUO@FkN|99tS|r3~ulu3I3b!mJrvj7b9JX2+Mt-+Gr9XhMH%M?qy#viXCrSK* LpVWK+S@1g@D$qr( literal 0 HcmV?d00001 diff --git a/test/fixtures/repo-multi-commits-files/dot-git/objects/36/dbd4548ee626e65fb4efc4ac0951d2e7d13ef9 b/test/fixtures/repo-multi-commits-files/dot-git/objects/36/dbd4548ee626e65fb4efc4ac0951d2e7d13ef9 new file mode 100644 index 0000000000000000000000000000000000000000..67c9a4615efcfdd4615410101446239e90ada0cd GIT binary patch literal 137 zcmV;40CxX)0V^p=O;s>7HeoO{FfcPQQApG)sVHIC7^RvWu=JiskJS$;u^zq6=NrOS zLlq=}6_jdtl_>3hI*Z}fElI~WC6^BvEQx?BNCqnqfAP(<^!MXnlf&`Ghk1*Hy(>i~ rniv3qLUCzQN@kI+A;Uj5Lq)rTOIC6++|X$L+p*(Dl7bolbJ;a)_uo5P literal 0 HcmV?d00001 diff --git a/test/fixtures/repo-multi-commits-files/dot-git/objects/45/2fd1b53795bdc838e89448f2954a2c1125ac08 b/test/fixtures/repo-multi-commits-files/dot-git/objects/45/2fd1b53795bdc838e89448f2954a2c1125ac08 new file mode 100644 index 0000000000000000000000000000000000000000..db68cf8e961f069795e54d3c8f49120802936bc5 GIT binary patch literal 824 zcmV-81IPS$0iBb}&Z9;UMziKA*jp1fcaSnsnmfkEfZ@0C4w~j-Y_5&Le){puD7&mu zyE;-yRi*Es?(wJz%>Y^cgNzu{oS0`hFYq$PA}eB1C^8-wh))qv1SV&(lrtPI6sk{0 z+!9(8bB2*ckQQRj$~fa>j#qFYb2;WEK~N-=0W4=cmCmHO^J2vrTj+^h}5JFRx0E!Aa5UO(?DGdm|s#I~o-g!kJvc~4L)o8Vtg z(d}tK%Cg;}Qp{xwxMJo5&Umn<6+g4{^)NM~Kx%;YL=1GjJ!nCw44@VXsjQ-3EQ9JQIeD@E(KqVQx* zxh(U6x8Ba}u{>kB?-ZrXbt5yp!M_J16s4x#jqNUZ+)G=HZgu~>FAa>V&nKPv9EaN* zVV_BKxBZkL#!xcH^ZHQdtiCnC3oi3M)Gnlm)sev(!)5#nMEM(rOGME!JD_1(UeW&T zuKP=`y!Z9~8A7{@P2X9z=>eeBmO+67S3?8CR%>`BR^>O5E2Sx<=&z1(O|~~~nGUqOub(^^?@5fU5ng-37i^EjnD=TG ziWOA-A~B!W+8QmjHQmtIKz+k(jPDfv{geHX`t6M2>HlR61>25PA1Qz9N&NsY8(ZZy C8J&v& literal 0 HcmV?d00001 diff --git a/test/fixtures/repo-multi-commits-files/dot-git/objects/57/16ca5987cbf97d6bb54920bea6adde242d87e6 b/test/fixtures/repo-multi-commits-files/dot-git/objects/57/16ca5987cbf97d6bb54920bea6adde242d87e6 new file mode 100644 index 0000000000000000000000000000000000000000..cfc3920fb3f273219368fbff8644d599281d31f4 GIT binary patch literal 19 acmbÁq%–ÄB‚×÷·ÑÑñû¿ëºfúƒ´” žqf“zï"jã"{fLa`l²= s@ím²Ô’… +L5VËw\ŸY–-ž¸®0ÑŒ‡N£Öj¯û¡¤·"µä +S™·ÖFzwœó€Ú¡ópÔvÔ½dÉô‚Ÿ¤>¸Ý;± \ No newline at end of file diff --git a/test/fixtures/repo-multi-commits-files/dot-git/objects/6a/ec5997172db87e793a8ed284cd37c377c32d59 b/test/fixtures/repo-multi-commits-files/dot-git/objects/6a/ec5997172db87e793a8ed284cd37c377c32d59 new file mode 100644 index 0000000000000000000000000000000000000000..9d95174d9cc937b58e58b5ebf462adab08edd26c GIT binary patch literal 138 zcmV;50CoR(0V^p=O;s>7HeoO{FfcPQQApG)sVHGktvQ;avvEPlhn^Gmx>M}J{@U%E z3005;R#2+pRid>2=`4m@w2nl`QPRdLX=Hm#s~sOaAXd64srsO@-QcPI?E|8qqt9!IU^vibcEfE zvTRNeJd39V`x%0O;wf5y1yALW<`_ocGJr!K&`7$>RX?KQRH0vWGxZ(%$JC}%wf!vq zD(ksg!0%b#{)rN_KoJy)3k3QNr*RzlpdXeq{6Fgq&T;>&m#=cFOZ3~1kW}4pP*3;J zz|b8ryj|6wID#T*?Hg}N6eU>{bFEfYC)m%vyj9U`(Q}Xg}sKOKV)=iSL!h0QKpuX=`Y&xp8YmDB2G%aUJQ*1-F9Q1Ak0Yx zL5G^COfACC?y%%zT|6u|n@`G_V^l|hPTjkMELM-&$FI)TXzDYCmVA&!`DSf*K9e5~ zdG{c(8ZQ(p(Vyap49&I2f=Xt^=xfLY=4MB-Y(vVbvD;o)Y1tR2&dVQbE_Sa-LQv+e zXRg`TMLeFkFR8I*;*9y*+rrs^CYt-?yk=lk8uf%c{vP^?Xb-*12I;YIhIWMrit%jo z$o7yRnSKSq;uD=mGzVZg{+*<_qfvh=9S+r-YM2J>#sQcKqKhVDrr1h`P9PaJTh@MxoKns|D*9{x2;eTCr`-dW zDLD@lj7NTTtT^7pv=Ww32llE<05}CX5t`#-PtOd5SHEm$6+!9a3=URHcOhH_3F$^A$*kS;Z)d{@- literal 0 HcmV?d00001 diff --git a/test/fixtures/repo-multi-commits-files/dot-git/objects/9e/6cfd1e2160f22c7c45259b720f0cc62c748c7c b/test/fixtures/repo-multi-commits-files/dot-git/objects/9e/6cfd1e2160f22c7c45259b720f0cc62c748c7c new file mode 100644 index 0000000000000000000000000000000000000000..395bc9ea6e22b026122827cc2ef29f54564e4101 GIT binary patch literal 156 zcmV;N0Av4n0cFj-4uUWghGFMk#n}y}r}PIBVsvzI;R+6IktWb4^>F_Z9UZ@Scs^gY zEiqu%p18&ck)l<7l*VZaK?jnvb<)^5w9%vLJa&`v+6mfuDHWc?3uPF~RA9*8aeF0R z_UYowb_LYdB3g$A8fm2zM?DTXR=8&_ITaW^<*?UEFwnd4Dg6VcN7QhZ>Hx$eCr-4& KPZA&SGCT=0-$#`I literal 0 HcmV?d00001 diff --git a/test/fixtures/repo-multi-commits-files/dot-git/objects/a2/6e7c03dba9e3e17b0091a95eff67e663b6093a b/test/fixtures/repo-multi-commits-files/dot-git/objects/a2/6e7c03dba9e3e17b0091a95eff67e663b6093a new file mode 100644 index 0000000000000000000000000000000000000000..9432c3ab82903a5f19dedbbbc40f9bf3eefd2e40 GIT binary patch literal 102 zcmV-s0Ga=I0V^p=O;xb8WH2-^Ff%bxNYpE-C}B{oIhvxgaY4w3o)h-EQ|!Y2+U=VO zRgeT$a3C<{_2PwfuDaK2uhmRuR+`$zF%haD8LVLMyC^GzugaPS0-sr?+}@(;HRnw= I06``x^h&)jTmS$7 literal 0 HcmV?d00001 diff --git a/test/fixtures/repo-multi-commits-files/dot-git/objects/b1/5a256b50a5de488c3af81a168c2eb3cf8056ab b/test/fixtures/repo-multi-commits-files/dot-git/objects/b1/5a256b50a5de488c3af81a168c2eb3cf8056ab new file mode 100644 index 0000000000000000000000000000000000000000..c59a2079e4701e047f21aa96acc280be1eb90e76 GIT binary patch literal 24 fcmb6V=y!@Ff%bxNYpE-C}B{oIhvxgaY4w3o)h-EQ|!Y2+U=VO iRgeT$5H5Bqvi literal 0 HcmV?d00001 diff --git a/test/fixtures/repo-multi-commits-files/dot-git/objects/c0/5164eba3a17e452dd77dd67c93032295860891 b/test/fixtures/repo-multi-commits-files/dot-git/objects/c0/5164eba3a17e452dd77dd67c93032295860891 new file mode 100644 index 0000000000000000000000000000000000000000..2cf183e1d8368cd9b51b7ef006ad50cc643b95aa GIT binary patch literal 27 icmbQxKP141@bsHah@-sR(EQ literal 0 HcmV?d00001 diff --git a/test/fixtures/repo-multi-commits-files/dot-git/objects/ea/696dc1e354b6014bc5cc6e87c282e23a7d8605 b/test/fixtures/repo-multi-commits-files/dot-git/objects/ea/696dc1e354b6014bc5cc6e87c282e23a7d8605 new file mode 100644 index 0000000000000000000000000000000000000000..0960a59fb02b8e821c5080d8679b5cadaae6a834 GIT binary patch literal 137 zcmV;40CxX)0V^p=O;s>7HeoO{FfcPQQApG)sVHGktvQ;avvEPlhn^Gmx>M}J{@U%E z3005;RuC?BDzg3b&)V#*o(lVxt-YtB+x`ryAQ`NnjIp8U!JJsb6UQD4T6Zn@mlQbl rl!*ZlC={0_rDPWA8Zx|kruiV;>#pyDZv{XL4r(0i)^P^_I&wMzuysCj literal 0 HcmV?d00001 diff --git a/test/fixtures/repo-multi-commits-files/dot-git/objects/ea/e629e0574add4da0f67056fd3ec128c18b2c47 b/test/fixtures/repo-multi-commits-files/dot-git/objects/ea/e629e0574add4da0f67056fd3ec128c18b2c47 new file mode 100644 index 0000000000000000000000000000000000000000..4013fd7e59651a5c99ecdd2decd3d7ecdc99114c GIT binary patch literal 102 zcmV-s0Ga=I0V^p=O;xb8WH2-^Ff%bxNYpE-C}B{oIhvxgaY4w3o)h-EQ|!Y2+U=VO zRgeT$5H5BqvicP;pr6gc%1 I09Vl}{eDC-*#H0l literal 0 HcmV?d00001 diff --git a/test/fixtures/repo-multi-commits-files/dot-git/objects/f3/d6e209f77d0152610976d0bab77573c92c252c b/test/fixtures/repo-multi-commits-files/dot-git/objects/f3/d6e209f77d0152610976d0bab77573c92c252c new file mode 100644 index 0000000000000000000000000000000000000000..39d3c83077017ac081bc62666926737add9111c7 GIT binary patch literal 138 zcmV;50CoR(0V^p=O;s>7HeoO{FfcPQQApG)sVHGktvQ;avvEPlhn^Gmx>M}J{@U%E z3005;RuC?BDzg3b&)V#*o(lVxt-YtB+x`ryAQ`NnjIp8U!JJsb6UQD4T6Zn@mlQbl sl!*ZlC={0_rDPWA8Zt!l_vx>zofUnnE2@uU$Ibt1{FeR(03csE;}CB`WdHyG literal 0 HcmV?d00001 diff --git a/test/fixtures/repo-multi-commits-files/dot-git/objects/f4/4d312c2f07c2b69ae48bdc2d1e2f125df388d1 b/test/fixtures/repo-multi-commits-files/dot-git/objects/f4/4d312c2f07c2b69ae48bdc2d1e2f125df388d1 new file mode 100644 index 0000000000..2a4e5659b1 --- /dev/null +++ b/test/fixtures/repo-multi-commits-files/dot-git/objects/f4/4d312c2f07c2b69ae48bdc2d1e2f125df388d1 @@ -0,0 +1,2 @@ +xK +1]ç}%­“dƒ¸uãÆtçc&¡çþz—¯ ê…ÑZ@粦œ½AæHzš/™#|d æÌ.ž³›3*Ú¤Œ$…:<XúwÜ^UÊÆ§0Úp²Ñ:4pÔVkµÓýPÒªº÷*•Þðk¨ÿ:º \ No newline at end of file diff --git a/test/fixtures/repo-multi-commits-files/dot-git/objects/fe/0631213ec1a4a90900d82883fd88b8d8622026 b/test/fixtures/repo-multi-commits-files/dot-git/objects/fe/0631213ec1a4a90900d82883fd88b8d8622026 new file mode 100644 index 0000000000000000000000000000000000000000..dd0dd9a1428167a9014a9c2cc74e03bdb72c3f8a GIT binary patch literal 102 zcmV-s0Ga=I0V^p=O;xb8WH2-^Ff%bxNYpE-C}Ehkft6#9%|M}Ty?M4UaOhRtTeTaVRGFZXhcTrXbUzIfv1U|D&xxGcxYtEZ$ I0QA2p2Jb>G-T(jq literal 0 HcmV?d00001 diff --git a/test/fixtures/repo-multi-commits-files/dot-git/refs/heads/master b/test/fixtures/repo-multi-commits-files/dot-git/refs/heads/master new file mode 100644 index 0000000000..ff0be8ace2 --- /dev/null +++ b/test/fixtures/repo-multi-commits-files/dot-git/refs/heads/master @@ -0,0 +1 @@ +8fd7c1d85c8c3c78ad9c8a21c328b1905df638c7 diff --git a/test/fixtures/repo-multi-commits-files/subdir-1/a.txt b/test/fixtures/repo-multi-commits-files/subdir-1/a.txt new file mode 100644 index 0000000000..257cc5642c --- /dev/null +++ b/test/fixtures/repo-multi-commits-files/subdir-1/a.txt @@ -0,0 +1 @@ +foo diff --git a/test/fixtures/repo-multi-commits-files/subdir-1/b.txt b/test/fixtures/repo-multi-commits-files/subdir-1/b.txt new file mode 100644 index 0000000000..c05164eba3 --- /dev/null +++ b/test/fixtures/repo-multi-commits-files/subdir-1/b.txt @@ -0,0 +1,2 @@ +bar +subbar 2 diff --git a/test/fixtures/repo-multi-commits-files/subdir-1/c.txt b/test/fixtures/repo-multi-commits-files/subdir-1/c.txt new file mode 100644 index 0000000000..bdee5a3a30 --- /dev/null +++ b/test/fixtures/repo-multi-commits-files/subdir-1/c.txt @@ -0,0 +1,2 @@ +baz +subbaz 2 From bc1dc4341e6859da31aced640fedd0e8320b21c9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 25 Apr 2017 11:54:08 -0400 Subject: [PATCH 12/55] :gear: Automated-ish cache invalidation test framework --- package.json | 1 + test/models/repository.test.js | 218 +++++++++++++++++++++++++++++---- 2 files changed, 198 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 144704072d..0e9091b8e4 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "eslint-plugin-promise": "^3.5.0", "eslint-plugin-react": "^6.7.1", "eslint-plugin-standard": "^3.0.1", + "lodash.isequal": "^4.5.0", "mkdirp": "^0.5.1", "mocha": "^3.2.0", "mocha-appveyor-reporter": "^0.4.0", diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 283681f8dd..416ef0ce84 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -3,11 +3,17 @@ import path from 'path'; import dedent from 'dedent-js'; import temp from 'temp'; import util from 'util'; +import compareSets from 'compare-sets'; +import isEqual from 'lodash.isequal'; import Repository from '../../lib/models/repository'; import {expectedDelegates} from '../../lib/models/repository-states'; -import {cloneRepository, assertDeepPropertyVals, setUpLocalAndRemoteRepositories, getHeadCommitOnRemote, assertEqualSortedArraysByKey} from '../helpers'; +import { + cloneRepository, setUpLocalAndRemoteRepositories, getHeadCommitOnRemote, + assertDeepPropertyVals, assertEqualSortedArraysByKey, +} from '../helpers'; +import {writeFile} from '../../lib/helpers'; const PRIMER = Symbol('cachePrimer'); @@ -512,8 +518,6 @@ describe('Repository', function() { }); it('clears the stored resolution progress'); - - it('selectively invalidates the cache'); }); describe('fetch(branchName)', function() { @@ -534,8 +538,6 @@ describe('Repository', function() { assert.equal(remoteHead.message, 'third commit'); assert.equal(localHead.message, 'second commit'); }); - - it('selectively invalidates the cache'); }); describe('pull()', function() { @@ -556,8 +558,6 @@ describe('Repository', function() { assert.equal(remoteHead.message, 'third commit'); assert.equal(localHead.message, 'third commit'); }); - - it('selectively invalidates the cache'); }); describe('push()', function() { @@ -588,8 +588,6 @@ describe('Repository', function() { assert.equal(remoteHead.message, 'fifth commit'); assert.deepEqual(remoteHead, localRemoteHead); }); - - it('selectively invalidates the cache'); }); describe('getAheadCount(branchName) and getBehindCount(branchName)', function() { @@ -608,8 +606,6 @@ describe('Repository', function() { assert.equal(await localRepo.getBehindCount('master'), 1); assert.equal(await localRepo.getAheadCount('master'), 2); }); - - it('uses cached data if available'); }); describe('getRemoteForBranch(branchName)', function() { @@ -635,8 +631,6 @@ describe('Repository', function() { const remote2 = await localRepo.getRemoteForBranch('master'); assert.isFalse(remote2.isPresent()); }); - - it('uses cached data if available'); }); describe('merge conflicts', function() { @@ -709,8 +703,6 @@ describe('Repository', function() { const mergeConflicts = await repo.getMergeConflicts(); assert.deepEqual(mergeConflicts, []); }); - - it('uses cached data if available'); }); describe('stageFiles([path])', function() { @@ -857,10 +849,6 @@ describe('Repository', function() { assert.deepEqual(await repo.getUnstagedChanges(), unstagedChanges); }); }); - - it('clears the stored resolution progress'); - - it('selectively invalidates the cache'); }); }); @@ -910,8 +898,6 @@ describe('Repository', function() { repo.refresh(); assert.deepEqual(await repo.getUnstagedChanges(), []); }); - - it('selectively invalidates the cache'); }); describe('maintaining discard history across repository instances', function() { @@ -955,4 +941,194 @@ describe('Repository', function() { await repo2.getLoadPromise(); }); }); + + describe('cache invalidation', function() { + const getCacheReaderMethods = options => { + const repository = options.repository; + const calls = new Map(); + + calls.set('getStatusesForChangedFiles', () => repository.getStatusesForChangedFiles()); + calls.set('getStagedChangesSinceParentCommit', () => repository.getStagedChangesSinceParentCommit()); + calls.set('getLastCommit', () => repository.getLastCommit()); + calls.set('getBranches', () => repository.getBranches()); + calls.set('getCurrentBranch', () => repository.getCurrentBranch()); + calls.set('getRemotes', () => repository.getRemotes()); + + const withFile = (fileName, description) => { + calls.set(`isPartiallyStaged ${description}`, () => repository.isPartiallyStaged(fileName)); + calls.set( + `getFilePatchForPath {unstaged} ${description}`, + () => repository.getFilePatchForPath(fileName, {staged: false}), + ); + calls.set( + `getFilePatchForPath {staged} ${description}`, + () => repository.getFilePatchForPath(fileName, {staged: true}), + ); + calls.set( + `getFilePatchForPath {staged, amending} ${description}`, + () => repository.getFilePatchForPath(fileName, {staged: true, amending: true}), + ); + calls.set(`readFileFromIndex ${description}`, () => repository.readFileFromIndex(fileName)); + }; + + if (options.changedFile) { + withFile(options.changedFile, 'changed'); + } + if (options.unchangedFile) { + withFile(options.unchangedFile, 'unchanged'); + } + + const withBranch = (branchName, description) => { + calls.set(`getAheadCount ${description}`, () => repository.getAheadCount(branchName)); + calls.set(`getBehindCount ${description}`, () => repository.getBehindCount(branchName)); + }; + + if (options.changedBranch) { + withBranch(options.changedBranch); + } + if (options.unchangedBranch) { + withBranch(options.unchangedBranch); + } + + if (options.changedConfig) { + calls.set('getConfig changed', () => repository.getConfig(options.changedConfig)); + calls.set('getConfig {local} changed', () => repository.getConfig(options.changedConfig, {local: true})); + } + if (options.unchangedConfig) { + calls.set('getConfig unchanged', () => repository.getConfig(options.unchangedConfig)); + calls.set('getConfig {local} unchanged', () => repository.getConfig(options.unchangedConfig, {local: true})); + } + + return calls; + }; + + /** + * Ensure that the correct cache keys are invalidated by a Repository operation. + */ + async function assertCorrectInvalidation(options, operation) { + const methods = getCacheReaderMethods(options); + + const record = () => { + const results = new Map(); + return Promise.all( + Array.from(methods, ([name, call]) => { + return call().then(result => results.set(name, result)); + }), + ).then(() => results); + }; + + const changedKeys = (mapA, mapB, identity) => { + assert.sameMembers(Array.from(mapA.keys()), Array.from(mapB.keys())); + + const compareValues = (valueA, valueB) => { + if (identity) { + return valueA === valueB; + } else { + return isEqual(valueA, valueB); + } + }; + + const results = new Set(); + for (const key of mapA.keys()) { + const valueA = mapA.get(key); + const valueB = mapB.get(key); + + if (!compareValues(valueA, valueB)) { + results.add(key); + } + } + return results; + }; + + const before = await record(); + await operation(); + const cached = await record(); + + options.repository.state.cache.clear(); + const after = await record(); + + const expected = changedKeys(before, after, false); + const actual = changedKeys(before, cached, true); + const {added, removed} = compareSets(expected, actual); + + /* eslint-disable no-console */ + if (added.size > 0) { + console.log('These cached method results were invalidated, but should not have been:'); + + for (const key of added) { + console.log(` ${key}:`); + console.log(' before:', before.get(key)); + console.log(' cached:', cached.get(key)); + console.log(' after:', after.get(key)); + } + } + + if (removed.size > 0) { + console.log('These cached method results should have been invalidated, but were not:'); + for (const key of removed) { + console.log(` ${key}:`); + console.log(' before:', before.get(key)); + console.log(' cached:', cached.get(key)); + console.log(' after:', after.get(key)); + } + } + /* eslint-enable no-console */ + + assert.isTrue(added.size === 0 && removed.size === 0, 'invalidated different method results'); + } + + describe('from method calls', function() { + it('when staging files', async function() { + const workdir = await cloneRepository('multi-commits-files'); + const repository = new Repository(workdir); + await repository.getLoadPromise(); + + const changedFile = 'a.txt'; + const unchangedFile = 'b.txt'; + + await writeFile(path.join(workdir, changedFile), 'bar\nbaz\n'); + + await assertCorrectInvalidation({repository, changedFile, unchangedFile}, async () => { + await repository.stageFiles([changedFile]); + }); + }); + + it('when unstaging files'); + it('when staging files from a parent commit'); + it('when applying a patch to the index'); + it('when applying a patch to the working directory'); + it('when committing'); + it('when merging'); + it('when aborting a merge'); + it('when checking out a side'); + it('when writing a merge conflict to the index'); + it('when checking out a revision'); + it('when checking out paths'); + it('when fetching'); + it('when pulling'); + it('when pushing'); + it('when setting a config option'); + it('when discarding working directory changes'); + }); + + describe('from filesystem events', function() { + it('when staging files'); + it('when unstaging files'); + it('when staging files from a parent commit'); + it('when applying a patch to the index'); + it('when applying a patch to the working directory'); + it('when committing'); + it('when merging'); + it('when aborting a merge'); + it('when checking out a side'); + it('when writing a merge conflict to the index'); + it('when checking out a revision'); + it('when checking out paths'); + it('when fetching'); + it('when pulling'); + it('when pushing'); + it('when setting a config option'); + it('when discarding working directory changes'); + }); + }); }); From da930a25b2b2041b319735b05589851c972910c7 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 26 Apr 2017 10:21:28 -0400 Subject: [PATCH 13/55] Use non-arrow functions for helpers --- test/models/repository.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 416ef0ce84..80640f296e 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -943,7 +943,7 @@ describe('Repository', function() { }); describe('cache invalidation', function() { - const getCacheReaderMethods = options => { + function getCacheReaderMethods(options) { const repository = options.repository; const calls = new Map(); @@ -1000,7 +1000,7 @@ describe('Repository', function() { } return calls; - }; + } /** * Ensure that the correct cache keys are invalidated by a Repository operation. From 34f233b949a7bc399dde5cb3e317f890013537a8 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 26 Apr 2017 10:21:48 -0400 Subject: [PATCH 14/55] Capture errors from Repository options --- test/models/repository.test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 80640f296e..fb4655ef70 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -1012,7 +1012,10 @@ describe('Repository', function() { const results = new Map(); return Promise.all( Array.from(methods, ([name, call]) => { - return call().then(result => results.set(name, result)); + return call().then( + result => results.set(name, result), + err => results.set(name, err), + ); }), ).then(() => results); }; From 77230f933605d716bc0cba6b9b6963b836aaac3f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 26 Apr 2017 10:22:30 -0400 Subject: [PATCH 15/55] Allow expected options to not change when uncached --- test/models/repository.test.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index fb4655ef70..8455242dd0 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -1054,6 +1054,12 @@ describe('Repository', function() { const actual = changedKeys(before, cached, true); const {added, removed} = compareSets(expected, actual); + if (options.expected) { + for (const opName of options.expected) { + added.delete(opName); + } + } + /* eslint-disable no-console */ if (added.size > 0) { console.log('These cached method results were invalidated, but should not have been:'); From e3a4d82baf9c97ed1cdcf7482df0c012d9396bca Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 26 Apr 2017 10:23:40 -0400 Subject: [PATCH 16/55] Staging, unstaging, commit and merge operations --- test/models/repository.test.js | 133 +++++++++++++++++++++++++++++++-- 1 file changed, 125 insertions(+), 8 deletions(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 8455242dd0..f015215e07 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -1102,14 +1102,131 @@ describe('Repository', function() { }); }); - it('when unstaging files'); - it('when staging files from a parent commit'); - it('when applying a patch to the index'); - it('when applying a patch to the working directory'); - it('when committing'); - it('when merging'); - it('when aborting a merge'); - it('when checking out a side'); + it('when unstaging files', async function() { + const workdir = await cloneRepository('multi-commits-files'); + const repository = new Repository(workdir); + await repository.getLoadPromise(); + + const changedFile = 'a.txt'; + const unchangedFile = 'b.txt'; + + await writeFile(path.join(workdir, changedFile), 'bar\nbaz\n'); + await repository.stageFiles([changedFile]); + + await assertCorrectInvalidation({repository, changedFile, unchangedFile}, async () => { + await repository.unstageFiles([changedFile]); + }); + }); + + it('when staging files from a parent commit', async function() { + const workdir = await cloneRepository('multi-commits-files'); + const repository = new Repository(workdir); + await repository.getLoadPromise(); + + const changedFile = 'a.txt'; + const unchangedFile = 'b.txt'; + + await writeFile(path.join(workdir, changedFile), 'bar\nbaz\n'); + await repository.stageFiles([changedFile]); + + await assertCorrectInvalidation({repository, changedFile, unchangedFile}, async () => { + await repository.stageFilesFromParentCommit([changedFile]); + }); + }); + + it('when applying a patch to the index', async function() { + const workdir = await cloneRepository('multi-commits-files'); + const repository = new Repository(workdir); + await repository.getLoadPromise(); + + const changedFile = 'a.txt'; + const unchangedFile = 'b.txt'; + + await writeFile(path.join(workdir, changedFile), 'foo\nfoo-1\n'); + const patch = await repository.getFilePatchForPath(changedFile); + await writeFile(path.join(workdir, changedFile), 'foo\nfoo-1\nfoo-2\n'); + + await assertCorrectInvalidation({repository, changedFile, unchangedFile}, async () => { + await repository.applyPatchToIndex(patch); + }); + }); + + it('when applying a patch to the working directory', async function() { + const workdir = await cloneRepository('multi-commits-files'); + const repository = new Repository(workdir); + await repository.getLoadPromise(); + + const changedFile = 'a.txt'; + const unchangedFile = 'b.txt'; + + await writeFile(path.join(workdir, changedFile), 'foo\nfoo-1\n'); + const patch = (await repository.getFilePatchForPath(changedFile)).getUnstagePatch(); + + await assertCorrectInvalidation({repository, changedFile, unchangedFile}, async () => { + await repository.applyPatchToWorkdir(patch); + }); + }); + + it('when committing', async function() { + const workdir = await cloneRepository('multi-commits-files'); + const repository = new Repository(workdir); + await repository.getLoadPromise(); + + const changedFile = 'b.txt'; + await writeFile(path.join(workdir, changedFile), 'foo\nfoo-1\nfoo-2\n'); + await repository.stageFiles([changedFile]); + + await assertCorrectInvalidation({repository, changedFile}, async () => { + await repository.commit('message'); + }); + }); + + it('when merging', async function() { + const workdir = await cloneRepository('merge-conflict'); + const repository = new Repository(workdir); + await repository.getLoadPromise(); + + const changedFile = 'modified-on-both-ours.txt'; + + // Needs to be invalidated when the commit succeeds + const expected = ['getLastCommit']; + + await assertCorrectInvalidation({repository, changedFile, expected}, async () => { + await assert.isRejected(repository.merge('origin/branch')); + }); + }); + + it('when aborting a merge', async function() { + const workdir = await cloneRepository('merge-conflict'); + const repository = new Repository(workdir); + await repository.getLoadPromise(); + await assert.isRejected(repository.merge('origin/branch')); + + const changedFile = 'modified-on-both-ours.txt'; + + await assertCorrectInvalidation({repository, changedFile}, async () => { + await repository.abortMerge(); + }); + }); + + it('when checking out a side', async function() { + const workdir = await cloneRepository('merge-conflict'); + const repository = new Repository(workdir); + await repository.getLoadPromise(); + await assert.isRejected(repository.merge('origin/branch')); + + const changedFile = 'modified-on-both-ours.txt'; + const unchangedFile = 'modified-on-both-theirs.txt'; + + const expected = [ + 'readFileFromIndex changed', // This is actually an error. + ]; + + await assertCorrectInvalidation({repository, changedFile, unchangedFile, expected}, async () => { + await repository.checkoutSide('ours', [changedFile]); + }); + }); + it('when writing a merge conflict to the index'); it('when checking out a revision'); it('when checking out paths'); From 003a05ea5269a2e61c35517302cf9a5e9c22c93f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 26 Apr 2017 10:23:57 -0400 Subject: [PATCH 17/55] Tune invalidated cache keys --- lib/models/repository-states/present.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 23e741cdf5..15c90fc077 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -168,19 +168,13 @@ export default class Present extends State { return this.git().unstageFiles(paths, 'HEAD~'); } - @invalidate(filePatch => [ - ...Keys.cacheOperationKeys([filePatch.getOldPath()]), - ...Keys.cacheOperationKeys([filePatch.getNewPath()]), - ]) + @invalidate(filePatch => Keys.cacheOperationKeys([filePatch.getOldPath(), filePatch.getNewPath()])) applyPatchToIndex(filePatch) { const patchStr = filePatch.getHeaderString() + filePatch.toString(); return this.git().applyPatch(patchStr, {index: true}); } - @invalidate(filePatch => [ - ...Keys.cacheOperationKeys([filePatch.getOldPath()]), - ...Keys.cacheOperationKeys([filePatch.getNewPath()]), - ]) + @invalidate(filePatch => Keys.workdirOperationKeys([filePatch.getOldPath(), filePatch.getNewPath()])) applyPatchToWorkdir(filePatch) { const patchStr = filePatch.getHeaderString() + filePatch.toString(); return this.git().applyPatch(patchStr); @@ -213,7 +207,6 @@ export default class Present extends State { @invalidate((side, paths) => [ Keys.changedFiles, - Keys.stagedChangesSinceParentCommit, ...paths.map(Keys.isPartiallyStaged), ...Keys.allFilePatchOps(paths), ...paths.map(Keys.index), @@ -684,11 +677,15 @@ const Keys = { // Common collections of keys and patterns for use with @invalidate(). - cacheOperationKeys: fileNames => [ + workdirOperationKeys: fileNames => [ Keys.changedFiles, - Keys.stagedChangesSinceParentCommit, ...fileNames.map(Keys.isPartiallyStaged), ...Keys.allFilePatchOps(fileNames), + ], + + cacheOperationKeys: fileNames => [ + ...Keys.workdirOperationKeys(fileNames), + Keys.stagedChangesSinceParentCommit, ...fileNames.map(Keys.index), ], From 39946c09d97e97a927d0d6241c50b910d7cb4c1a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 26 Apr 2017 13:47:22 -0400 Subject: [PATCH 18/55] Use identity comparison of Promises to locate cache invalidations --- test/models/repository.test.js | 63 ++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index f015215e07..55c8fbbe3d 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -1010,48 +1010,51 @@ describe('Repository', function() { const record = () => { const results = new Map(); - return Promise.all( - Array.from(methods, ([name, call]) => { - return call().then( - result => results.set(name, result), - err => results.set(name, err), - ); - }), - ).then(() => results); + for (const [name, call] of methods) { + results.set(name, call()); + } + return results; }; - const changedKeys = (mapA, mapB, identity) => { - assert.sameMembers(Array.from(mapA.keys()), Array.from(mapB.keys())); + const invalidatedKeys = (mapA, mapB) => { + const allKeys = Array.from(mapA.keys()); + assert.sameMembers(allKeys, Array.from(mapB.keys())); - const compareValues = (valueA, valueB) => { - if (identity) { - return valueA === valueB; - } else { - return isEqual(valueA, valueB); - } - }; + return new Set( + allKeys.filter(key => mapA.get(key) !== mapB.get(key)), + ); + }; - const results = new Set(); - for (const key of mapA.keys()) { - const valueA = mapA.get(key); - const valueB = mapB.get(key); + const changedKeys = async (mapA, mapB) => { + const allKeys = Array.from(mapA.keys()); + assert.sameMembers(allKeys, Array.from(mapB.keys())); + + const syncResults = await Promise.all( + allKeys.map(async key => { + return { + key, + aSync: await mapA.get(key).catch(e => e), + bSync: await mapB.get(key).catch(e => e), + }; + }), + ); - if (!compareValues(valueA, valueB)) { - results.add(key); - } - } - return results; + return new Set( + syncResults + .filter(({aSync, bSync}) => !isEqual(aSync, bSync)) + .map(({key}) => key), + ); }; - const before = await record(); + const before = record(); await operation(); - const cached = await record(); + const cached = record(); options.repository.state.cache.clear(); const after = await record(); - const expected = changedKeys(before, after, false); - const actual = changedKeys(before, cached, true); + const expected = await changedKeys(before, after); + const actual = invalidatedKeys(before, cached); const {added, removed} = compareSets(expected, actual); if (options.expected) { From d3f094099989c8fc645887e76afad695d08f2f15 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 26 Apr 2017 15:11:47 -0400 Subject: [PATCH 19/55] Use cached status results for isPartiallyStaged --- lib/models/repository-states/present.js | 15 --------------- lib/models/repository-states/state.js | 5 ----- lib/models/repository.js | 10 +++++++++- 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 15c90fc077..417897b751 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -88,7 +88,6 @@ export default class Present extends State { if (endsWith('.git', 'index')) { keys.add(Keys.changedFiles); keys.add(Keys.stagedChangesSinceParentCommit); - keys.add(Keys.allPartiallyStaged); keys.add(Keys.allFilePatches); keys.add(Keys.allIndices); continue; @@ -122,7 +121,6 @@ export default class Present extends State { // File change within the working directory const relativePath = path.relative(this.workdir(), fullPath); keys.add(Keys.changedFiles); - keys.add(Keys.isPartiallyStaged(relativePath)); keys.add(Keys.filePatch(relativePath, {staged: false, amending: false})); } @@ -197,7 +195,6 @@ export default class Present extends State { @invalidate(() => [ Keys.changedFiles, Keys.stagedChangesSinceParentCommit, - Keys.allPartiallyStaged, Keys.allFilePatches, Keys.allIndices, ]) @@ -207,7 +204,6 @@ export default class Present extends State { @invalidate((side, paths) => [ Keys.changedFiles, - ...paths.map(Keys.isPartiallyStaged), ...Keys.allFilePatchOps(paths), ...paths.map(Keys.index), ]) @@ -385,12 +381,6 @@ export default class Present extends State { }); } - isPartiallyStaged(fileName) { - return this.cache.getOrSet(Keys.isPartiallyStaged(fileName), () => { - return this.git().isPartiallyStaged(fileName); - }); - } - getFilePatchForPath(filePath, {staged, amending} = {staged: false, amending: false}) { return this.cache.getOrSet(Keys.filePatch(filePath, {staged, amending}), async () => { const options = {staged, amending}; @@ -622,9 +612,6 @@ const Keys = { stagedChangesSinceParentCommit: 'staged-changes-since-parent-commit', - isPartiallyStaged: fileName => `is-partially-staged:${fileName}`, - allPartiallyStaged: '*is-partially-staged:', - filePatch: (fileName, {staged, amending}) => { let opKey = ''; if (staged && amending) { @@ -679,7 +666,6 @@ const Keys = { workdirOperationKeys: fileNames => [ Keys.changedFiles, - ...fileNames.map(Keys.isPartiallyStaged), ...Keys.allFilePatchOps(fileNames), ], @@ -692,7 +678,6 @@ const Keys = { headOperationKeys: () => [ Keys.changedFiles, Keys.stagedChangesSinceParentCommit, - Keys.allPartiallyStaged, Keys.allFilePatches, Keys.allIndices, Keys.lastCommit, diff --git a/lib/models/repository-states/state.js b/lib/models/repository-states/state.js index f4bb702de8..403e0a1c1a 100644 --- a/lib/models/repository-states/state.js +++ b/lib/models/repository-states/state.js @@ -304,11 +304,6 @@ export default class State { return Promise.resolve([]); } - @shouldDelegate - isPartiallyStaged(fileName) { - return Promise.resolve(false); - } - @shouldDelegate getFilePatchForPath(filePath, options = {}) { return Promise.resolve(null); diff --git a/lib/models/repository.js b/lib/models/repository.js index e3f8d340a7..ddd1dea762 100644 --- a/lib/models/repository.js +++ b/lib/models/repository.js @@ -164,6 +164,15 @@ export default class Repository { }); } + async isPartiallyStaged(fileName) { + const {unstagedFiles, stagedFiles} = await this.getStatusesForChangedFiles(); + const u = unstagedFiles[fileName]; + const s = stagedFiles[fileName]; + return (u === 'modified' && s === 'modified') || + (u === 'added' && s === 'modified') || + (u === 'modified' && s === 'deleted'); + } + async getRemoteForBranch(branchName) { const name = await this.getConfig(`branch.${branchName}.remote`); if (name === null) { @@ -244,7 +253,6 @@ const delegates = [ 'getStatusesForChangedFiles', 'getStagedChangesSinceParentCommit', - 'isPartiallyStaged', 'getFilePatchForPath', 'readFileFromIndex', From 9248fa6eea07aab7f6ac4895e65b09bc3224150d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 26 Apr 2017 15:12:25 -0400 Subject: [PATCH 20/55] :fire: obsolete verbose caching tests --- test/models/repository.test.js | 95 ---------------------------------- 1 file changed, 95 deletions(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 55c8fbbe3d..425e90f3ed 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -244,101 +244,6 @@ describe('Repository', function() { repo.refresh(); assert.deepEqual(await repo.getStagedChangesSinceParentCommit(), []); }); - - it('selectively invalidates the cache on stage', async function() { - const workingDirPath = await cloneRepository('three-files'); - const changedFileName = path.join('subdir-1', 'a.txt'); - const unchangedFileName = 'b.txt'; - fs.writeFileSync(path.join(workingDirPath, changedFileName), 'wat', 'utf8'); - const repo = new Repository(workingDirPath); - await repo.getLoadPromise(); - - primeCache(repo, - 'changed-files', - `is-partially-staged:${changedFileName}`, - `file-patch:u:${changedFileName}`, `file-patch:s:${changedFileName}`, - `index:${changedFileName}`, - `is-partially-staged:${unchangedFileName}`, - `file-patch:u:${unchangedFileName}`, `file-patch:s:${unchangedFileName}`, - `index:${unchangedFileName}`, - 'last-commit', 'branches', 'current-branch', 'remotes', 'ahead-count:master', 'behind-count:master', - ); - - await repo.stageFiles([changedFileName]); - - const uninvalidated = await Promise.all([ - repo.isPartiallyStaged(unchangedFileName), - repo.getFilePatchForPath(unchangedFileName), - repo.getFilePatchForPath(unchangedFileName, {staged: true}), - repo.readFileFromIndex(unchangedFileName), - repo.getLastCommit(), - repo.getBranches(), - repo.getCurrentBranch(), - repo.getRemotes(), - repo.getAheadCount('master'), - repo.getBehindCount('master'), - ]); - - assert.isTrue(uninvalidated.every(result => result === PRIMER)); - - const invalidated = await Promise.all([ - repo.getStatusesForChangedFiles(), - repo.isPartiallyStaged(changedFileName), - repo.getFilePatchForPath(changedFileName), - repo.getFilePatchForPath(changedFileName, {staged: true}), - repo.readFileFromIndex(changedFileName), - ]); - - assert.isTrue(invalidated.every(result => result !== PRIMER)); - }); - - it('selectively invalidates the cache on unstage', async function() { - const workingDirPath = await cloneRepository('three-files'); - const changedFileName = path.join('subdir-1', 'a.txt'); - const unchangedFileName = 'b.txt'; - fs.writeFileSync(path.join(workingDirPath, changedFileName), 'wat', 'utf8'); - const repo = new Repository(workingDirPath); - await repo.getLoadPromise(); - await repo.stageFiles([changedFileName]); - - primeCache(repo, - 'changed-files', - `is-partially-staged:${changedFileName}`, - `file-patch:u:${changedFileName}`, `file-patch:s:${changedFileName}`, - `index:${changedFileName}`, - `is-partially-staged:${unchangedFileName}`, - `file-patch:u:${unchangedFileName}`, `file-patch:s:${unchangedFileName}`, - `index:${unchangedFileName}`, - 'last-commit', 'branches', 'current-branch', 'remotes', 'ahead-count:master', 'behind-count:master', - ); - - await repo.unstageFiles([changedFileName]); - - const uninvalidated = await Promise.all([ - repo.isPartiallyStaged(unchangedFileName), - repo.getFilePatchForPath(unchangedFileName), - repo.getFilePatchForPath(unchangedFileName, {staged: true}), - repo.readFileFromIndex(unchangedFileName), - repo.getLastCommit(), - repo.getBranches(), - repo.getCurrentBranch(), - repo.getRemotes(), - repo.getAheadCount('master'), - repo.getBehindCount('master'), - ]); - - assert.isTrue(uninvalidated.every(result => result === PRIMER)); - - const invalidated = await Promise.all([ - repo.getStatusesForChangedFiles(), - repo.isPartiallyStaged(changedFileName), - repo.getFilePatchForPath(changedFileName), - repo.getFilePatchForPath(changedFileName, {staged: true}), - repo.readFileFromIndex(changedFileName), - ]); - - assert.isTrue(invalidated.every(result => result !== PRIMER)); - }); }); describe('getFilePatchForPath', function() { From 941591e68459a931707571a10f4e09a13e632ec7 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 26 Apr 2017 15:12:49 -0400 Subject: [PATCH 21/55] Don't check isPartiallyStaged for cache ops --- test/models/repository.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 425e90f3ed..96aceec064 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -860,7 +860,6 @@ describe('Repository', function() { calls.set('getRemotes', () => repository.getRemotes()); const withFile = (fileName, description) => { - calls.set(`isPartiallyStaged ${description}`, () => repository.isPartiallyStaged(fileName)); calls.set( `getFilePatchForPath {unstaged} ${description}`, () => repository.getFilePatchForPath(fileName, {staged: false}), From dc833f61dc6404f399ae4dd71d0016bb199d8e3d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 26 Apr 2017 16:08:19 -0400 Subject: [PATCH 22/55] Accept a "files" array --- test/models/repository.test.js | 35 ++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 96aceec064..13d3c5a7c5 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -859,27 +859,24 @@ describe('Repository', function() { calls.set('getCurrentBranch', () => repository.getCurrentBranch()); calls.set('getRemotes', () => repository.getRemotes()); - const withFile = (fileName, description) => { + const withFile = fileName => { calls.set( - `getFilePatchForPath {unstaged} ${description}`, + `getFilePatchForPath {unstaged} ${fileName}`, () => repository.getFilePatchForPath(fileName, {staged: false}), ); calls.set( - `getFilePatchForPath {staged} ${description}`, + `getFilePatchForPath {staged} ${fileName}`, () => repository.getFilePatchForPath(fileName, {staged: true}), ); calls.set( - `getFilePatchForPath {staged, amending} ${description}`, + `getFilePatchForPath {staged, amending} ${fileName}`, () => repository.getFilePatchForPath(fileName, {staged: true, amending: true}), ); - calls.set(`readFileFromIndex ${description}`, () => repository.readFileFromIndex(fileName)); + calls.set(`readFileFromIndex ${fileName}`, () => repository.readFileFromIndex(fileName)); }; - if (options.changedFile) { - withFile(options.changedFile, 'changed'); - } - if (options.unchangedFile) { - withFile(options.unchangedFile, 'unchanged'); + for (const fileName of (options.files || [])) { + withFile(fileName); } const withBranch = (branchName, description) => { @@ -1004,7 +1001,8 @@ describe('Repository', function() { await writeFile(path.join(workdir, changedFile), 'bar\nbaz\n'); - await assertCorrectInvalidation({repository, changedFile, unchangedFile}, async () => { + const files = [changedFile, unchangedFile]; + await assertCorrectInvalidation({repository, files}, async () => { await repository.stageFiles([changedFile]); }); }); @@ -1020,7 +1018,8 @@ describe('Repository', function() { await writeFile(path.join(workdir, changedFile), 'bar\nbaz\n'); await repository.stageFiles([changedFile]); - await assertCorrectInvalidation({repository, changedFile, unchangedFile}, async () => { + const files = [changedFile, unchangedFile]; + await assertCorrectInvalidation({repository, files}, async () => { await repository.unstageFiles([changedFile]); }); }); @@ -1036,7 +1035,8 @@ describe('Repository', function() { await writeFile(path.join(workdir, changedFile), 'bar\nbaz\n'); await repository.stageFiles([changedFile]); - await assertCorrectInvalidation({repository, changedFile, unchangedFile}, async () => { + const files = [changedFile, unchangedFile]; + await assertCorrectInvalidation({repository, files}, async () => { await repository.stageFilesFromParentCommit([changedFile]); }); }); @@ -1053,7 +1053,8 @@ describe('Repository', function() { const patch = await repository.getFilePatchForPath(changedFile); await writeFile(path.join(workdir, changedFile), 'foo\nfoo-1\nfoo-2\n'); - await assertCorrectInvalidation({repository, changedFile, unchangedFile}, async () => { + const files = [changedFile, unchangedFile]; + await assertCorrectInvalidation({repository, files}, async () => { await repository.applyPatchToIndex(patch); }); }); @@ -1069,7 +1070,8 @@ describe('Repository', function() { await writeFile(path.join(workdir, changedFile), 'foo\nfoo-1\n'); const patch = (await repository.getFilePatchForPath(changedFile)).getUnstagePatch(); - await assertCorrectInvalidation({repository, changedFile, unchangedFile}, async () => { + const files = [changedFile, unchangedFile]; + await assertCorrectInvalidation({repository, files}, async () => { await repository.applyPatchToWorkdir(patch); }); }); @@ -1083,7 +1085,8 @@ describe('Repository', function() { await writeFile(path.join(workdir, changedFile), 'foo\nfoo-1\nfoo-2\n'); await repository.stageFiles([changedFile]); - await assertCorrectInvalidation({repository, changedFile}, async () => { + const files = [changedFile]; + await assertCorrectInvalidation({repository, files}, async () => { await repository.commit('message'); }); }); From d77e2fa44645c9fd2678d1723c0e04283a05ae57 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 26 Apr 2017 16:08:32 -0400 Subject: [PATCH 23/55] Allow skipped operations --- test/models/repository.test.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 13d3c5a7c5..93bd3d7f43 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -908,6 +908,9 @@ describe('Repository', function() { */ async function assertCorrectInvalidation(options, operation) { const methods = getCacheReaderMethods(options); + for (const opName of (options.skip || [])) { + methods.delete(opName); + } const record = () => { const results = new Map(); From 213e169089a3008a80fe15eaf00ce96f00abc414 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 26 Apr 2017 16:09:18 -0400 Subject: [PATCH 24/55] Back to green --- lib/models/repository-states/present.js | 45 +++++++++++-------- test/models/repository.test.js | 60 ++++++++++++++++--------- 2 files changed, 64 insertions(+), 41 deletions(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 417897b751..1e900486be 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -180,14 +180,20 @@ export default class Present extends State { // Committing - @invalidate(() => Keys.headOperationKeys()) + @invalidate(() => [ + ...Keys.headOperationKeys(), + ...Keys.filePatchOps({staged: true}), + ]) commit(message, options) { return this.git().commit(formatCommitMessage(message), options); } // Merging - @invalidate(() => Keys.headOperationKeys()) + @invalidate(() => [ + ...Keys.headOperationKeys(), + Keys.allIndices, + ]) merge(branchName) { return this.git().merge(branchName); } @@ -202,11 +208,6 @@ export default class Present extends State { return this.git().abortMerge(); } - @invalidate((side, paths) => [ - Keys.changedFiles, - ...Keys.allFilePatchOps(paths), - ...paths.map(Keys.index), - ]) checkoutSide(side, paths) { return this.git().checkoutSide(side, paths); } @@ -227,7 +228,14 @@ export default class Present extends State { // Checkout - @invalidate(() => Keys.headOperationKeys) + @invalidate(() => [ + Keys.stagedChangesSinceParentCommit, + Keys.lastCommit, + Keys.allIndices, + Keys.allAheadCounts, + Keys.allBehindCounts, + Keys.currentBranch, + ]) checkout(revision, options = {}) { return this.git().checkout(revision, options); } @@ -235,7 +243,8 @@ export default class Present extends State { @invalidate(paths => [ Keys.changedFiles, Keys.stagedChangesSinceParentCommit, - ...Keys.allFilePatchOps(paths), + ...paths.map(Keys.index), + ...Keys.allFilePatchOps(paths, [{staged: true}, {staged: true, amending: true}]), ]) checkoutPathsAtRevision(paths, revision = 'HEAD') { return this.git().checkoutFiles(paths, revision); @@ -343,7 +352,6 @@ export default class Present extends State { @invalidate(paths => [ Keys.changedFiles, Keys.stagedChangesSinceParentCommit, - ...paths.map(Keys.isPartiallyStaged), ...paths.map(filePath => Keys.filePatch(filePath, {staged: false})), ]) async discardWorkDirChangesForPaths(paths) { @@ -624,17 +632,16 @@ const Keys = { return `file-patch:${opKey}:${fileName}`; }, - allFilePatchOps: fileNames => { + allFilePatchOps: (fileNames, ops = [{staged: false}, {staged: true}, {staged: true, amending: true}]) => { const keys = []; for (let i = 0; i < fileNames.length; i++) { - keys.push( - Keys.filePatch(fileNames[i], {staged: false}), - Keys.filePatch(fileNames[i], {staged: true}), - Keys.filePatch(fileNames[i], {staged: true, amending: true}), - ); + for (let j = 0; j < ops.length; j++) { + keys.push(Keys.filePatch(fileNames[i], ops[j])); + } } return keys; }, + filePatchOps: (...ops) => ops.map(op => '*' + Keys.filePatch('', op)), allFilePatches: '*file-patch:', index: fileName => `index:${fileName}`, @@ -666,11 +673,12 @@ const Keys = { workdirOperationKeys: fileNames => [ Keys.changedFiles, - ...Keys.allFilePatchOps(fileNames), + ...Keys.allFilePatchOps(fileNames, [{staged: false}]), ], cacheOperationKeys: fileNames => [ ...Keys.workdirOperationKeys(fileNames), + ...Keys.allFilePatchOps(fileNames, [{staged: true}, {staged: true, amending: true}]), Keys.stagedChangesSinceParentCommit, ...fileNames.map(Keys.index), ], @@ -678,8 +686,7 @@ const Keys = { headOperationKeys: () => [ Keys.changedFiles, Keys.stagedChangesSinceParentCommit, - Keys.allFilePatches, - Keys.allIndices, + ...Keys.filePatchOps({staged: true, amending: true}), Keys.lastCommit, Keys.allAheadCounts, Keys.allBehindCounts, diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 93bd3d7f43..40f4164c9d 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -1002,7 +1002,7 @@ describe('Repository', function() { const changedFile = 'a.txt'; const unchangedFile = 'b.txt'; - await writeFile(path.join(workdir, changedFile), 'bar\nbaz\n'); + await writeFile(path.join(workdir, changedFile), 'bar\nbar-1\n'); const files = [changedFile, unchangedFile]; await assertCorrectInvalidation({repository, files}, async () => { @@ -1099,12 +1099,14 @@ describe('Repository', function() { const repository = new Repository(workdir); await repository.getLoadPromise(); - const changedFile = 'modified-on-both-ours.txt'; - - // Needs to be invalidated when the commit succeeds - const expected = ['getLastCommit']; - - await assertCorrectInvalidation({repository, changedFile, expected}, async () => { + const skip = [ + 'readFileFromIndex modified-on-both-ours.txt', + ]; + const expected = [ + 'getLastCommit', // Needs to be invalidated when the commit succeeds + ]; + const files = ['modified-on-both-ours.txt']; + await assertCorrectInvalidation({repository, files, expected, skip}, async () => { await assert.isRejected(repository.merge('origin/branch')); }); }); @@ -1115,34 +1117,48 @@ describe('Repository', function() { await repository.getLoadPromise(); await assert.isRejected(repository.merge('origin/branch')); - const changedFile = 'modified-on-both-ours.txt'; + const stagedFile = 'modified-on-both-ours.txt'; + await repository.stageFiles([stagedFile]); - await assertCorrectInvalidation({repository, changedFile}, async () => { + const expected = [ + 'getFilePatchForPath {unstaged} modified-on-both-ours.txt', + ]; + const files = [stagedFile]; + await assertCorrectInvalidation({repository, files, expected}, async () => { await repository.abortMerge(); }); }); - it('when checking out a side', async function() { - const workdir = await cloneRepository('merge-conflict'); + it('when writing a merge conflict to the index'); + + it('when checking out a revision', async function() { + const workdir = await cloneRepository('multi-commits-files'); const repository = new Repository(workdir); await repository.getLoadPromise(); - await assert.isRejected(repository.merge('origin/branch')); - - const changedFile = 'modified-on-both-ours.txt'; - const unchangedFile = 'modified-on-both-theirs.txt'; - const expected = [ - 'readFileFromIndex changed', // This is actually an error. + const skip = [ + 'getFilePatchForPath {staged, amending} b.txt', ]; + const files = ['b.txt']; + await assertCorrectInvalidation({repository, files, skip}, async () => { + await repository.checkout('HEAD^'); + }); + }); + + it('when checking out paths', async function() { + const workdir = await cloneRepository('multi-commits-files'); + const repository = new Repository(workdir); + await repository.getLoadPromise(); - await assertCorrectInvalidation({repository, changedFile, unchangedFile, expected}, async () => { - await repository.checkoutSide('ours', [changedFile]); + const changedFile = 'b.txt'; + const unchangedFile = 'c.txt'; + + const files = [changedFile, unchangedFile]; + await assertCorrectInvalidation({repository, files}, async () => { + await repository.checkoutPathsAtRevision([changedFile], 'HEAD^'); }); }); - it('when writing a merge conflict to the index'); - it('when checking out a revision'); - it('when checking out paths'); it('when fetching'); it('when pulling'); it('when pushing'); From 2c326666091ddaacce1bcdf2398d842d14b15501 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 26 Apr 2017 16:37:19 -0400 Subject: [PATCH 25/55] Fetching, pushing, pulling --- lib/models/repository-states/present.js | 5 ++- test/models/repository.test.js | 50 +++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 1e900486be..657f31407e 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -264,7 +264,10 @@ export default class Present extends State { await this.git().fetch(remote.getName(), branchName); } - @invalidate(() => Keys.headOperationKeys) + @invalidate(() => [ + ...Keys.headOperationKeys(), + Keys.allIndices, + ]) async pull(branchName) { const remote = await this.getRemoteForBranch(branchName); if (!remote.isPresent()) { diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 40f4164c9d..2d721edebf 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -1159,9 +1159,53 @@ describe('Repository', function() { }); }); - it('when fetching'); - it('when pulling'); - it('when pushing'); + it('when fetching', async function() { + const {localRepoPath} = await setUpLocalAndRemoteRepositories(); + const repository = new Repository(localRepoPath); + await repository.getLoadPromise(); + + await repository.commit('wat', {allowEmpty: true}); + await repository.commit('huh', {allowEmpty: true}); + + await assertCorrectInvalidation({repository}, async () => { + await repository.fetch('master'); + }); + }); + + it('when pulling', async function() { + const {localRepoPath} = await setUpLocalAndRemoteRepositories({remoteAhead: true}); + const repository = new Repository(localRepoPath); + await repository.getLoadPromise(); + + await writeFile(path.join(localRepoPath, 'new-file.txt'), 'one\n'); + await repository.stageFiles(['new-file.txt']); + await repository.commit('wat'); + + const expected = [ + 'getStatusesForChangedFiles', + 'readFileFromIndex new-file.txt', + ]; + const files = ['new-file.txt', 'file.txt']; + await assertCorrectInvalidation({repository, files, expected}, async () => { + await repository.pull('master'); + }); + }); + + it('when pushing', async function() { + const {localRepoPath} = await setUpLocalAndRemoteRepositories(); + const repository = new Repository(localRepoPath); + await repository.getLoadPromise(); + + await writeFile(path.join(localRepoPath, 'new-file.txt'), 'one\n'); + await repository.stageFiles(['new-file.txt']); + await repository.commit('wat'); + + const files = ['new-file.txt', 'file.txt']; + await assertCorrectInvalidation({repository, files}, async () => { + await repository.push('master'); + }); + }); + it('when setting a config option'); it('when discarding working directory changes'); }); From b9412440763d1a37745b5998034311c3c708f1b5 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 26 Apr 2017 22:03:31 -0400 Subject: [PATCH 26/55] Invalidate specific config options --- lib/models/repository-states/present.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 657f31407e..c46aba23fc 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -295,9 +295,7 @@ export default class Present extends State { // Configuration - @invalidate(() => [ - Keys.allConfigs, - ]) + @invalidate(option => Keys.allConfigOpts(option)) setConfig(option, value, options) { return this.git().setConfig(option, value, options); } @@ -668,6 +666,7 @@ const Keys = { const opKey = local ? 'l' : ''; return `config:${opKey}:${option}`; }, + allConfigOpts: option => [Keys.config(option, {local: true}), Keys.config(option, {local: false})], allConfigs: '*config:', blob: sha => `blob:${sha}`, From 27d973f178d1bedf0331683d85ae11946c916303 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 26 Apr 2017 22:03:53 -0400 Subject: [PATCH 27/55] stagedChangesSinceParentCommit can't be changed by a workdir op --- lib/models/repository-states/present.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index c46aba23fc..4edd90893b 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -352,7 +352,6 @@ export default class Present extends State { @invalidate(paths => [ Keys.changedFiles, - Keys.stagedChangesSinceParentCommit, ...paths.map(filePath => Keys.filePatch(filePath, {staged: false})), ]) async discardWorkDirChangesForPaths(paths) { From f74467b4ac37d6dcf8d5444c00ffc165a440b80e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 26 Apr 2017 22:04:14 -0400 Subject: [PATCH 28/55] Accept an array of config options to test --- test/models/repository.test.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 2d721edebf..7e8be33c97 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -891,13 +891,9 @@ describe('Repository', function() { withBranch(options.unchangedBranch); } - if (options.changedConfig) { - calls.set('getConfig changed', () => repository.getConfig(options.changedConfig)); - calls.set('getConfig {local} changed', () => repository.getConfig(options.changedConfig, {local: true})); - } - if (options.unchangedConfig) { - calls.set('getConfig unchanged', () => repository.getConfig(options.unchangedConfig)); - calls.set('getConfig {local} unchanged', () => repository.getConfig(options.unchangedConfig, {local: true})); + for (const optionName of (options.optionNames || [])) { + calls.set(`getConfig ${optionName}`, () => repository.getConfig(optionName)); + calls.set(`getConfig {local} ${optionName}`, () => repository.getConfig(optionName, {local: true})); } return calls; From d2fd249699bd51724c5baaba51d146c52014b245 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 26 Apr 2017 22:04:37 -0400 Subject: [PATCH 29/55] Final two operations :tada: --- test/models/repository.test.js | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 7e8be33c97..2546f50cd4 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -1202,8 +1202,32 @@ describe('Repository', function() { }); }); - it('when setting a config option'); - it('when discarding working directory changes'); + it('when setting a config option', async function() { + const workdir = await cloneRepository('three-files'); + const repository = new Repository(workdir); + await repository.getLoadPromise(); + + const optionNames = ['core.editor', 'color.ui']; + await assertCorrectInvalidation({repository, optionNames}, async () => { + await repository.setConfig('core.editor', 'atom --wait #obvs'); + }); + }); + + it('when discarding working directory changes', async function() { + const workdir = await cloneRepository('multi-commits-files'); + const repository = new Repository(workdir); + await repository.getLoadPromise(); + + await Promise.all([ + writeFile(path.join(workdir, 'a.txt'), 'aaa\n'), + writeFile(path.join(workdir, 'c.txt'), 'baz\n'), + ]); + + const files = ['a.txt', 'b.txt', 'c.txt']; + await assertCorrectInvalidation({repository, files}, async () => { + await repository.discardWorkDirChangesForPaths(['a.txt', 'c.txt']); + }); + }); }); describe('from filesystem events', function() { From 501f5873609c5c301e3fab5611a4ced70a79443e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 27 Apr 2017 08:25:53 -0400 Subject: [PATCH 30/55] :fire: unused code --- test/models/repository.test.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 2546f50cd4..4a19a60f53 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -15,15 +15,7 @@ import { } from '../helpers'; import {writeFile} from '../../lib/helpers'; -const PRIMER = Symbol('cachePrimer'); - describe('Repository', function() { - function primeCache(repo, ...keys) { - for (const key of keys) { - repo.state.cache.getOrSet(key, () => Promise.resolve(PRIMER)); - } - } - it('delegates all state methods', function() { const missing = expectedDelegates.filter(delegateName => { return Repository.prototype[delegateName] === undefined; From 9a0a102913b2778724d72f18ce68819d0957642a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 27 Apr 2017 09:21:58 -0400 Subject: [PATCH 31/55] The first filesystem event test --- test/models/repository.test.js | 62 +++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 4a19a60f53..2980dce171 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -5,9 +5,11 @@ import temp from 'temp'; import util from 'util'; import compareSets from 'compare-sets'; import isEqual from 'lodash.isequal'; +import {CompositeDisposable, Disposable} from 'event-kit'; import Repository from '../../lib/models/repository'; import {expectedDelegates} from '../../lib/models/repository-states'; +import FileSystemChangeObserver from '../../lib/models/file-system-change-observer'; import { cloneRepository, setUpLocalAndRemoteRepositories, getHeadCommitOnRemote, @@ -1223,7 +1225,65 @@ describe('Repository', function() { }); describe('from filesystem events', function() { - it('when staging files'); + let workdir, sub; + let observedEvents, eventCallback; + + async function wireUpObserver(fixtureName = 'multi-commits-files') { + observedEvents = []; + eventCallback = () => {}; + + workdir = await cloneRepository(fixtureName); + const repository = new Repository(workdir); + await repository.getLoadPromise(); + + const observer = new FileSystemChangeObserver(repository); + + sub = new CompositeDisposable( + new Disposable(async () => { + await observer.destroy(); + repository.destroy(); + }), + ); + + sub.add(observer.onDidChange(events => { + console.log(require('util').inspect(events)); + + repository.observeFilesystemChange(events); + observedEvents.push(...events); + eventCallback(); + })); + + return {repository, observer}; + } + + function expectEvents(...fileNames) { + return new Promise((resolve, reject) => { + eventCallback = () => { + const observedFileNames = new Set(observedEvents.map(event => event.file || event.newFile)); + if (fileNames.every(fileName => observedFileNames.has(fileName))) { + resolve(); + } + }; + }); + } + + afterEach(function() { + sub && sub.dispose(); + }); + + it('when staging files', async function() { + const {repository, observer} = await wireUpObserver(); + + await writeFile(path.join(workdir, 'a.txt'), 'boop\n'); + + const files = ['a.txt']; + await observer.start(); + await assertCorrectInvalidation({repository, files, verbose: true}, async () => { + await repository.git.stageFiles(['a.txt']); + await expectEvents('index'); + }); + }); + it('when unstaging files'); it('when staging files from a parent commit'); it('when applying a patch to the index'); From a938b5677e387c273c808225c2b192af101d2c16 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 27 Apr 2017 09:22:43 -0400 Subject: [PATCH 32/55] Allow test cases to invalidate *more* than necessary without failing --- test/models/repository.test.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 2980dce171..ed32c28235 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -958,7 +958,7 @@ describe('Repository', function() { } /* eslint-disable no-console */ - if (added.size > 0) { + if (added.size > 0 && (options.strict || options.verbose)) { console.log('These cached method results were invalidated, but should not have been:'); for (const key of added) { @@ -980,7 +980,11 @@ describe('Repository', function() { } /* eslint-enable no-console */ - assert.isTrue(added.size === 0 && removed.size === 0, 'invalidated different method results'); + if (options.strict) { + assert.isTrue(added.size === 0 && removed.size === 0, 'invalidated different method results'); + } else { + assert.isTrue(removed.size === 0, 'bzzzt, overzealous invalidation detected'); + } } describe('from method calls', function() { From 20e83d35641982f0e4bcae5081a01b984281e6e6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 27 Apr 2017 11:29:03 -0400 Subject: [PATCH 33/55] Correct a race condition in getCurrentBranch() --- lib/git-shell-out-strategy.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/git-shell-out-strategy.js b/lib/git-shell-out-strategy.js index fb847ea135..98daf5ace6 100644 --- a/lib/git-shell-out-strategy.js +++ b/lib/git-shell-out-strategy.js @@ -502,14 +502,17 @@ export default class GitShellOutStrategy { * Branches */ async getCurrentBranch() { - const content = await readFile(path.join(this.workingDir, '.git', 'HEAD')); - if (content.startsWith('ref: ')) { + try { // The common case: you're on a branch return { name: (await this.exec(['symbolic-ref', '--short', 'HEAD'])).trim(), isDetached: false, }; - } else { + } catch (e) { + if (!/not a symbolic ref/.test(e.stdErr)) { + throw e; + } + // Detached HEAD return { name: (await this.exec(['describe', '--contains', '--all', 'HEAD'])).trim(), From a9ff5c464f79e71d8f8d1c6a6dde3546ded2fac1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 27 Apr 2017 11:29:24 -0400 Subject: [PATCH 34/55] Invalidate lastCommit on refs/heads events --- lib/models/repository-states/present.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 4edd90893b..ac04ba58b6 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -103,6 +103,7 @@ export default class Present extends State { if (includes('.git', 'refs', 'heads')) { keys.add(Keys.branches); + keys.add(Keys.lastCommit); continue; } From 41ff7515b7cc8ffca8e8b80bfc49ef1d554daf38 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 27 Apr 2017 11:29:38 -0400 Subject: [PATCH 35/55] Oops, got the error message backwards --- test/models/repository.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index ed32c28235..f7a640d6e4 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -983,7 +983,7 @@ describe('Repository', function() { if (options.strict) { assert.isTrue(added.size === 0 && removed.size === 0, 'invalidated different method results'); } else { - assert.isTrue(removed.size === 0, 'bzzzt, overzealous invalidation detected'); + assert.isTrue(removed.size === 0, 'bzzzt, inadequate cache busting detected'); } } From 8da28cced00f346af5a2dc1789fc57044eab4918 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 27 Apr 2017 11:29:57 -0400 Subject: [PATCH 36/55] Support an existing workdir in wireUpObserver() --- test/models/repository.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index f7a640d6e4..cab4b93f59 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -1232,11 +1232,11 @@ describe('Repository', function() { let workdir, sub; let observedEvents, eventCallback; - async function wireUpObserver(fixtureName = 'multi-commits-files') { + async function wireUpObserver(fixtureName = 'multi-commits-files', existingWorkdir = null) { observedEvents = []; eventCallback = () => {}; - workdir = await cloneRepository(fixtureName); + workdir = existingWorkdir || await cloneRepository(fixtureName); const repository = new Repository(workdir); await repository.getLoadPromise(); From 514115b0bb9786d066e3ea05b1888c70f4a31672 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 27 Apr 2017 11:30:22 -0400 Subject: [PATCH 37/55] :fire: {verbose: true} argument --- test/models/repository.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index cab4b93f59..68ffd632ce 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -1282,7 +1282,7 @@ describe('Repository', function() { const files = ['a.txt']; await observer.start(); - await assertCorrectInvalidation({repository, files, verbose: true}, async () => { + await assertCorrectInvalidation({repository, files}, async () => { await repository.git.stageFiles(['a.txt']); await expectEvents('index'); }); From 0da1e9a78567703cbacd512889f728645cd67257 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 27 Apr 2017 11:30:46 -0400 Subject: [PATCH 38/55] Filesystem event tests --- test/models/repository.test.js | 208 ++++++++++++++++++++++++++++++--- 1 file changed, 193 insertions(+), 15 deletions(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 68ffd632ce..9c0f019273 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -1288,22 +1288,200 @@ describe('Repository', function() { }); }); - it('when unstaging files'); - it('when staging files from a parent commit'); - it('when applying a patch to the index'); - it('when applying a patch to the working directory'); - it('when committing'); - it('when merging'); - it('when aborting a merge'); - it('when checking out a side'); + it('when unstaging files', async function() { + const {repository, observer} = await wireUpObserver(); + + await writeFile(path.join(workdir, 'a.txt'), 'boop\n'); + await repository.git.stageFiles(['a.txt']); + + const files = ['a.txt']; + await observer.start(); + await assertCorrectInvalidation({repository, files}, async () => { + await repository.git.unstageFiles(['a.txt']); + await expectEvents('index'); + }); + }); + + it('when staging files from a parent commit', async function() { + const {repository, observer} = await wireUpObserver(); + + const files = ['a.txt']; + await observer.start(); + await assertCorrectInvalidation({repository, files}, async () => { + await repository.git.unstageFiles(['a.txt'], 'HEAD~'); + await expectEvents('index'); + }); + }); + + it('when applying a patch to the index', async function() { + const {repository, observer} = await wireUpObserver(); + + await writeFile(path.join(workdir, 'a.txt'), 'boop\n'); + const patch = await repository.getFilePatchForPath('a.txt'); + + const files = ['a.txt']; + await observer.start(); + await assertCorrectInvalidation({repository, files}, async () => { + await repository.git.applyPatch(patch.getHeaderString() + patch.toString(), {index: true}); + await expectEvents('index'); + }); + }); + + it('when applying a patch to the working directory', async function() { + const {repository, observer} = await wireUpObserver(); + + await writeFile(path.join(workdir, 'a.txt'), 'boop\n'); + const patch = (await repository.getFilePatchForPath('a.txt')).getUnstagePatch(); + + const files = ['a.txt']; + await observer.start(); + await assertCorrectInvalidation({repository, files}, async () => { + await repository.git.applyPatch(patch.getHeaderString() + patch.toString()); + await expectEvents('a.txt'); + }); + }); + + it('when committing', async function() { + const {repository, observer} = await wireUpObserver(); + + await writeFile(path.join(workdir, 'a.txt'), 'boop\n'); + await repository.stageFiles(['a.txt']); + + const files = ['a.txt']; + await observer.start(); + await assertCorrectInvalidation({repository, files}, async () => { + await repository.git.commit('boop your snoot'); + await expectEvents('index', 'master'); + }); + }); + + it('when merging', async function() { + const {repository, observer} = await wireUpObserver('merge-conflict'); + + const skip = [ + 'readFileFromIndex modified-on-both-ours.txt', + ]; + const files = ['modified-on-both-ours.txt']; + await observer.start(); + await assertCorrectInvalidation({repository, files, skip}, async () => { + await assert.isRejected(repository.git.merge('origin/branch')); + await expectEvents('index', 'modified-on-both-ours.txt', 'MERGE_HEAD'); + }); + }); + + it('when aborting a merge', async function() { + const {repository, observer} = await wireUpObserver('merge-conflict'); + await assert.isRejected(repository.merge('origin/branch')); + + const skip = [ + 'readFileFromIndex modified-on-both-ours.txt', + ]; + const files = ['modified-on-both-ours.txt']; + await observer.start(); + await assertCorrectInvalidation({repository, files, skip}, async () => { + await repository.git.abortMerge(); + await expectEvents('index', 'modified-on-both-ours.txt', 'MERGE_HEAD', 'HEAD'); + }); + }); + it('when writing a merge conflict to the index'); - it('when checking out a revision'); - it('when checking out paths'); - it('when fetching'); - it('when pulling'); - it('when pushing'); - it('when setting a config option'); - it('when discarding working directory changes'); + + it('when checking out a revision', async function() { + const {repository, observer} = await wireUpObserver(); + + const skip = [ + 'getFilePatchForPath {staged, amending} b.txt', + ]; + const files = ['b.txt']; + await observer.start(); + await assertCorrectInvalidation({repository, files, skip}, async () => { + await repository.git.checkout('HEAD^'); + await expectEvents('index', 'HEAD', 'b.txt', 'c.txt'); + }); + }); + + it('when checking out paths', async function() { + const {repository, observer} = await wireUpObserver(); + + const files = ['b.txt', 'c.txt']; + + await observer.start(); + await assertCorrectInvalidation({repository, files}, async () => { + await repository.git.checkoutFiles(['b.txt'], 'HEAD^'); + await expectEvents('b.txt', 'index'); + }); + }); + + it('when fetching', async function() { + const {localRepoPath} = await setUpLocalAndRemoteRepositories({remoteAhead: true}); + const {repository, observer} = await wireUpObserver(null, localRepoPath); + + await repository.commit('wat', {allowEmpty: true}); + await repository.commit('huh', {allowEmpty: true}); + + await observer.start(); + await assertCorrectInvalidation({repository}, async () => { + await repository.git.fetch('origin', 'master'); + await expectEvents('master'); + }); + }); + + it('when pulling', async function() { + const {localRepoPath} = await setUpLocalAndRemoteRepositories({remoteAhead: true}); + const {repository, observer} = await wireUpObserver(null, localRepoPath); + + await writeFile(path.join(localRepoPath, 'file.txt'), 'one\n'); + await repository.stageFiles(['file.txt']); + await repository.commit('wat'); + + const skip = [ + 'readFileFromIndex file.txt', + ]; + const files = ['file.txt']; + await observer.start(); + await assertCorrectInvalidation({repository, files, skip}, async () => { + await assert.isRejected(repository.git.pull('origin', 'master')); + await expectEvents('file.txt', 'master', 'MERGE_HEAD', 'index'); + }); + }); + + it('when pushing', async function() { + const {localRepoPath} = await setUpLocalAndRemoteRepositories(); + const {repository, observer} = await wireUpObserver(null, localRepoPath); + + await writeFile(path.join(localRepoPath, 'new-file.txt'), 'one\n'); + await repository.stageFiles(['new-file.txt']); + await repository.commit('wat'); + + const files = ['new-file.txt', 'file.txt']; + await observer.start(); + await assertCorrectInvalidation({repository, files}, async () => { + await repository.git.push('origin', 'master'); + await expectEvents('master'); + }); + }); + + it('when setting a config option', async function() { + const {repository, observer} = await wireUpObserver(); + + const optionNames = ['core.editor', 'color.ui']; + await observer.start(); + await assertCorrectInvalidation({repository, optionNames}, async () => { + await repository.git.setConfig('core.editor', 'ed # :trollface:'); + await expectEvents('config'); + }); + }); + + it('when changing files in the working directory', async function() { + const {repository, observer} = await wireUpObserver(); + + await observer.start(); + const files = ['b.txt']; + await assertCorrectInvalidation({repository, files}, async () => { + await writeFile(path.join(workdir, 'b.txt'), 'new contents\n'); + expectEvents('b.txt'); + }); + }); }); }); }); From ba656b8d0bc48f642d27b1c013d31b2ec1ab7ec3 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 27 Apr 2017 11:34:46 -0400 Subject: [PATCH 39/55] :fire: console.log --- test/models/repository.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 9c0f019273..5c2aafe767 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -1250,8 +1250,6 @@ describe('Repository', function() { ); sub.add(observer.onDidChange(events => { - console.log(require('util').inspect(events)); - repository.observeFilesystemChange(events); observedEvents.push(...events); eventCallback(); From 3b8b2a5941d283458cd57fd0ee4c396941d77b8d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 27 Apr 2017 11:38:12 -0400 Subject: [PATCH 40/55] :shirt: no shadowing variables --- lib/models/workspace-change-observer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/models/workspace-change-observer.js b/lib/models/workspace-change-observer.js index f94265d2c8..f16f214eab 100644 --- a/lib/models/workspace-change-observer.js +++ b/lib/models/workspace-change-observer.js @@ -111,10 +111,10 @@ export default class WorkspaceChangeObserver { } } - activeRepositoryContainsPath(path) { + activeRepositoryContainsPath(filePath) { const repository = this.getRepository(); if (repository) { - return path.indexOf(repository.getWorkingDirectoryPath()) !== -1; + return filePath.indexOf(repository.getWorkingDirectoryPath()) !== -1; } else { return false; } From c89880ef1509a73ff0ba3009a3a4c0f5edf226df Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 27 Apr 2017 11:41:42 -0400 Subject: [PATCH 41/55] :fire: unused default @invalidate() arg --- lib/models/repository-states/present.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index ac04ba58b6..7833f681be 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -11,14 +11,12 @@ import Branch from '../branch'; import Remote from '../remote'; import Commit from '../commit'; -const ALL = Symbol('all'); - /** * Decorator for an async method that invalidates the cache after execution (regardless of success or failure). * Optionally parameterized by a function that accepts the same arguments as the function that returns the list of cache * keys to invalidate. */ -function invalidate(spec = () => ALL) { +function invalidate(spec) { return function(target, name, descriptor) { const original = descriptor.value; descriptor.value = function(...args) { @@ -67,11 +65,7 @@ export default class Present extends State { acceptInvalidation(spec, args) { const keys = spec(...args); - if (keys !== ALL) { - this.cache.invalidate(keys); - } else { - this.cache.clear(); - } + this.cache.invalidate(keys); this.didUpdate(); } From f49cee0561c8dd9b4583fdd12243f24966f01d09 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 27 Apr 2017 14:24:26 -0400 Subject: [PATCH 42/55] Evict groups of cache keys more efficiently --- lib/models/repository-states/present.js | 269 ++++++++++++++++-------- 1 file changed, 177 insertions(+), 92 deletions(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 7833f681be..38cfb8fcf4 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -82,16 +82,16 @@ export default class Present extends State { if (endsWith('.git', 'index')) { keys.add(Keys.changedFiles); keys.add(Keys.stagedChangesSinceParentCommit); - keys.add(Keys.allFilePatches); - keys.add(Keys.allIndices); + keys.add(Keys.filePatch.all); + keys.add(Keys.index.all); continue; } if (endsWith('.git', 'HEAD')) { keys.add(Keys.lastCommit); keys.add(Keys.currentBranch); - keys.add(Keys.allAheadCounts); - keys.add(Keys.allBehindCounts); + keys.add(Keys.aheadCount.all); + keys.add(Keys.behindCount.all); continue; } @@ -107,16 +107,16 @@ export default class Present extends State { } if (endsWith('.git', 'config')) { - keys.add(Keys.allConfigs); - keys.add(Keys.allAheadCounts); - keys.add(Keys.allBehindCounts); + keys.add(Keys.config.all); + keys.add(Keys.aheadCount.all); + keys.add(Keys.behindCount.all); continue; } // File change within the working directory const relativePath = path.relative(this.workdir(), fullPath); keys.add(Keys.changedFiles); - keys.add(Keys.filePatch(relativePath, {staged: false, amending: false})); + keys.add(Keys.filePatch.oneWith(relativePath, {staged: false})); } this.cache.invalidate(Array.from(keys)); @@ -177,7 +177,7 @@ export default class Present extends State { @invalidate(() => [ ...Keys.headOperationKeys(), - ...Keys.filePatchOps({staged: true}), + ...Keys.filePatch.eachWithOpts({staged: true}), ]) commit(message, options) { return this.git().commit(formatCommitMessage(message), options); @@ -187,7 +187,7 @@ export default class Present extends State { @invalidate(() => [ ...Keys.headOperationKeys(), - Keys.allIndices, + Keys.index.all, ]) merge(branchName) { return this.git().merge(branchName); @@ -196,8 +196,8 @@ export default class Present extends State { @invalidate(() => [ Keys.changedFiles, Keys.stagedChangesSinceParentCommit, - Keys.allFilePatches, - Keys.allIndices, + Keys.filePatch.all, + Keys.index.all, ]) abortMerge() { return this.git().abortMerge(); @@ -226,10 +226,10 @@ export default class Present extends State { @invalidate(() => [ Keys.stagedChangesSinceParentCommit, Keys.lastCommit, - Keys.allIndices, - Keys.allAheadCounts, - Keys.allBehindCounts, Keys.currentBranch, + Keys.index.all, + Keys.aheadCount.all, + Keys.behindCount.all, ]) checkout(revision, options = {}) { return this.git().checkout(revision, options); @@ -238,8 +238,8 @@ export default class Present extends State { @invalidate(paths => [ Keys.changedFiles, Keys.stagedChangesSinceParentCommit, - ...paths.map(Keys.index), - ...Keys.allFilePatchOps(paths, [{staged: true}, {staged: true, amending: true}]), + ...paths.map(fileName => Keys.index.oneWith(fileName)), + ...Keys.filePatch.eachWithFileOpts(paths, [{staged: true}, {staged: true, amending: true}]), ]) checkoutPathsAtRevision(paths, revision = 'HEAD') { return this.git().checkoutFiles(paths, revision); @@ -248,8 +248,8 @@ export default class Present extends State { // Remote interactions @invalidate(branchName => [ - Keys.aheadCount(branchName), - Keys.behindCount(branchName), + Keys.aheadCount.oneWith(branchName), + Keys.behindCount.oneWith(branchName), ]) async fetch(branchName) { const remote = await this.getRemoteForBranch(branchName); @@ -261,7 +261,7 @@ export default class Present extends State { @invalidate(() => [ ...Keys.headOperationKeys(), - Keys.allIndices, + Keys.index.all, ]) async pull(branchName) { const remote = await this.getRemoteForBranch(branchName); @@ -273,12 +273,12 @@ export default class Present extends State { @invalidate((branchName, options = {}) => { const keys = [ - Keys.aheadCount(branchName), - Keys.behindCount(branchName), + Keys.aheadCount.oneWith(branchName), + Keys.behindCount.oneWith(branchName), ]; if (options.setUpstream) { - keys.push(Keys.config(`branch.${branchName}.remote`)); + keys.push(...Keys.config.eachWithSetting(`branch.${branchName}.remote`)); } return keys; @@ -290,9 +290,9 @@ export default class Present extends State { // Configuration - @invalidate(option => Keys.allConfigOpts(option)) - setConfig(option, value, options) { - return this.git().setConfig(option, value, options); + @invalidate(setting => Keys.config.eachWithSetting(setting)) + setConfig(setting, value, options) { + return this.git().setConfig(setting, value, options); } // Direct blob interactions @@ -347,7 +347,7 @@ export default class Present extends State { @invalidate(paths => [ Keys.changedFiles, - ...paths.map(filePath => Keys.filePatch(filePath, {staged: false})), + ...paths.map(filePath => Keys.filePatch.oneWith(filePath, {staged: false})), ]) async discardWorkDirChangesForPaths(paths) { const untrackedFiles = await this.git().getUntrackedFiles(); @@ -385,7 +385,7 @@ export default class Present extends State { } getFilePatchForPath(filePath, {staged, amending} = {staged: false, amending: false}) { - return this.cache.getOrSet(Keys.filePatch(filePath, {staged, amending}), async () => { + return this.cache.getOrSet(Keys.filePatch.oneWith(filePath, {staged, amending}), async () => { const options = {staged, amending}; if (amending) { options.baseCommit = 'HEAD~'; @@ -402,7 +402,7 @@ export default class Present extends State { } readFileFromIndex(filePath) { - return this.cache.getOrSet(Keys.index(filePath), () => { + return this.cache.getOrSet(Keys.index.oneWith(filePath), () => { return this.git().readFileFromIndex(filePath); }); } @@ -452,19 +452,19 @@ export default class Present extends State { } getAheadCount(branchName) { - return this.cache.getOrSet(Keys.aheadCount(branchName), () => { + return this.cache.getOrSet(Keys.aheadCount.oneWith(branchName), () => { return this.git().getAheadCount(branchName); }); } getBehindCount(branchName) { - return this.cache.getOrSet(Keys.behindCount(branchName), () => { + return this.cache.getOrSet(Keys.behindCount.oneWith(branchName), () => { return this.git().getBehindCount(branchName); }); } getConfig(option, {local} = {local: false}) { - return this.cache.getOrSet(Keys.config(option, {local}), () => { + return this.cache.getOrSet(Keys.config.oneWith(option, {local}), () => { return this.git().getConfig(option, {local}); }); } @@ -571,120 +571,205 @@ function buildFilePatchesFromRawDiffs(rawDiffs) { class Cache { constructor() { this.storage = new Map(); + this.byGroup = new Map(); } getOrSet(key, operation) { - const existing = this.storage.get(key); + const primary = key.getPrimary(); + const existing = this.storage.get(primary); if (existing !== undefined) { return existing; } const created = operation(); - this.storage.set(key, created); + + this.storage.set(primary, created); + + const groups = key.getGroups(); + for (let i = 0; i < groups.length; i++) { + const group = groups[i]; + let groupSet = this.byGroup.get(group); + if (groupSet === undefined) { + groupSet = new Set(); + this.byGroup.set(group, groupSet); + } + groupSet.add(key); + } + return created; } invalidate(keys) { - const keyPrefixes = []; for (let i = 0; i < keys.length; i++) { - if (keys[i].startsWith('*')) { - keyPrefixes.push(keys[i]); - } else { - this.storage.delete(keys[i]); - } + keys[i].removeFromCache(this); } + } - if (keyPrefixes.length > 0) { - const keyPattern = new RegExp(`^(?:${keyPrefixes.map(prefix => prefix.substring(1)).join('|')})`); - for (const existingKey of this.storage.keys()) { - if (keyPattern.test(existingKey)) { - this.storage.delete(existingKey); - } - } - } + keysInGroup(group) { + return this.byGroup.get(group) || []; + } + + removePrimary(primary) { + this.storage.delete(primary); + } + + removeFromGroup(group, key) { + const groupSet = this.byGroup.get(group); + groupSet && groupSet.delete(key); } clear() { this.storage.clear(); + this.byGroup.clear(); } } +class CacheKey { + constructor(primary, groups = []) { + this.primary = primary; + this.groups = groups; + } -const Keys = { - changedFiles: 'changed-files', + getPrimary() { + return this.primary; + } - stagedChangesSinceParentCommit: 'staged-changes-since-parent-commit', + getGroups() { + return this.groups; + } - filePatch: (fileName, {staged, amending}) => { - let opKey = ''; - if (staged && amending) { - opKey = 'a'; - } else if (staged) { - opKey = 's'; - } else { - opKey = 'u'; - } + removeFromCache(cache, withoutGroup = null) { + cache.removePrimary(this.getPrimary()); - return `file-patch:${opKey}:${fileName}`; - }, - allFilePatchOps: (fileNames, ops = [{staged: false}, {staged: true}, {staged: true, amending: true}]) => { - const keys = []; - for (let i = 0; i < fileNames.length; i++) { - for (let j = 0; j < ops.length; j++) { - keys.push(Keys.filePatch(fileNames[i], ops[j])); + const groups = this.getGroups(); + for (let i = 0; i < groups.length; i++) { + const group = groups[i]; + if (group === withoutGroup) { + continue; } + + cache.removeFromGroup(group, this); } - return keys; + } +} + +class GroupKey { + constructor(group) { + this.group = group; + } + + removeFromCache(cache) { + for (const matchingKey of cache.keysInGroup(this.group)) { + matchingKey.removeFromCache(cache, this.group); + } + } +} + +const Keys = { + changedFiles: new CacheKey('changed-files'), + + stagedChangesSinceParentCommit: new CacheKey('staged-changes-since-parent-commit'), + + filePatch: { + _optKey: ({staged, amending}) => { + if (staged && amending) { + return 'a'; + } else if (staged) { + return 's'; + } else { + return 'u'; + } + }, + + oneWith: (fileName, options) => { // <-- Keys.filePatch + const optKey = Keys.filePatch._optKey(options); + return new CacheKey(`file-patch:${optKey}:${fileName}`, [ + 'file-patch', + `file-patch:${optKey}`, + ]); + }, + + eachWithFileOpts: (fileNames, opts) => { + const keys = []; + for (let i = 0; i < fileNames.length; i++) { + for (let j = 0; j < opts.length; j++) { + keys.push(Keys.filePatch.oneWith(fileNames[i], opts[j])); + } + } + return keys; + }, + + eachWithOpts: (...opts) => opts.map(opt => new GroupKey(`file-patch:${Keys.filePatch._optKey(opt)}`)), + + all: new GroupKey('file-patch'), }, - filePatchOps: (...ops) => ops.map(op => '*' + Keys.filePatch('', op)), - allFilePatches: '*file-patch:', - index: fileName => `index:${fileName}`, - allIndices: '*index:', + index: { + oneWith: fileName => new CacheKey(`index:${fileName}`, ['index']), - lastCommit: 'last-commit', + all: new GroupKey('index'), + }, - branches: 'branches', + lastCommit: new CacheKey('last-commit'), - currentBranch: 'current-branch', + branches: new CacheKey('branches'), - remotes: 'remotes', + currentBranch: new CacheKey('current-branch'), - aheadCount: branchName => `ahead-count:${branchName}`, - allAheadCounts: '*ahead-count:', + remotes: new CacheKey('remotes'), + + aheadCount: { + oneWith: branchName => new CacheKey(`ahead-count:${branchName}`, ['ahead-count']), + + all: new GroupKey('ahead-count'), + }, - behindCount: branchName => `behind-count:${branchName}`, - allBehindCounts: '*behind-count:', + behindCount: { + oneWith: branchName => new CacheKey(`behind-count:${branchName}`, ['behind-count']), - config: (option, {local}) => { - const opKey = local ? 'l' : ''; - return `config:${opKey}:${option}`; + all: new GroupKey('behind-count'), }, - allConfigOpts: option => [Keys.config(option, {local: true}), Keys.config(option, {local: false})], - allConfigs: '*config:', - blob: sha => `blob:${sha}`, + config: { + _optKey: options => (options.local ? 'l' : ''), + + oneWith: (setting, options) => { + const optKey = Keys.config._optKey(options); + return new CacheKey(`config:${optKey}:${setting}`, ['config', `config:${optKey}`]); + }, + + eachWithSetting: setting => [ + Keys.config.oneWith(setting, {local: true}), + Keys.config.oneWith(setting, {local: false}), + ], + + all: new GroupKey('config'), + }, + + blob: { + oneWith: sha => `blob:${sha}`, + }, // Common collections of keys and patterns for use with @invalidate(). workdirOperationKeys: fileNames => [ Keys.changedFiles, - ...Keys.allFilePatchOps(fileNames, [{staged: false}]), + ...Keys.filePatch.eachWithFileOpts(fileNames, [{staged: false}]), ], cacheOperationKeys: fileNames => [ ...Keys.workdirOperationKeys(fileNames), - ...Keys.allFilePatchOps(fileNames, [{staged: true}, {staged: true, amending: true}]), + ...Keys.filePatch.eachWithFileOpts(fileNames, [{staged: true}, {staged: true, amending: true}]), + ...fileNames.map(Keys.index.oneWith), Keys.stagedChangesSinceParentCommit, - ...fileNames.map(Keys.index), ], headOperationKeys: () => [ + ...Keys.filePatch.eachWithOpts({staged: true, amending: true}), Keys.changedFiles, Keys.stagedChangesSinceParentCommit, - ...Keys.filePatchOps({staged: true, amending: true}), Keys.lastCommit, - Keys.allAheadCounts, - Keys.allBehindCounts, + Keys.aheadCount.all, + Keys.behindCount.all, ], }; From 928663114e3ab3284f0f3755db5c6cbbd720660c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 27 Apr 2017 14:26:39 -0400 Subject: [PATCH 43/55] Bweep bweep, typo alert --- lib/models/workspace-change-observer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/models/workspace-change-observer.js b/lib/models/workspace-change-observer.js index f16f214eab..29feb4754a 100644 --- a/lib/models/workspace-change-observer.js +++ b/lib/models/workspace-change-observer.js @@ -76,7 +76,7 @@ export default class WorkspaceChangeObserver { events => { const filteredEvents = events.filter(e => { return ['config', 'index', 'HEAD', 'MERGE_HEAD'].includes(e.file || e.newFile) || - event.directory.includes(path.join('.git', 'refs')); + e.directory.includes(path.join('.git', 'refs')); }); if (filteredEvents.length) { this.logger.showEvents(filteredEvents); From 8900307ae7fd5049e4aa1420b5184229e745f66a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 27 Apr 2017 14:28:40 -0400 Subject: [PATCH 44/55] Filesystem events trigger observeFilesystemChange, not refresh --- test/github-package.test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/github-package.test.js b/test/github-package.test.js index 8f9892334c..c5c8c96383 100644 --- a/test/github-package.test.js +++ b/test/github-package.test.js @@ -546,8 +546,8 @@ describe('GithubPackage', function() { repository1 = contextPool.getContext(workdirPath1).getRepository(); repository2 = contextPool.getContext(workdirPath2).getRepository(); - sinon.stub(repository1, 'refresh'); - sinon.stub(repository2, 'refresh'); + sinon.stub(repository1, 'observeFilesystemChange'); + sinon.stub(repository2, 'observeFilesystemChange'); }); it('refreshes the appropriate Repository and Atom GitRepository when a file is changed in workspace 1', async function() { @@ -557,7 +557,7 @@ describe('GithubPackage', function() { fs.writeFileSync(path.join(workdirPath1, 'a.txt'), 'some changes', 'utf8'); - await assert.async.isTrue(repository1.refresh.called); + await assert.async.isTrue(repository1.observeFilesystemChange.called); await assert.async.isTrue(atomGitRepository1.refreshStatus.called); }); @@ -568,21 +568,21 @@ describe('GithubPackage', function() { fs.writeFileSync(path.join(workdirPath2, 'b.txt'), 'other changes', 'utf8'); - await assert.async.isTrue(repository2.refresh.called); + await assert.async.isTrue(repository2.observeFilesystemChange.called); await assert.async.isTrue(atomGitRepository2.refreshStatus.called); }); it('refreshes the appropriate Repository and Atom GitRepository when a commit is made in workspace 1', async function() { await repository1.git.exec(['commit', '-am', 'commit in repository1']); - await assert.async.isTrue(repository1.refresh.called); + await assert.async.isTrue(repository1.observeFilesystemChange.called); await assert.async.isTrue(atomGitRepository1.refreshStatus.called); }); it('refreshes the appropriate Repository and Atom GitRepository when a commit is made in workspace 2', async function() { await repository2.git.exec(['commit', '-am', 'commit in repository2']); - await assert.async.isTrue(repository2.refresh.called); + await assert.async.isTrue(repository2.observeFilesystemChange.called); await assert.async.isTrue(atomGitRepository2.refreshStatus.called); }); }); From 6ddee89bc22af60624991d12242cdadc08e9cc8b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 27 Apr 2017 15:30:57 -0400 Subject: [PATCH 45/55] Let's see what's happening to those events on Windows --- test/models/repository.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 5c2aafe767..6db094122d 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -1261,6 +1261,7 @@ describe('Repository', function() { function expectEvents(...fileNames) { return new Promise((resolve, reject) => { eventCallback = () => { + console.log(require('util').inspect({fileNames, observedEvents}, { depth: null })); const observedFileNames = new Set(observedEvents.map(event => event.file || event.newFile)); if (fileNames.every(fileName => observedFileNames.has(fileName))) { resolve(); From 6747f43bab63c673233b526fb3a9d10e49de366f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 27 Apr 2017 15:40:22 -0400 Subject: [PATCH 46/55] Fire eventCallback() even if all events are received before test --- test/models/repository.test.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 6db094122d..4995da1fec 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -1267,6 +1267,10 @@ describe('Repository', function() { resolve(); } }; + + if (observedEvents.length > 0) { + eventCallback(); + } }); } From 87a06e30dfa542eda0f947a8bc029157aa187519 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 27 Apr 2017 15:50:25 -0400 Subject: [PATCH 47/55] :fire: console --- test/models/repository.test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 4995da1fec..2566940ddf 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -1261,8 +1261,7 @@ describe('Repository', function() { function expectEvents(...fileNames) { return new Promise((resolve, reject) => { eventCallback = () => { - console.log(require('util').inspect({fileNames, observedEvents}, { depth: null })); - const observedFileNames = new Set(observedEvents.map(event => event.file || event.newFile)); + onst observedFileNames = new Set(observedEvents.map(event => event.file || event.newFile)); if (fileNames.every(fileName => observedFileNames.has(fileName))) { resolve(); } From 17c697f869544ce408b0364073c8018ba77a66f7 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 27 Apr 2017 15:57:32 -0400 Subject: [PATCH 48/55] I have a bajillion commits but it's only because I can't type --- test/models/repository.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 2566940ddf..b1b47055d5 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -1261,7 +1261,7 @@ describe('Repository', function() { function expectEvents(...fileNames) { return new Promise((resolve, reject) => { eventCallback = () => { - onst observedFileNames = new Set(observedEvents.map(event => event.file || event.newFile)); + const observedFileNames = new Set(observedEvents.map(event => event.file || event.newFile)); if (fileNames.every(fileName => observedFileNames.has(fileName))) { resolve(); } From 0aa21db7ddb3f70f2cc2f13ab913132e65c9ccfb Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 27 Apr 2017 16:09:11 -0400 Subject: [PATCH 49/55] wake up Travis From c7dbc40ccf61d21039e1c52dfee4eabe62efae3b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 2 May 2017 15:38:32 -0400 Subject: [PATCH 50/55] Restore missing "await" --- test/models/repository.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index b1b47055d5..78fd5149f2 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -1481,7 +1481,7 @@ describe('Repository', function() { const files = ['b.txt']; await assertCorrectInvalidation({repository, files}, async () => { await writeFile(path.join(workdir, 'b.txt'), 'new contents\n'); - expectEvents('b.txt'); + await expectEvents('b.txt'); }); }); }); From effc03de875f6409ad62e7a5c870990464e65ed4 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 2 May 2017 15:39:00 -0400 Subject: [PATCH 51/55] Only update if there's an actual change --- lib/models/repository-states/present.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 38cfb8fcf4..64691596d1 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -119,8 +119,10 @@ export default class Present extends State { keys.add(Keys.filePatch.oneWith(relativePath, {staged: false})); } - this.cache.invalidate(Array.from(keys)); - this.didUpdate(); + if (keys.size > 0) { + this.cache.invalidate(Array.from(keys)); + this.didUpdate(); + } } refresh() { From dc37811912345bb0cccc64852f3a1f3f42229263 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 2 May 2017 15:51:04 -0400 Subject: [PATCH 52/55] :fire: `expected` lists and pending tests --- test/models/repository.test.js | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 78fd5149f2..5b46e63725 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -1096,11 +1096,8 @@ describe('Repository', function() { const skip = [ 'readFileFromIndex modified-on-both-ours.txt', ]; - const expected = [ - 'getLastCommit', // Needs to be invalidated when the commit succeeds - ]; const files = ['modified-on-both-ours.txt']; - await assertCorrectInvalidation({repository, files, expected, skip}, async () => { + await assertCorrectInvalidation({repository, files, skip}, async () => { await assert.isRejected(repository.merge('origin/branch')); }); }); @@ -1114,11 +1111,8 @@ describe('Repository', function() { const stagedFile = 'modified-on-both-ours.txt'; await repository.stageFiles([stagedFile]); - const expected = [ - 'getFilePatchForPath {unstaged} modified-on-both-ours.txt', - ]; const files = [stagedFile]; - await assertCorrectInvalidation({repository, files, expected}, async () => { + await assertCorrectInvalidation({repository, files}, async () => { await repository.abortMerge(); }); }); @@ -1175,12 +1169,8 @@ describe('Repository', function() { await repository.stageFiles(['new-file.txt']); await repository.commit('wat'); - const expected = [ - 'getStatusesForChangedFiles', - 'readFileFromIndex new-file.txt', - ]; const files = ['new-file.txt', 'file.txt']; - await assertCorrectInvalidation({repository, files, expected}, async () => { + await assertCorrectInvalidation({repository, files}, async () => { await repository.pull('master'); }); }); @@ -1386,8 +1376,6 @@ describe('Repository', function() { }); }); - it('when writing a merge conflict to the index'); - it('when checking out a revision', async function() { const {repository, observer} = await wireUpObserver(); From 56db6fd30e9822d17edd171c90e56da3b3681fdb Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 2 May 2017 16:32:51 -0400 Subject: [PATCH 53/55] Account for repository destruction --- lib/models/repository.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/models/repository.js b/lib/models/repository.js index e3116fb49b..2497ed4b72 100644 --- a/lib/models/repository.js +++ b/lib/models/repository.js @@ -190,7 +190,14 @@ export default class Repository { } async saveDiscardHistory() { + if (this.isDestroyed()) { + return; + } + const historySha = await this.createDiscardHistoryBlob(); + if (this.isDestroyed()) { + return; + } await this.setConfig('atomGithub.historySha', historySha); } } From 45a7572b572b7c2550bcb10280cebb4198fb14fd Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 2 May 2017 16:40:32 -0400 Subject: [PATCH 54/55] Update Keys invalidated for writeMergeConflictToIndex --- lib/models/repository-states/present.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/models/repository-states/present.js b/lib/models/repository-states/present.js index 89dc7e2a3a..b6fbd3aebb 100644 --- a/lib/models/repository-states/present.js +++ b/lib/models/repository-states/present.js @@ -216,8 +216,8 @@ export default class Present extends State { @invalidate(filePath => [ Keys.changedFiles, Keys.stagedChangesSinceParentCommit, - ...Keys.allFilePatchOps([filePath]), - Keys.index(filePath), + ...Keys.filePatch.eachWithFileOpts([filePath], [{staged: false}, {staged: true}, {staged: true, amending: true}]), + Keys.index.oneWith(filePath), ]) writeMergeConflictToIndex(filePath, commonBaseSha, oursSha, theirsSha) { return this.git().writeMergeConflictToIndex(filePath, commonBaseSha, oursSha, theirsSha); From a5e9503333eb56d69c3c05b9e28e4dcf9bedcc65 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 3 May 2017 08:15:55 -0400 Subject: [PATCH 55/55] Cache invalidation test for writeMergeConflictToIndex() --- test/models/repository.test.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/test/models/repository.test.js b/test/models/repository.test.js index 5b46e63725..decc4f3c4b 100644 --- a/test/models/repository.test.js +++ b/test/models/repository.test.js @@ -1117,7 +1117,30 @@ describe('Repository', function() { }); }); - it('when writing a merge conflict to the index'); + it('when writing a merge conflict to the index', async function() { + const workdir = await cloneRepository('three-files'); + const repository = new Repository(workdir); + await repository.getLoadPromise(); + + const fullPath = path.join(workdir, 'a.txt'); + await writeFile(fullPath, 'qux\nfoo\nbar\n'); + await repository.git.exec(['update-index', '--chmod=+x', 'a.txt']); + + const commonBaseSha = '7f95a814cbd9b366c5dedb6d812536dfef2fffb7'; + const oursSha = '95d4c5b7b96b3eb0853f586576dc8b5ac54837e0'; + const theirsSha = '5da808cc8998a762ec2761f8be2338617f8f12d9'; + + const files = ['a.txt']; + const skip = [ + 'getFilePatchForPath {unstaged} a.txt', + 'getFilePatchForPath {staged} a.txt', + 'getFilePatchForPath {staged, amending} a.txt', + 'readFileFromIndex a.txt', + ]; + await assertCorrectInvalidation({repository, files, skip}, async () => { + await repository.writeMergeConflictToIndex('a.txt', commonBaseSha, oursSha, theirsSha); + }); + }); it('when checking out a revision', async function() { const workdir = await cloneRepository('multi-commits-files');