Skip to content

Commit 0055354

Browse files
Copilotalexr00
andauthored
Fix commit links in PR description to open multi diff editor instead of browser (#7217)
* Initial plan * Implement command to open commit changes in multi diff editor Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Address PR review feedback - move to static method and fix parameter naming Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Remove command registration and call static method directly Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Conditionally use href for commit links when PR not checked out Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Use review scheme URIs in openCommitChanges method Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Fix renames * Replace ~1 syntax with actual parent commit SHA lookup Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Use the correct path * nit * Add OpenCommitChangesArgs type to common/views.ts and use it in pullRequestOverview and context Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * nit --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com>
1 parent aab9a21 commit 0055354

File tree

7 files changed

+136
-29
lines changed

7 files changed

+136
-29
lines changed

common/views.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -169,10 +169,14 @@ export interface TitleAndDescriptionResult {
169169
description: string | undefined;
170170
}
171171

172-
export interface CloseResult {
173-
state: GithubItemStateEnum;
174-
commentEvent?: CommentEvent;
175-
closeEvent: ClosedEvent;
176-
}
177-
172+
export interface CloseResult {
173+
state: GithubItemStateEnum;
174+
commentEvent?: CommentEvent;
175+
closeEvent: ClosedEvent;
176+
}
177+
178+
export interface OpenCommitChangesArgs {
179+
commitSha: string;
180+
}
181+
178182
// #endregion

src/common/uri.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,13 @@ export namespace DataUri {
323323
}
324324
}
325325

326+
/**
327+
* @param fileName The repo relative path to the file
328+
*/
329+
export function reviewPath(fileName: string, commitSha: string) {
330+
return vscode.Uri.parse(pathUtils.posix.join(`commit~${commitSha.substr(0, 8)}`, fileName));
331+
}
332+
326333
export function toReviewUri(
327334
uri: vscode.Uri,
328335
filePath: string | undefined,

src/github/pullRequestModel.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ import * as vscode from 'vscode';
1111
import { Repository } from '../api/api';
1212
import { COPILOT_ACCOUNTS, DiffSide, IComment, IReviewThread, SubjectType, ViewedState } from '../common/comment';
1313
import { getModifiedContentFromDiffHunk, parseDiff } from '../common/diffHunk';
14+
import { commands } from '../common/executeCommands';
1415
import { GitChangeType, InMemFileChange, SlimFileChange } from '../common/file';
1516
import { GitHubRef } from '../common/githubRef';
1617
import Logger from '../common/logger';
1718
import { Remote } from '../common/remote';
1819
import { ITelemetry } from '../common/telemetry';
1920
import { ClosedEvent, EventType, ReviewEvent } from '../common/timelineEvent';
20-
import { resolvePath, Schemes, toPRUri, toReviewUri } from '../common/uri';
21+
import { resolvePath, reviewPath, Schemes, toPRUri, toReviewUri } from '../common/uri';
2122
import { formatError, isDescendant } from '../common/utils';
2223
import { InMemFileChangeModel, RemoteFileChangeModel } from '../view/fileChangeModel';
2324
import { OctokitCommon } from './common';
@@ -1190,6 +1191,64 @@ export class PullRequestModel extends IssueModel<PullRequest> implements IPullRe
11901191
return vscode.commands.executeCommand('vscode.changes', vscode.l10n.t('Changes in Pull Request #{0}', pullRequestModel.number), args);
11911192
}
11921193

1194+
static async openCommitChanges(folderManager: FolderRepositoryManager, commitSha: string) {
1195+
try {
1196+
// Get the repository from the folder manager
1197+
const repository = folderManager.repository;
1198+
if (!repository) {
1199+
vscode.window.showErrorMessage(vscode.l10n.t('No repository found'));
1200+
return;
1201+
}
1202+
1203+
// Get the commit to find its parent
1204+
const commit = await repository.getCommit(commitSha);
1205+
if (!commit.parents || commit.parents.length === 0) {
1206+
vscode.window.showErrorMessage(vscode.l10n.t('Commit {0} has no parent', commitSha.substring(0, 7)));
1207+
return;
1208+
}
1209+
const parentSha = commit.parents[0];
1210+
1211+
// Get the changes between the commit and its parent
1212+
const changes = await repository.diffBetween(parentSha, commitSha);
1213+
if (!changes || changes.length === 0) {
1214+
vscode.window.showInformationMessage(vscode.l10n.t('No changes found in commit {0}', commitSha.substring(0, 7)));
1215+
return;
1216+
}
1217+
1218+
// Create URI pairs for the multi diff editor using review scheme
1219+
const args: [vscode.Uri, vscode.Uri | undefined, vscode.Uri | undefined][] = [];
1220+
for (const change of changes) {
1221+
const rightRelativePath = path.relative(repository.rootUri.fsPath, change.uri.fsPath);
1222+
const rightPath = reviewPath(rightRelativePath, commitSha);
1223+
let rightUri = toReviewUri(rightPath, rightRelativePath, undefined, commitSha, false, { base: false }, repository.rootUri);
1224+
1225+
const leftRelativePath = path.relative(repository.rootUri.fsPath, change.originalUri.fsPath);
1226+
const leftPath = reviewPath(leftRelativePath, parentSha);
1227+
let leftUri = toReviewUri(leftPath, (change.status === GitChangeType.RENAME) ? path.relative(repository.rootUri.fsPath, change.originalUri.fsPath) : leftRelativePath, undefined, parentSha, false, { base: true }, repository.rootUri);
1228+
1229+
if (change.status === GitChangeType.ADD) {
1230+
// For added files, show against empty
1231+
args.push([rightUri, undefined, rightUri]);
1232+
} else if (change.status === GitChangeType.DELETE) {
1233+
// For deleted files, show old version against empty
1234+
args.push([rightPath, leftUri, undefined]);
1235+
} else {
1236+
args.push([rightUri, leftUri, rightUri]);
1237+
}
1238+
}
1239+
1240+
/* __GDPR__
1241+
"pr.openCommitChanges" : {}
1242+
*/
1243+
folderManager.telemetry.sendTelemetryEvent('pr.openCommitChanges');
1244+
1245+
return commands.executeCommand('vscode.changes', vscode.l10n.t('Changes in Commit {0}', commitSha.substring(0, 7)), args);
1246+
} catch (error) {
1247+
const errorMessage = error instanceof Error ? error.message : String(error);
1248+
vscode.window.showErrorMessage(vscode.l10n.t('Failed to open commit changes: {0}', errorMessage));
1249+
}
1250+
}
1251+
11931252
static async openDiffFromComment(
11941253
folderManager: FolderRepositoryManager,
11951254
pullRequestModel: PullRequestModel,

src/github/pullRequestOverview.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
'use strict';
66

77
import * as vscode from 'vscode';
8+
import { OpenCommitChangesArgs } from '../../common/views';
89
import { openPullRequestOnGitHub } from '../commands';
910
import { IComment } from '../common/comment';
1011
import { copilotEventToStatus, CopilotPRStatus, mostRecentCopilotEvent } from '../common/copilot';
@@ -384,6 +385,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
384385
return this.openSessionLog(message);
385386
case 'pr.cancel-coding-agent':
386387
return this.cancelCodingAgent(message);
388+
case 'pr.openCommitChanges':
389+
return this.openCommitChanges(message);
387390
}
388391
}
389392

@@ -532,6 +535,16 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
532535
}
533536
}
534537

538+
private async openCommitChanges(message: IRequestMessage<OpenCommitChangesArgs>): Promise<void> {
539+
try {
540+
const { commitSha } = message.args;
541+
await PullRequestModel.openCommitChanges(this._folderRepositoryManager, commitSha);
542+
} catch (error) {
543+
Logger.error(`Failed to open commit changes: ${formatError(error)}`, PullRequestOverviewPanel.ID);
544+
vscode.window.showErrorMessage(vscode.l10n.t('Failed to open commit changes: {0}', formatError(error)));
545+
}
546+
}
547+
535548
private async openChanges(message?: IRequestMessage<{ openToTheSide?: boolean }>): Promise<void> {
536549
const openToTheSide = message?.args?.openToTheSide || false;
537550
return PullRequestModel.openChanges(this._folderRepositoryManager, this._item, openToTheSide);

src/view/treeNodes/commitNode.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import * as path from 'path';
76
import * as vscode from 'vscode';
87
import { getGitChangeType } from '../../common/diffHunk';
98
import { FILE_LIST_LAYOUT, PR_SETTINGS_NAMESPACE } from '../../common/settingKeys';
10-
import { DataUri, toReviewUri } from '../../common/uri';
9+
import { DataUri, reviewPath, toReviewUri } from '../../common/uri';
1110
import { dateFromNow } from '../../common/utils';
1211
import { OctokitCommon } from '../../github/common';
1312
import { FolderRepositoryManager } from '../../github/folderRepositoryManager';
@@ -58,7 +57,7 @@ export class CommitNode extends TreeNode implements vscode.TreeItem {
5857

5958
const fileChangeNodes = fileChanges.map(change => {
6059
const fileName = change.filename!;
61-
const uri = vscode.Uri.parse(path.posix.join(`commit~${this.commit.sha.substr(0, 8)}`, fileName));
60+
const uri = reviewPath(fileName, this.commit.sha);
6261
const changeModel = new GitFileChangeModel(
6362
this.pullRequestManager,
6463
this.pullRequest,

webviews/common/context.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { createContext } from 'react';
7-
import { CloseResult } from '../../common/views';
7+
import { CloseResult, OpenCommitChangesArgs } from '../../common/views';
88
import { IComment } from '../../src/common/comment';
99
import { EventType, ReviewEvent, SessionLinkInfo, TimelineEvent } from '../../src/common/timelineEvent';
1010
import { IProjectItem, MergeMethod, ReadyForReview } from '../../src/github/interface';
@@ -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 = (commitSha: string) => this.postMessage({ command: 'pr.openCommitChanges', args: { commitSha } as OpenCommitChangesArgs });
258+
257259
setPR = (pr: PullRequest) => {
258260
this.pr = pr;
259261
setState(this.pr);

webviews/components/timeline.tsx

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -105,28 +105,51 @@ 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+
const pr = context.pr;
111+
112+
const handleCommitClick = (e: React.MouseEvent) => {
113+
if (pr.isCurrentlyCheckedOut) {
114+
e.preventDefault();
115+
context.openCommitChanges(event.sha);
116+
}
117+
// If not checked out, let the default href behavior proceed
118+
};
119+
120+
return (
121+
<div className="comment-container commit">
122+
<div className="commit-message">
123+
{commitIcon}
124+
{nbsp}
125+
<div className="avatar-container">
126+
<Avatar for={event.author} />
127+
</div>
128+
<div className="message-container">
129+
<a
130+
className="message"
131+
onClick={handleCommitClick}
132+
href={pr.isCurrentlyCheckedOut ? undefined : event.htmlUrl}
133+
title={event.htmlUrl}
134+
>
135+
{event.message.substr(0, event.message.indexOf('\n') > -1 ? event.message.indexOf('\n') : event.message.length)}
136+
</a>
137+
</div>
115138
</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)}
139+
<div className="timeline-detail">
140+
<a
141+
className="sha"
142+
onClick={handleCommitClick}
143+
href={pr.isCurrentlyCheckedOut ? undefined : event.htmlUrl}
144+
title={event.htmlUrl}
145+
>
146+
{event.sha.slice(0, 7)}
119147
</a>
148+
<Timestamp date={event.committedDate} />
120149
</div>
121150
</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-
);
151+
);
152+
};
130153

131154
const NewCommitsSinceReviewEventView = () => {
132155
const { gotoChangesSinceReview, pr } = useContext(PullRequestContext);

0 commit comments

Comments
 (0)