diff --git a/src/dataSource.ts b/src/dataSource.ts index cc3028a7..a41bb17a 100644 --- a/src/dataSource.ts +++ b/src/dataSource.ts @@ -342,10 +342,11 @@ export class DataSource extends Disposable { * @param repo The path of the repository. * @param commitHash The hash of the commit open in the Commit Details View. * @param hasParents Does the commit have parents + * @param parentIndex The index of the commit parent * @returns The commit details. */ - public getCommitDetails(repo: string, commitHash: string, hasParents: boolean): Promise { - const fromCommit = commitHash + (hasParents ? '^' : ''); + public getCommitDetails(repo: string, commitHash: string, hasParents: boolean, parentIndex: number): Promise { + const fromCommit = commitHash + (hasParents ? '^' + parentIndex : ''); return Promise.all([ this.getCommitDetailsBase(repo, commitHash), this.getDiffNameStatus(repo, fromCommit, commitHash), @@ -363,13 +364,15 @@ export class DataSource extends Disposable { * @param repo The path of the repository. * @param commitHash The hash of the stash commit open in the Commit Details View. * @param stash The stash. + * @param parentIndex the index of the stash parent * @returns The stash details. */ - public getStashDetails(repo: string, commitHash: string, stash: GitCommitStash): Promise { + public getStashDetails(repo: string, commitHash: string, stash: GitCommitStash, parentIndex: number): Promise { + const fromCommit = commitHash + '^' + parentIndex; return Promise.all([ this.getCommitDetailsBase(repo, commitHash), - this.getDiffNameStatus(repo, stash.baseHash, commitHash), - this.getDiffNumStat(repo, stash.baseHash, commitHash), + this.getDiffNameStatus(repo, fromCommit, commitHash), + this.getDiffNumStat(repo, fromCommit, commitHash), stash.untrackedFilesHash !== null ? this.getDiffNameStatus(repo, stash.untrackedFilesHash, stash.untrackedFilesHash) : Promise.resolve([]), stash.untrackedFilesHash !== null ? this.getDiffNumStat(repo, stash.untrackedFilesHash, stash.untrackedFilesHash) : Promise.resolve([]) ]).then((results) => { diff --git a/src/gitGraphView.ts b/src/gitGraphView.ts index ed3f8722..8b2e21b2 100644 --- a/src/gitGraphView.ts +++ b/src/gitGraphView.ts @@ -232,13 +232,14 @@ export class GitGraphView extends Disposable { msg.commitHash === UNCOMMITTED ? this.dataSource.getUncommittedDetails(msg.repo) : msg.stash === null - ? this.dataSource.getCommitDetails(msg.repo, msg.commitHash, msg.hasParents) - : this.dataSource.getStashDetails(msg.repo, msg.commitHash, msg.stash), + ? this.dataSource.getCommitDetails(msg.repo, msg.commitHash, msg.hasParents, msg.parentIndex) + : this.dataSource.getStashDetails(msg.repo, msg.commitHash, msg.stash, msg.parentIndex), msg.avatarEmail !== null ? this.avatarManager.getAvatarImage(msg.avatarEmail) : Promise.resolve(null) ]); this.sendMessage({ command: 'commitDetails', ...data[0], + parentIndex: msg.parentIndex, avatar: data[1], codeReview: msg.commitHash !== UNCOMMITTED ? this.extensionState.getCodeReview(msg.repo, msg.commitHash) : null, refresh: msg.refresh diff --git a/src/types.ts b/src/types.ts index 8410deae..d31845a4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -662,6 +662,7 @@ export interface RequestCommitDetails extends RepoRequest { readonly command: 'commitDetails'; readonly commitHash: string; readonly hasParents: boolean; + readonly parentIndex: number; readonly stash: GitCommitStash | null; // null => request is for a commit, otherwise => request is for a stash readonly avatarEmail: string | null; // string => fetch avatar with the given email, null => don't fetch avatar readonly refresh: boolean; @@ -669,6 +670,7 @@ export interface RequestCommitDetails extends RepoRequest { export interface ResponseCommitDetails extends ResponseWithErrorInfo { readonly command: 'commitDetails'; readonly commitDetails: GitCommitDetails | null; + readonly parentIndex: number; readonly avatar: string | null; readonly codeReview: CodeReview | null; readonly refresh: boolean; diff --git a/tests/dataSource.test.ts b/tests/dataSource.test.ts index 07143639..b2c49dd6 100644 --- a/tests/dataSource.test.ts +++ b/tests/dataSource.test.ts @@ -2662,7 +2662,7 @@ describe('DataSource', () => { mockGitSuccessOnce(['0 0 dir/deleted.txt', '1 1 dir/modified.txt', '2 3 ', 'dir/renamed-old.txt', 'dir/renamed-new.txt', ''].join('\0')); // Run - const result = await dataSource.getCommitDetails('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', true); + const result = await dataSource.getCommitDetails('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', true, 1); // Assert expect(result).toStrictEqual({ @@ -2704,8 +2704,8 @@ describe('DataSource', () => { error: null }); expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['-c', 'log.showSignature=false', 'show', '--quiet', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '--format=%HXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%PXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%anXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%aeXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%atXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%cnXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%ceXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%ctXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%B'], expect.objectContaining({ cwd: '/path/to/repo' })); - expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--name-status', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); - expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--numstat', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); + expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--name-status', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^1', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); + expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--numstat', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^1', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); }); it('Should return the commit details (commit doesn\'t have parents)', async () => { @@ -2715,7 +2715,7 @@ describe('DataSource', () => { mockGitSuccessOnce(['1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '0 0 dir/deleted.txt', '1 1 dir/modified.txt', '2 3 ', 'dir/renamed-old.txt', 'dir/renamed-new.txt', ''].join('\0')); // Run - const result = await dataSource.getCommitDetails('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', false); + const result = await dataSource.getCommitDetails('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', false, 1); // Assert expect(result).toStrictEqual({ @@ -2775,7 +2775,7 @@ describe('DataSource', () => { onDidChangeConfiguration.emit({ affectsConfiguration: (section) => section === 'git-graph.repository.commits.showSignatureStatus' }); - const result = await dataSource.getCommitDetails('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', true); + const result = await dataSource.getCommitDetails('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', true, 1); // Assert expect(result).toStrictEqual({ @@ -2821,8 +2821,8 @@ describe('DataSource', () => { error: null }); expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['-c', 'log.showSignature=false', 'show', '--quiet', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '--format=%HXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%PXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%anXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%aeXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%atXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%cnXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%ceXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%ctXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%G?XX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%GSXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%GKXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%B'], expect.objectContaining({ cwd: '/path/to/repo' })); - expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--name-status', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); - expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--numstat', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); + expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--name-status', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^1', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); + expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--numstat', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^1', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); }); it('Should return the commit details (using git-graph.showSignatureStatus)', async () => { @@ -2839,7 +2839,7 @@ describe('DataSource', () => { onDidChangeConfiguration.emit({ affectsConfiguration: (section) => section === 'git-graph.showSignatureStatus' }); - const result = await dataSource.getCommitDetails('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', true); + const result = await dataSource.getCommitDetails('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', true, 1); // Assert expect(result).toStrictEqual({ @@ -2885,8 +2885,8 @@ describe('DataSource', () => { error: null }); expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['-c', 'log.showSignature=false', 'show', '--quiet', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '--format=%HXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%PXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%anXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%aeXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%atXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%cnXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%ceXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%ctXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%G?XX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%GSXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%GKXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%B'], expect.objectContaining({ cwd: '/path/to/repo' })); - expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--name-status', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); - expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--numstat', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); + expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--name-status', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^1', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); + expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--numstat', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^1', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); }); it('Should return the commit details (without signature status) when Git is older than 2.4.0', async () => { @@ -2900,7 +2900,7 @@ describe('DataSource', () => { onDidChangeGitExecutable.emit({ path: '/path/to/git', version: '2.3.0' }); // Run - const result = await dataSource.getCommitDetails('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', true); + const result = await dataSource.getCommitDetails('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', true, 1); // Assert expect(result).toStrictEqual({ @@ -2942,8 +2942,8 @@ describe('DataSource', () => { error: null }); expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['-c', 'log.showSignature=false', 'show', '--quiet', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '--format=%HXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%PXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%anXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%aeXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%atXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%cnXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%ceXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%ctXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%B'], expect.objectContaining({ cwd: '/path/to/repo' })); - expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--name-status', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); - expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--numstat', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); + expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--name-status', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^1', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); + expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--numstat', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^1', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); }); it('Should return the commit details (using git-graph.repository.useMailmap)', async () => { @@ -2959,7 +2959,7 @@ describe('DataSource', () => { onDidChangeConfiguration.emit({ affectsConfiguration: (section) => section === 'git-graph.repository.useMailmap' }); - const result = await dataSource.getCommitDetails('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', true); + const result = await dataSource.getCommitDetails('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', true, 1); // Assert expect(result).toStrictEqual({ @@ -3001,8 +3001,8 @@ describe('DataSource', () => { error: null }); expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['-c', 'log.showSignature=false', 'show', '--quiet', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '--format=%HXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%PXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%aNXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%aEXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%atXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%cNXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%cEXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%ctXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%B'], expect.objectContaining({ cwd: '/path/to/repo' })); - expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--name-status', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); - expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--numstat', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); + expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--name-status', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^1', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); + expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--numstat', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^1', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); }); it('Should return the commit details (handling unknown Git file status returned by git diff --name-status)', async () => { @@ -3012,7 +3012,7 @@ describe('DataSource', () => { mockGitSuccessOnce(['0 0 dir/deleted.txt', '1 1 dir/modified.txt', '2 3 ', 'dir/renamed-old.txt', 'dir/renamed-new.txt', ''].join('\0')); // Run - const result = await dataSource.getCommitDetails('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', true); + const result = await dataSource.getCommitDetails('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', true, 1); // Assert expect(result).toStrictEqual({ @@ -3040,8 +3040,8 @@ describe('DataSource', () => { error: null }); expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['-c', 'log.showSignature=false', 'show', '--quiet', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '--format=%HXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%PXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%anXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%aeXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%atXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%cnXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%ceXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%ctXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%B'], expect.objectContaining({ cwd: '/path/to/repo' })); - expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--name-status', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); - expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--numstat', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); + expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--name-status', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^1', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); + expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--numstat', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^1', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); }); it('Should return the commit details (handling unexpected response format returned by git diff --numstat)', async () => { @@ -3051,7 +3051,7 @@ describe('DataSource', () => { mockGitSuccessOnce(['0 0 dir/deleted.txt', '1 1', '2 3 ', 'dir/renamed-old.txt', 'dir/renamed-new.txt', ''].join('\0')); // Run - const result = await dataSource.getCommitDetails('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', true); + const result = await dataSource.getCommitDetails('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', true, 1); // Assert expect(result).toStrictEqual({ @@ -3093,8 +3093,61 @@ describe('DataSource', () => { error: null }); expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['-c', 'log.showSignature=false', 'show', '--quiet', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '--format=%HXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%PXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%anXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%aeXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%atXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%cnXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%ceXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%ctXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%B'], expect.objectContaining({ cwd: '/path/to/repo' })); - expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--name-status', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); - expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--numstat', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); + expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--name-status', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^1', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); + expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--numstat', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^1', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); + }); + + it('Should return the commit details (and request diff to 2nd parent)', async () => { + // Setup + mockGitSuccessOnce('1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2bXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPba1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2XX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbTest AuthorXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbtest-author@mhutchie.comXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb1587559258XX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbTest CommitterXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbtest-committer@mhutchie.comXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb1587559259XX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbCommit Message.\r\nSecond Line.'); + mockGitSuccessOnce(['D', 'dir/deleted.txt', 'M', 'dir/modified.txt', 'R100', 'dir/renamed-old.txt', 'dir/renamed-new.txt', ''].join('\0')); + mockGitSuccessOnce(['0 0 dir/deleted.txt', '1 1 dir/modified.txt', '2 3 ', 'dir/renamed-old.txt', 'dir/renamed-new.txt', ''].join('\0')); + + // Run + const result = await dataSource.getCommitDetails('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', true, 2); + + // Assert + expect(result).toStrictEqual({ + commitDetails: { + hash: '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', + parents: ['a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2'], + author: 'Test Author', + authorEmail: 'test-author@mhutchie.com', + authorDate: 1587559258, + committer: 'Test Committer', + committerEmail: 'test-committer@mhutchie.com', + committerDate: 1587559259, + signature: null, + body: 'Commit Message.\nSecond Line.', + fileChanges: [ + { + additions: 0, + deletions: 0, + newFilePath: 'dir/deleted.txt', + oldFilePath: 'dir/deleted.txt', + type: 'D' + }, + { + additions: 1, + deletions: 1, + newFilePath: 'dir/modified.txt', + oldFilePath: 'dir/modified.txt', + type: 'M' + }, + { + additions: 2, + deletions: 3, + newFilePath: 'dir/renamed-new.txt', + oldFilePath: 'dir/renamed-old.txt', + type: 'R' + } + ] + }, + error: null + }); + expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['-c', 'log.showSignature=false', 'show', '--quiet', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '--format=%HXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%PXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%anXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%aeXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%atXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%cnXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%ceXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%ctXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%B'], expect.objectContaining({ cwd: '/path/to/repo' })); + expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--name-status', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^2', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); + expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--numstat', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^2', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); }); it('Should return an error message thrown by git (when thrown by git show)', async () => { @@ -3104,7 +3157,7 @@ describe('DataSource', () => { mockGitSuccessOnce(['0 0 dir/deleted.txt', '1 1 dir/modified.txt', '2 3 ', 'dir/renamed-old.txt', 'dir/renamed-new.txt', ''].join('\0')); // Run - const result = await dataSource.getCommitDetails('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', true); + const result = await dataSource.getCommitDetails('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', true, 1); // Assert expect(result).toStrictEqual({ @@ -3120,7 +3173,7 @@ describe('DataSource', () => { mockGitSuccessOnce(['0 0 dir/deleted.txt', '1 1 dir/modified.txt', '2 3 ', 'dir/renamed-old.txt', 'dir/renamed-new.txt', ''].join('\0')); // Run - const result = await dataSource.getCommitDetails('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', true); + const result = await dataSource.getCommitDetails('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', true, 1); // Assert expect(result).toStrictEqual({ @@ -3136,7 +3189,7 @@ describe('DataSource', () => { mockGitThrowingErrorOnce(); // Run - const result = await dataSource.getCommitDetails('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', true); + const result = await dataSource.getCommitDetails('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', true, 1); // Assert expect(result).toStrictEqual({ @@ -3158,7 +3211,7 @@ describe('DataSource', () => { selector: 'refs/stash@{0}', baseHash: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', untrackedFilesHash: null - }); + }, 1); // Assert expect(result).toStrictEqual({ @@ -3200,8 +3253,8 @@ describe('DataSource', () => { error: null }); expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['-c', 'log.showSignature=false', 'show', '--quiet', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '--format=%HXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%PXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%anXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%aeXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%atXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%cnXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%ceXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%ctXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%B'], expect.objectContaining({ cwd: '/path/to/repo' })); - expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--name-status', '--find-renames', '--diff-filter=AMDR', '-z', 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); - expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--numstat', '--find-renames', '--diff-filter=AMDR', '-z', 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); + expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--name-status', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^1', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); + expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--numstat', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^1', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); }); it('Should return the stash details (including untracked files)', async () => { @@ -3217,7 +3270,7 @@ describe('DataSource', () => { selector: 'refs/stash@{0}', baseHash: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', untrackedFilesHash: 'c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4' - }); + }, 1); // Assert expect(result).toStrictEqual({ @@ -3266,12 +3319,69 @@ describe('DataSource', () => { error: null }); expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['-c', 'log.showSignature=false', 'show', '--quiet', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '--format=%HXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%PXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%anXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%aeXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%atXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%cnXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%ceXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%ctXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%B'], expect.objectContaining({ cwd: '/path/to/repo' })); - expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--name-status', '--find-renames', '--diff-filter=AMDR', '-z', 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); - expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--numstat', '--find-renames', '--diff-filter=AMDR', '-z', 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); + expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--name-status', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^1', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); + expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--numstat', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^1', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff-tree', '--name-status', '-r', '--root', '--find-renames', '--diff-filter=AMDR', '-z', 'c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'], expect.objectContaining({ cwd: '/path/to/repo' })); expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff-tree', '--numstat', '-r', '--root', '--find-renames', '--diff-filter=AMDR', '-z', 'c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'], expect.objectContaining({ cwd: '/path/to/repo' })); }); + it('Should return the stash details with diff to second parent', async () => { + // Setup + mockGitSuccessOnce('1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2bXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPba1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3XX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbTest AuthorXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbtest-author@mhutchie.comXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb1587559258XX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbTest CommitterXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbtest-committer@mhutchie.comXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb1587559259XX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbCommit Message.\r\nSecond Line.'); + mockGitSuccessOnce(['D', 'dir/deleted.txt', 'M', 'dir/modified.txt', 'R100', 'dir/renamed-old.txt', 'dir/renamed-new.txt', ''].join('\0')); + mockGitSuccessOnce(['0 0 dir/deleted.txt', '1 1 dir/modified.txt', '2 3 ', 'dir/renamed-old.txt', 'dir/renamed-new.txt', ''].join('\0')); + + // Run + const result = await dataSource.getStashDetails('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', { + selector: 'refs/stash@{0}', + baseHash: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', + untrackedFilesHash: null + }, 2); + + // Assert + expect(result).toStrictEqual({ + commitDetails: { + hash: '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', + parents: ['a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', 'b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3'], + author: 'Test Author', + authorEmail: 'test-author@mhutchie.com', + authorDate: 1587559258, + committer: 'Test Committer', + committerEmail: 'test-committer@mhutchie.com', + committerDate: 1587559259, + signature: null, + body: 'Commit Message.\nSecond Line.', + fileChanges: [ + { + additions: 0, + deletions: 0, + newFilePath: 'dir/deleted.txt', + oldFilePath: 'dir/deleted.txt', + type: 'D' + }, + { + additions: 1, + deletions: 1, + newFilePath: 'dir/modified.txt', + oldFilePath: 'dir/modified.txt', + type: 'M' + }, + { + additions: 2, + deletions: 3, + newFilePath: 'dir/renamed-new.txt', + oldFilePath: 'dir/renamed-old.txt', + type: 'R' + } + ] + }, + error: null + }); + expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['-c', 'log.showSignature=false', 'show', '--quiet', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '--format=%HXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%PXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%anXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%aeXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%atXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%cnXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%ceXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%ctXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPbXX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb%B'], expect.objectContaining({ cwd: '/path/to/repo' })); + expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--name-status', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^2', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); + expect(spyOnSpawn).toBeCalledWith('/path/to/git', ['diff', '--numstat', '--find-renames', '--diff-filter=AMDR', '-z', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^2', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'], expect.objectContaining({ cwd: '/path/to/repo' })); + }); + it('Should return an error message thrown by git (when thrown by git show)', async () => { // Setup mockGitThrowingErrorOnce(); @@ -3283,7 +3393,7 @@ describe('DataSource', () => { selector: 'refs/stash@{0}', baseHash: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', untrackedFilesHash: null - }); + }, 1); // Assert expect(result).toStrictEqual({ @@ -3303,7 +3413,7 @@ describe('DataSource', () => { selector: 'refs/stash@{0}', baseHash: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', untrackedFilesHash: null - }); + }, 1); // Assert expect(result).toStrictEqual({ @@ -3323,7 +3433,7 @@ describe('DataSource', () => { selector: 'refs/stash@{0}', baseHash: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', untrackedFilesHash: null - }); + }, 1); // Assert expect(result).toStrictEqual({ @@ -3345,7 +3455,7 @@ describe('DataSource', () => { selector: 'refs/stash@{0}', baseHash: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', untrackedFilesHash: 'c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4' - }); + }, 1); // Assert expect(result).toStrictEqual({ @@ -3367,7 +3477,7 @@ describe('DataSource', () => { selector: 'refs/stash@{0}', baseHash: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', untrackedFilesHash: 'c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4' - }); + }, 1); // Assert expect(result).toStrictEqual({ diff --git a/tests/gitGraphView.test.ts b/tests/gitGraphView.test.ts index fb64254f..5fab2a31 100644 --- a/tests/gitGraphView.test.ts +++ b/tests/gitGraphView.test.ts @@ -959,6 +959,7 @@ describe('GitGraphView', () => { repo: '/path/to/repo', commitHash: '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', hasParents: true, + parentIndex: 1, stash: null, avatarEmail: 'user@mhutchie.com', refresh: false @@ -966,13 +967,14 @@ describe('GitGraphView', () => { // Assert await waitForExpect(() => { - expect(spyOnGetCommitDetails).toHaveBeenCalledWith('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', true); + expect(spyOnGetCommitDetails).toHaveBeenCalledWith('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', true, 1); expect(spyOnGetAvatarImage).toHaveBeenCalledWith('user@mhutchie.com'); expect(spyOnGetCodeReview).toHaveBeenCalledWith('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'); expect(messages).toStrictEqual([ { command: 'commitDetails', commitDetails: null, + parentIndex: 1, avatar: getAvatarImageResolvedValue, codeReview: getCodeReviewResolvedValue, refresh: false, @@ -996,6 +998,7 @@ describe('GitGraphView', () => { repo: '/path/to/repo', commitHash: utils.UNCOMMITTED, hasParents: true, + parentIndex: 1, stash: null, avatarEmail: null, refresh: false @@ -1010,6 +1013,7 @@ describe('GitGraphView', () => { { command: 'commitDetails', commitDetails: null, + parentIndex: 1, avatar: null, codeReview: null, refresh: false, @@ -1040,6 +1044,7 @@ describe('GitGraphView', () => { repo: '/path/to/repo', commitHash: '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', hasParents: true, + parentIndex: 1, stash: stash, avatarEmail: null, refresh: false @@ -1047,13 +1052,14 @@ describe('GitGraphView', () => { // Assert await waitForExpect(() => { - expect(spyOnGetStashDetails).toHaveBeenCalledWith('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', stash); + expect(spyOnGetStashDetails).toHaveBeenCalledWith('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', stash, 1); expect(spyOnGetAvatarImage).not.toHaveBeenCalled(); expect(spyOnGetCodeReview).toHaveBeenCalledWith('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b'); expect(messages).toStrictEqual([ { command: 'commitDetails', commitDetails: null, + parentIndex: 1, avatar: null, codeReview: getCodeReviewResolvedValue, refresh: false, diff --git a/web/findWidget.ts b/web/findWidget.ts index ff0e0974..9ec5765d 100644 --- a/web/findWidget.ts +++ b/web/findWidget.ts @@ -397,7 +397,7 @@ class FindWidget { if (commitHash !== null && !this.view.isCdvOpen(commitHash, null)) { const commitElem = findCommitElemWithId(getCommitElems(), this.view.getCommitId(commitHash)); if (commitElem !== null) { - this.view.loadCommitDetails(commitElem); + this.view.loadCommitDetails(commitElem, DEFAULT_PARENT_INDEX); } } } diff --git a/web/global.d.ts b/web/global.d.ts index 5e1072ff..a4bafd06 100644 --- a/web/global.d.ts +++ b/web/global.d.ts @@ -25,6 +25,7 @@ declare global { index: number; commitHash: string; commitElem: HTMLElement | null; + parentIndex: number; compareWithHash: string | null; compareWithElem: HTMLElement | null; commitDetails: GG.GitCommitDetails | null; diff --git a/web/main.ts b/web/main.ts index b12ba1e5..69839bab 100644 --- a/web/main.ts +++ b/web/main.ts @@ -322,7 +322,7 @@ class GitGraphView { if (this.expandedCommit.compareWithHash === null) { // Commit Details View is open if (this.expandedCommit.commitHash === UNCOMMITTED) { - this.requestCommitDetails(this.expandedCommit.commitHash, true); + this.requestCommitDetails(this.expandedCommit.commitHash, true, this.expandedCommit.parentIndex); } } else { // Commit Comparison is open @@ -424,7 +424,7 @@ class GitGraphView { if (compareWithElem !== null) { this.loadCommitComparison(commitElem, compareWithElem); } else { - this.loadCommitDetails(commitElem); + this.loadCommitDetails(commitElem, DEFAULT_PARENT_INDEX); } } else { showErrorMessage('Unable to resume Code Review, it could not be found in the latest ' + this.maxCommits + ' commits that were loaded in this repository.'); @@ -660,13 +660,14 @@ class GitGraphView { this.settingsWidget.refresh(); } - public requestCommitDetails(hash: string, refresh: boolean) { + public requestCommitDetails(hash: string, refresh: boolean, parentIndex: number) { let commit = this.commits[this.commitLookup[hash]]; sendMessage({ command: 'commitDetails', repo: this.currentRepo, commitHash: hash, hasParents: commit.parents.length > 0, + parentIndex: parentIndex, stash: commit.stash, avatarEmail: this.config.fetchAvatars && hash !== UNCOMMITTED ? commit.email : null, refresh: refresh @@ -746,6 +747,7 @@ class GitGraphView { index: index, commitHash: commitHash, commitElem: commitElem, + parentIndex: DEFAULT_PARENT_INDEX, compareWithHash: compareWithHash, compareWithElem: compareWithElem, commitDetails: null, @@ -900,12 +902,12 @@ class GitGraphView { if (expandedCommit.compareWithHash === null) { // Commit Details View is open if (!expandedCommit.loading && expandedCommit.commitDetails !== null && expandedCommit.fileTree !== null) { - this.showCommitDetails(expandedCommit.commitDetails, expandedCommit.fileTree, expandedCommit.avatar, expandedCommit.codeReview, expandedCommit.lastViewedFile, true); + this.showCommitDetails(expandedCommit.commitDetails, expandedCommit.parentIndex, expandedCommit.fileTree, expandedCommit.avatar, expandedCommit.codeReview, expandedCommit.lastViewedFile, true); if (expandedCommit.commitHash === UNCOMMITTED) { - this.requestCommitDetails(expandedCommit.commitHash, true); + this.requestCommitDetails(expandedCommit.commitHash, true, expandedCommit.parentIndex); } } else { - this.loadCommitDetails(commitElem); + this.loadCommitDetails(commitElem, expandedCommit.parentIndex); } } else { // Commit Comparison is open @@ -2046,7 +2048,7 @@ class GitGraphView { if (newHashIndex > -1) { handledEvent(e); const elem = findCommitElemWithId(getCommitElems(), newHashIndex); - if (elem !== null) this.loadCommitDetails(elem); + if (elem !== null) this.loadCommitDetails(elem, DEFAULT_PARENT_INDEX); } } else if (e.key && (e.ctrlKey || e.metaKey)) { const key = e.key.toLowerCase(), keybindings = this.config.keybindings; @@ -2092,15 +2094,36 @@ class GitGraphView { const value = unescapeHtml((e.target).dataset.value!); switch ((e.target).dataset.type!) { case 'commit': - if (typeof this.commitLookup[value] === 'number' && (this.expandedCommit === null || this.expandedCommit.commitHash !== value || this.expandedCommit.compareWithHash !== null)) { - const elem = findCommitElemWithId(getCommitElems(), this.commitLookup[value]); - if (elem !== null) this.loadCommitDetails(elem); + if (e.ctrlKey || e.metaKey) { + if(this.expandedCommit !== null && this.expandedCommit.commitElem !== null && this.expandedCommit.commitDetails !== null) { + const parentIndex = this.expandedCommit.commitDetails.parents.indexOf(value) + 1; + this.loadCommitDetails(this.expandedCommit.commitElem, parentIndex); + } + } else { + if (this.expandedCommit === null || this.expandedCommit.commitHash !== value || this.expandedCommit.compareWithHash !== null) { + const elem = findCommitElemWithId(getCommitElems(), this.commitLookup[value] || null); + if (elem === null) { + this.showErrorOnNonLoadedCommit(); + return; + } + this.loadCommitDetails(elem, DEFAULT_PARENT_INDEX); + } } break; } } }; + const getParentIndexFromElement = (element: Element) => { + const parentIndexAsString = (element as HTMLElement).dataset.parent; + + if(parentIndexAsString === undefined) { + return -1; + } + + return parseInt(parentIndexAsString); + }; + document.body.addEventListener('click', followInternalLink); document.body.addEventListener('contextmenu', (e: MouseEvent) => { @@ -2141,6 +2164,8 @@ class GitGraphView { isInDialog = true; } + const parentIndex = getParentIndexFromElement(eventTarget); + handledEvent(e); contextMenu.show([ [ @@ -2156,6 +2181,13 @@ class GitGraphView { visible: isInternalUrl, onClick: () => followInternalLink(e) }, + { + title: 'View Changes with this Parent', + visible: isInternalUrl && parentIndex !== undefined, + onClick: () => { + this.loadCommitDetails(this.expandedCommit!.commitElem!, parentIndex); + } + }, { title: 'Copy URL to Clipboard', visible: isExternalUrl, @@ -2204,10 +2236,10 @@ class GitGraphView { this.loadCommitComparison(this.expandedCommit.commitElem, eventElem); } } else { - this.loadCommitDetails(eventElem); + this.loadCommitDetails(eventElem, DEFAULT_PARENT_INDEX); } } else { - this.loadCommitDetails(eventElem); + this.loadCommitDetails(eventElem, DEFAULT_PARENT_INDEX); } } }); @@ -2324,15 +2356,40 @@ class GitGraphView { /* Commit Details View */ - public loadCommitDetails(commitElem: HTMLElement) { + public loadCommitDetails(commitElem: HTMLElement, parentIndex: number) { const commit = this.getCommitOfElem(commitElem); - if (commit === null) return; + if (commit === null) { + this.showErrorOnNonLoadedCommit(); + return; + } this.closeCommitDetails(false); this.saveExpandedCommitLoading(parseInt(commitElem.dataset.id!), commit.hash, commitElem, null, null); commitElem.classList.add(CLASS_COMMIT_DETAILS_OPEN); this.renderCommitDetailsView(false); - this.requestCommitDetails(commit.hash, false); + this.requestCommitDetails(commit.hash, false, parentIndex); + } + + private showErrorOnNonLoadedCommit() { + const actionName = this.moreCommitsAvailable ? 'Load More Commits' : null; + const reflogCommitsNotShown = !getIncludeCommitsMentionedByReflogs(this.gitRepos[this.currentRepo].includeCommitsMentionedByReflogs); + + let detailedMessage = 'Possible causes:\n'; + + detailedMessage += '\n• Filtering has been applied. Try removing any filters.\n'; + + if(this.moreCommitsAvailable) { + detailedMessage += '\n• The commit is further down the tree and hasn\'t been loaded yet. Try loading more commits.\n'; + } + + if(reflogCommitsNotShown) { + detailedMessage += '\n• You are trying to see a commit present in the reflog, but not in the normal log (e.g. stash parents). Turn "Include Commits Mentioned By Reflogs" on in the extension settings.\n'; + } + + detailedMessage += '\n'; + + dialog.showError('The commit could not be found in the loaded commits.', + detailedMessage, actionName, () => {this.loadMoreCommits();}); } public closeCommitDetails(saveAndRender: boolean) { @@ -2362,7 +2419,7 @@ class GitGraphView { } } - public showCommitDetails(commitDetails: GG.GitCommitDetails, fileTree: FileTreeFolder, avatar: string | null, codeReview: GG.CodeReview | null, lastViewedFile: string | null, refresh: boolean) { + public showCommitDetails(commitDetails: GG.GitCommitDetails, parentIndex: number, fileTree: FileTreeFolder, avatar: string | null, codeReview: GG.CodeReview | null, lastViewedFile: string | null, refresh: boolean) { const expandedCommit = this.expandedCommit; if (expandedCommit === null || expandedCommit.commitElem === null || expandedCommit.commitHash !== commitDetails.hash || expandedCommit.compareWithHash !== null) return; @@ -2377,6 +2434,7 @@ class GitGraphView { expandedCommit.fileTree = fileTree; GitGraphView.closeCdvContextMenuIfOpen(expandedCommit); } + expandedCommit.parentIndex = parentIndex; expandedCommit.avatar = avatar; expandedCommit.codeReview = codeReview; if (!refresh) { @@ -2455,7 +2513,7 @@ class GitGraphView { if (expandedCommit.commitElem !== null) { this.saveExpandedCommitLoading(expandedCommit.index, expandedCommit.commitHash, expandedCommit.commitElem, null, null); this.renderCommitDetailsView(false); - this.requestCommitDetails(expandedCommit.commitHash, false); + this.requestCommitDetails(expandedCommit.commitHash, false, expandedCommit.parentIndex); } else { this.closeCommitDetails(true); } @@ -2524,11 +2582,17 @@ class GitGraphView { }); const commitDetails = expandedCommit.commitDetails!; const parents = commitDetails.parents.length > 0 - ? commitDetails.parents.map((parent) => { + ? commitDetails.parents.map((parent, parentIndex) => { const escapedParent = escapeHtml(parent); - return typeof this.commitLookup[parent] === 'number' - ? '' + escapedParent + '' - : escapedParent; + const escapedAbbreviatedParent = escapeHtml(abbrevCommit(parent)); + const isComparedToParent = parentIndex + 1 === expandedCommit.parentIndex; + + let parentHtml = '' + escapedAbbreviatedParent + ''; + if(isComparedToParent) { + parentHtml = '' + parentHtml + ''; + } + + return parentHtml; }).join(', ') : 'None'; html += '' @@ -2554,6 +2618,7 @@ class GitGraphView { (codeReviewPossible ? '
' + SVG_ICONS.review + '
' : '') + (!expandedCommit.loading ? '
' + SVG_ICONS.fileTree + '
' + SVG_ICONS.fileList + '
' : '') + (externalDiffPossible ? '
' + SVG_ICONS.linkExternal + '
' : '') + + (expandedCommit.commitDetails && expandedCommit.commitDetails.parents.length > 1 ? '
' + SVG_ICONS.merge + '
' : '') + '
'; elem.innerHTML = isDocked ? html : '
' + html + ''; @@ -2625,6 +2690,39 @@ class GitGraphView { this.changeFileViewType(GG.FileViewType.List); }); + document.getElementById('cdvChooseParent')?.addEventListener('click', (event) => { + // Prevent closing of context menu by the same click + event.stopPropagation(); + + const expandedCommit = this.expandedCommit!; + const currentParentIndex = expandedCommit.parentIndex; + const parents = expandedCommit.commitDetails!.parents; + + const contextMenuItems = parents.map((parent, index) => { + const parentIndex = index + 1; + const parentCommit = this.commitLookup[parent] !== undefined ? this.commits[this.commitLookup[parent]] : undefined; + const subject = parentCommit?.message.split('\n')[0]; + + return { + title: escapeHtml('[' + parentIndex + '] ' + abbrevCommit(parent) + (subject ? ': ' + subject : '')), + visible: true, + checked: parentIndex === currentParentIndex, + onClick: () => { + this.loadCommitDetails(expandedCommit.commitElem!, parentIndex); + } + }; + }); + + const target: ContextMenuTarget & CommitTarget = { + type: TargetType.CommitDetailsView, + hash: expandedCommit.commitHash, + index: this.commitLookup[expandedCommit.commitHash], + elem: document.getElementById('cdvChooseParent')! + }; + + contextMenu.show([contextMenuItems], true, target, event, this.isCdvDocked() ? document.body : this.viewElem); + }); + if (codeReviewPossible) { this.renderCodeReviewBtn(); document.getElementById('cdvCodeReview')!.addEventListener('click', (e) => { @@ -3170,7 +3268,7 @@ window.addEventListener('load', () => { break; case 'commitDetails': if (msg.commitDetails !== null) { - gitGraph.showCommitDetails(msg.commitDetails, gitGraph.createFileTree(msg.commitDetails.fileChanges, msg.codeReview), msg.avatar, msg.codeReview, msg.codeReview !== null ? msg.codeReview.lastViewedFile : null, msg.refresh); + gitGraph.showCommitDetails(msg.commitDetails, msg.parentIndex, gitGraph.createFileTree(msg.commitDetails.fileChanges, msg.codeReview), msg.avatar, msg.codeReview, msg.codeReview !== null ? msg.codeReview.lastViewedFile : null, msg.refresh); } else { gitGraph.closeCommitDetails(true); dialog.showError('Unable to load Commit Details', msg.error, null, null); diff --git a/web/utils.ts b/web/utils.ts index a63b64c8..9bdfb30b 100644 --- a/web/utils.ts +++ b/web/utils.ts @@ -27,6 +27,7 @@ const SVG_ICONS = { openFolder: '', closedFolder: '', file: '', + merge: '', // The SVG icons below are custom made arrowDown: '', @@ -100,6 +101,8 @@ const CSS_PROP_LIMIT_GRAPH_WIDTH = '--limitGraphWidth'; const ATTR_ERROR = 'data-error'; +const DEFAULT_PARENT_INDEX = 1; + /* General Helpers */