Skip to content

Commit 96c0c5b

Browse files
Copilotalexr00
andcommitted
Implement command to open commit changes in multi diff editor
Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com>
1 parent 9e9ea9e commit 96c0c5b

File tree

4 files changed

+127
-27
lines changed

4 files changed

+127
-27
lines changed

src/commands.ts

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
import * as pathLib from 'path';
88
import * as vscode from 'vscode';
9-
import { Repository } from './api/api';
109
import { GitErrorCodes } from './api/api1';
1110
import { CommentReply, findActiveHandler, resolveCommentHandler } from './commentHandlerResolver';
1211
import { COPILOT_LOGINS } from './common/copilot';
@@ -56,7 +55,7 @@ const DISCARD_CHANGES = vscode.l10n.t('Discard changes');
5655
* @param repository The git repository with uncommitted changes
5756
* @returns Promise<boolean> true if user chose to proceed (after staging/discarding), false if cancelled
5857
*/
59-
async function handleUncommittedChanges(repository: Repository): Promise<boolean> {
58+
async function handleUncommittedChanges(repository: any): Promise<boolean> {
6059
const hasWorkingTreeChanges = repository.state.workingTreeChanges.length > 0;
6160
const hasIndexChanges = repository.state.indexChanges.length > 0;
6261

@@ -521,19 +520,19 @@ export function registerCommands(
521520
);
522521
}
523522

524-
function isSourceControl(x: any): x is Repository {
523+
function isSourceControl(x: any): x is any {
525524
return !!x?.rootUri;
526525
}
527526

528527
context.subscriptions.push(
529528
vscode.commands.registerCommand(
530529
'pr.create',
531-
async (args?: { repoPath: string; compareBranch: string } | Repository) => {
530+
async (args?: { repoPath: string; compareBranch: string } | any) => {
532531
// The arguments this is called with are either from the SCM view, or manually passed.
533532
if (isSourceControl(args)) {
534533
(await chooseReviewManager(args.rootUri.fsPath))?.createPullRequest();
535534
} else {
536-
(await chooseReviewManager(args?.repoPath))?.createPullRequest(args?.compareBranch);
535+
(await chooseReviewManager((args as any)?.repoPath))?.createPullRequest((args as any)?.compareBranch);
537536
}
538537
},
539538
),
@@ -542,7 +541,7 @@ export function registerCommands(
542541
context.subscriptions.push(
543542
vscode.commands.registerCommand(
544543
'pr.pushAndCreate',
545-
async (args?: any | Repository) => {
544+
async (args?: any) => {
546545
if (isSourceControl(args)) {
547546
const reviewManager = await chooseReviewManager(args.rootUri.fsPath);
548547
const folderManager = reposManager.getManagerForFile(args.rootUri);
@@ -577,7 +576,7 @@ export function registerCommands(
577576
}
578577

579578
let pullRequestModel: PullRequestModel;
580-
let repository: Repository | undefined;
579+
let repository: any | undefined;
581580

582581
if (pr instanceof PRNode || pr instanceof RepositoryChangesNode) {
583582
pullRequestModel = pr.pullRequestModel;
@@ -654,6 +653,79 @@ export function registerCommands(
654653
}),
655654
);
656655

656+
context.subscriptions.push(
657+
vscode.commands.registerCommand('pr.openCommitChanges', async (commitShaOrUrl: string, folderManager?: FolderRepositoryManager) => {
658+
try {
659+
// Extract commit SHA from GitHub URL if needed
660+
let commitSha: string;
661+
if (commitShaOrUrl.includes('github.com') && commitShaOrUrl.includes('/commit/')) {
662+
const match = commitShaOrUrl.match(/\/commit\/([a-f0-9]{7,40})/);
663+
if (!match) {
664+
vscode.window.showErrorMessage(vscode.l10n.t('Invalid commit URL: {0}', commitShaOrUrl));
665+
return;
666+
}
667+
commitSha = match[1];
668+
} else {
669+
commitSha = commitShaOrUrl;
670+
}
671+
672+
// Use provided folder manager or try to find one for the active workspace
673+
if (!folderManager) {
674+
folderManager = reposManager.folderManagers.find(fm => fm.repository.rootUri.fsPath === vscode.workspace.workspaceFolders?.[0]?.uri.fsPath);
675+
if (!folderManager) {
676+
vscode.window.showErrorMessage(vscode.l10n.t('No repository found to show commit changes.'));
677+
return;
678+
}
679+
}
680+
681+
const repository = folderManager.repository;
682+
683+
// Get the changes for this commit
684+
const changes = await repository.diffBetween(commitSha + '^', commitSha);
685+
686+
if (changes.length === 0) {
687+
vscode.window.showInformationMessage(vscode.l10n.t('No file changes found in commit {0}', commitSha.substring(0, 7)));
688+
return;
689+
}
690+
691+
// Build arguments for vscode.changes command
692+
const args: [vscode.Uri, vscode.Uri | undefined, vscode.Uri | undefined][] = [];
693+
for (const change of changes) {
694+
// For each changed file, create URIs for before and after
695+
const parentCommit = commitSha + '^';
696+
let beforeUri: vscode.Uri | undefined;
697+
let afterUri: vscode.Uri | undefined;
698+
699+
if (change.status === 7) { // DELETED
700+
// File was deleted, show old version vs empty
701+
beforeUri = change.uri.with({ scheme: 'git', authority: parentCommit });
702+
afterUri = undefined;
703+
} else if (change.status === 1 || change.status === 11) { // INDEX_ADDED || ADDED_BY_US
704+
// File was added, show empty vs new version
705+
beforeUri = undefined;
706+
afterUri = change.uri.with({ scheme: 'git', authority: commitSha });
707+
} else {
708+
// File was modified, show old vs new
709+
beforeUri = change.uri.with({ scheme: 'git', authority: parentCommit });
710+
afterUri = change.uri.with({ scheme: 'git', authority: commitSha });
711+
}
712+
713+
args.push([change.uri, beforeUri, afterUri]);
714+
}
715+
716+
// Send telemetry
717+
folderManager.telemetry.sendTelemetryEvent('pr.openCommitChanges');
718+
719+
// Open multi diff editor
720+
return vscode.commands.executeCommand('vscode.changes', vscode.l10n.t('Changes in Commit {0}', commitSha.substring(0, 7)), args);
721+
722+
} catch (error) {
723+
Logger.error(`Failed to open commit changes: ${formatError(error)}`, 'Commands');
724+
vscode.window.showErrorMessage(vscode.l10n.t('Failed to open commit changes: {0}', formatError(error)));
725+
}
726+
}),
727+
);
728+
657729
let isCheckingOutFromReadonlyFile = false;
658730
context.subscriptions.push(vscode.commands.registerCommand('pr.checkoutFromReadonlyFile', async () => {
659731
const uri = vscode.window.activeTextEditor?.document.uri;

src/github/pullRequestOverview.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
386386
return this.openSessionLog(message);
387387
case 'pr.cancel-coding-agent':
388388
return this.cancelCodingAgent(message);
389+
case 'pr.openCommitChanges':
390+
return this.openCommitChanges(message);
389391
}
390392
}
391393

@@ -525,6 +527,16 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
525527
}
526528
}
527529

530+
private async openCommitChanges(message: IRequestMessage<{ commitShaOrUrl: string }>): Promise<void> {
531+
try {
532+
const { commitShaOrUrl } = message.args;
533+
await vscode.commands.executeCommand('pr.openCommitChanges', commitShaOrUrl, this._folderRepositoryManager);
534+
} catch (error) {
535+
Logger.error(`Failed to open commit changes: ${formatError(error)}`, PullRequestOverviewPanel.ID);
536+
vscode.window.showErrorMessage(vscode.l10n.t('Failed to open commit changes: {0}', formatError(error)));
537+
}
538+
}
539+
528540
private async openChanges(message?: IRequestMessage<{ openToTheSide?: boolean }>): Promise<void> {
529541
const openToTheSide = message?.args?.openToTheSide || false;
530542
return PullRequestModel.openChanges(this._folderRepositoryManager, this._item, openToTheSide);

webviews/common/context.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,8 @@ export class PRContext {
254254

255255
public openSessionLog = (link: SessionLinkInfo, openToTheSide?: boolean) => this.postMessage({ command: 'pr.open-session-log', args: { link, openToTheSide } });
256256

257+
public openCommitChanges = (commitShaOrUrl: string) => this.postMessage({ command: 'pr.openCommitChanges', args: { commitShaOrUrl } });
258+
257259
setPR = (pr: PullRequest) => {
258260
this.pr = pr;
259261
setState(this.pr);

webviews/components/timeline.tsx

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -105,28 +105,37 @@ export const Timeline = ({ events, isIssue }: { events: TimelineEvent[], isIssue
105105

106106
export default Timeline;
107107

108-
const CommitEventView = (event: CommitEvent) => (
109-
<div className="comment-container commit">
110-
<div className="commit-message">
111-
{commitIcon}
112-
{nbsp}
113-
<div className="avatar-container">
114-
<Avatar for={event.author} />
108+
const CommitEventView = (event: CommitEvent) => {
109+
const context = useContext(PullRequestContext);
110+
111+
const handleCommitClick = (e: React.MouseEvent) => {
112+
e.preventDefault();
113+
context.openCommitChanges(event.htmlUrl);
114+
};
115+
116+
return (
117+
<div className="comment-container commit">
118+
<div className="commit-message">
119+
{commitIcon}
120+
{nbsp}
121+
<div className="avatar-container">
122+
<Avatar for={event.author} />
123+
</div>
124+
<div className="message-container">
125+
<a className="message" onClick={handleCommitClick} style={{ cursor: 'pointer' }} title={event.htmlUrl}>
126+
{event.message.substr(0, event.message.indexOf('\n') > -1 ? event.message.indexOf('\n') : event.message.length)}
127+
</a>
128+
</div>
115129
</div>
116-
<div className="message-container">
117-
<a className="message" href={event.htmlUrl} title={event.htmlUrl}>
118-
{event.message.substr(0, event.message.indexOf('\n') > -1 ? event.message.indexOf('\n') : event.message.length)}
130+
<div className="timeline-detail">
131+
<a className="sha" onClick={handleCommitClick} style={{ cursor: 'pointer' }} title={event.htmlUrl}>
132+
{event.sha.slice(0, 7)}
119133
</a>
134+
<Timestamp date={event.committedDate} />
120135
</div>
121136
</div>
122-
<div className="timeline-detail">
123-
<a className="sha" href={event.htmlUrl} title={event.htmlUrl}>
124-
{event.sha.slice(0, 7)}
125-
</a>
126-
<Timestamp date={event.committedDate} />
127-
</div>
128-
</div>
129-
);
137+
);
138+
};
130139

131140
const NewCommitsSinceReviewEventView = () => {
132141
const { gotoChangesSinceReview } = useContext(PullRequestContext);
@@ -300,7 +309,12 @@ function AddReviewSummaryComment() {
300309
const CommentEventView = (event: CommentEvent) => <CommentView headerInEditMode comment={event} />;
301310

302311
const MergedEventView = (event: MergedEvent) => {
303-
const { revert, pr } = useContext(PullRequestContext);
312+
const { revert, pr, openCommitChanges } = useContext(PullRequestContext);
313+
314+
const handleCommitClick = (e: React.MouseEvent) => {
315+
e.preventDefault();
316+
openCommitChanges(event.commitUrl);
317+
};
304318

305319
return (
306320
<div className="comment-container commit">
@@ -313,7 +327,7 @@ const MergedEventView = (event: MergedEvent) => {
313327
<AuthorLink for={event.user} />
314328
<div className="message">
315329
merged commit{nbsp}
316-
<a className="sha" href={event.commitUrl} title={event.commitUrl}>
330+
<a className="sha" onClick={handleCommitClick} style={{ cursor: 'pointer' }} title={event.commitUrl}>
317331
{event.sha.substr(0, 7)}
318332
</a>
319333
{nbsp}

0 commit comments

Comments
 (0)