Skip to content

Commit 6f8b959

Browse files
committed
Shows the item that matches the entered URL (if exists)
(#3543, #3684)
1 parent 45a4648 commit 6f8b959

File tree

3 files changed

+187
-0
lines changed

3 files changed

+187
-0
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import * as assert from 'assert';
2+
import { suite, test } from 'mocha';
3+
import { getPullRequestIdentityValuesFromSearch } from '../pullRequest';
4+
5+
suite('Test GitHub PR URL parsing to identity: getPullRequestIdentityValuesFromSearch()', () => {
6+
function t(message: string, query: string, prNumber: string | undefined, ownerAndRepo?: string) {
7+
assert.deepStrictEqual(
8+
getPullRequestIdentityValuesFromSearch(query),
9+
{
10+
ownerAndRepo: ownerAndRepo,
11+
prNumber: prNumber,
12+
},
13+
`${message} (${JSON.stringify(query)})`,
14+
);
15+
}
16+
17+
test('full URL or without protocol but with domain, should parse to ownerAndRepo and prNumber', () => {
18+
t('full URL', 'https://github.com/eamodio/vscode-gitlens/pull/1', '1', 'eamodio/vscode-gitlens');
19+
t(
20+
'with suffix',
21+
'https://github.com/eamodio/vscode-gitlens/pull/1/files?diff=unified#hello',
22+
'1',
23+
'eamodio/vscode-gitlens',
24+
);
25+
t(
26+
'with query',
27+
'https://github.com/eamodio/vscode-gitlens/pull/1?diff=unified#hello',
28+
'1',
29+
'eamodio/vscode-gitlens',
30+
);
31+
32+
t('with anchor', 'https://github.com/eamodio/vscode-gitlens/pull/1#hello', '1', 'eamodio/vscode-gitlens');
33+
t('a weird suffix', 'https://github.com/eamodio/vscode-gitlens/pull/1-files', '1', 'eamodio/vscode-gitlens');
34+
t('numeric repo name', 'https://github.com/sergeibbb/1/pull/16', '16', 'sergeibbb/1');
35+
36+
t('no protocol with leading slash', '/github.com/sergeibbb/1/pull/16?diff=unified', '16', 'sergeibbb/1');
37+
t('no protocol without leading slash', 'github.com/sergeibbb/1/pull/16/files', '16', 'sergeibbb/1');
38+
});
39+
40+
test('no domain, should parse to ownerAndRepo and prNumber', () => {
41+
t('with leading slash', '/sergeibbb/1/pull/16#hello', '16', 'sergeibbb/1');
42+
t('words in repo name', 'eamodio/vscode-gitlens/pull/1?diff=unified#hello', '1', 'eamodio/vscode-gitlens');
43+
t('numeric repo name', 'sergeibbb/1/pull/16/files', '16', 'sergeibbb/1');
44+
});
45+
46+
test('domain vs. no domain', () => {
47+
t(
48+
'with anchor',
49+
'https://github.com/eamodio/vscode-gitlens/pull/1#hello/sergeibbb/1/pull/16',
50+
'1',
51+
'eamodio/vscode-gitlens',
52+
);
53+
});
54+
55+
test('has "pull/" fragment', () => {
56+
t('with leading slash', '/pull/16/files#hello', '16');
57+
t('without leading slash', 'pull/16?diff=unified#hello', '16');
58+
t('with numeric repo name', '1/pull/16?diff=unified#hello', '16');
59+
t('with double slash', '1//pull/16?diff=unified#hello', '16');
60+
});
61+
62+
test('has "/<num>" fragment', () => {
63+
t('with leading slash', '/16/files#hello', '16');
64+
});
65+
66+
test('is a number', () => {
67+
t('just a number', '16', '16');
68+
t('with a hash', '#16', '16');
69+
});
70+
71+
test('does not match', () => {
72+
t('without leading slash', '16?diff=unified#hello', undefined);
73+
t('with leading hash', '/#16/files#hello', undefined);
74+
t('number is a part of a word', 'hello16', undefined);
75+
t('number is a part of a word', '16hello', undefined);
76+
77+
t('with a number', '1/16?diff=unified#hello', '16');
78+
t('with a number and slash', '/1/16?diff=unified#hello', '1');
79+
t('with a word', 'anything/16?diff=unified#hello', '16');
80+
81+
t('with a wrong character leading to pull', 'sergeibbb/1/-pull/16?diff=unified#hello', '1');
82+
t('with a wrong character leading to pull', 'sergeibbb/1-pull/16?diff=unified#hello', '1');
83+
});
84+
});

src/git/models/pullRequest.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Uri, window } from 'vscode';
22
import { Schemes } from '../../constants';
33
import { Container } from '../../container';
44
import type { RepositoryIdentityDescriptor } from '../../gk/models/repositoryIdentities';
5+
import type { EnrichablePullRequest } from '../../plus/integrations/providers/models';
56
import { formatDate, fromNow } from '../../system/date';
67
import { memoize } from '../../system/decorators/memoize';
78
import type { LeftRightCommitCountResult } from '../gitProvider';
@@ -415,3 +416,60 @@ export async function getOpenedPullRequestRepo(
415416
const repo = await getOrOpenPullRequestRepository(container, pr, { promptIfNeeded: true });
416417
return repo;
417418
}
419+
420+
export type PullRequestURLIdentity = {
421+
ownerAndRepo?: string;
422+
prNumber?: string;
423+
};
424+
425+
export function getPullRequestIdentityValuesFromSearch(search: string): PullRequestURLIdentity {
426+
let ownerAndRepo: string | undefined = undefined;
427+
let prNumber: string | undefined = undefined;
428+
429+
let match = search.match(/([^/]+\/[^/]+)\/pull\/(\d+)/); // with org and rep name
430+
if (match != null) {
431+
ownerAndRepo = match[1];
432+
prNumber = match[2];
433+
}
434+
435+
if (prNumber == null) {
436+
match = search.match(/(?:\/|^)pull\/(\d+)/); // without repo name
437+
if (match != null) {
438+
prNumber = match[1];
439+
}
440+
}
441+
442+
if (prNumber == null) {
443+
match = search.match(/(?:\/)(\d+)/); // any number starting with "/"
444+
if (match != null) {
445+
prNumber = match[1];
446+
}
447+
}
448+
449+
if (prNumber == null) {
450+
match = search.match(/^#?(\d+)$/); // just a number or with a leading "#"
451+
if (match != null) {
452+
prNumber = match[1];
453+
}
454+
}
455+
456+
return { ownerAndRepo: ownerAndRepo, prNumber: prNumber };
457+
}
458+
459+
export function doesPullRequestSatisfyRepositoryURLIdentity(
460+
pr: EnrichablePullRequest | undefined,
461+
{ ownerAndRepo, prNumber }: PullRequestURLIdentity,
462+
): boolean {
463+
if (pr == null) {
464+
return false;
465+
}
466+
const satisfiesPrNumber = prNumber != null && pr.number === parseInt(prNumber, 10);
467+
if (!satisfiesPrNumber) {
468+
return false;
469+
}
470+
const satisfiesOwnerAndRepo = ownerAndRepo != null && pr.repoIdentity.name === ownerAndRepo;
471+
if (!satisfiesOwnerAndRepo) {
472+
return false;
473+
}
474+
return true;
475+
}

src/plus/launchpad/launchpad.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ import { HostingIntegrationId, SelfHostedIntegrationId } from '../../constants.i
4242
import type { LaunchpadTelemetryContext, Source, Sources, TelemetryEvents } from '../../constants.telemetry';
4343
import type { Container } from '../../container';
4444
import { PlusFeatures } from '../../features';
45+
import {
46+
doesPullRequestSatisfyRepositoryURLIdentity,
47+
getPullRequestIdentityValuesFromSearch,
48+
} from '../../git/models/pullRequest';
4549
import type { QuickPickItemOfT } from '../../quickpicks/items/common';
4650
import { createQuickPickItemOfT, createQuickPickSeparator } from '../../quickpicks/items/common';
4751
import type { DirectiveQuickPickItem } from '../../quickpicks/items/directive';
@@ -540,12 +544,53 @@ export class LaunchpadCommand extends QuickCommand<State> {
540544
RefreshQuickInputButton,
541545
],
542546
onDidChangeValue: quickpick => {
547+
const { value } = quickpick;
543548
const hideGroups = Boolean(quickpick.value?.length);
544549

545550
if (groupsHidden !== hideGroups) {
546551
groupsHidden = hideGroups;
547552
quickpick.items = hideGroups ? items.filter(i => !isDirectiveQuickPickItem(i)) : items;
548553
}
554+
const activeLaunchpadItems = quickpick.activeItems.filter(
555+
(i): i is LaunchpadItemQuickPickItem => 'item' in i && !i.alwaysShow,
556+
);
557+
558+
let updated = false;
559+
for (const item of quickpick.items) {
560+
if (item.alwaysShow) {
561+
item.alwaysShow = false;
562+
updated = true;
563+
}
564+
}
565+
if (updated) {
566+
// Force quickpick to update by changing the items object:
567+
quickpick.items = [...quickpick.items];
568+
}
569+
570+
if (!value?.length || activeLaunchpadItems.length) {
571+
// Nothing to search
572+
return true;
573+
}
574+
575+
const prUrlIdentity = getPullRequestIdentityValuesFromSearch(value);
576+
if (prUrlIdentity.prNumber != null) {
577+
const launchpadItems = quickpick.items.filter((i): i is LaunchpadItemQuickPickItem => 'item' in i);
578+
let item = launchpadItems.find(i =>
579+
// perform strict match first
580+
doesPullRequestSatisfyRepositoryURLIdentity(i.item, prUrlIdentity),
581+
);
582+
if (item == null) {
583+
// Haven't found full match, so let's at least find something with the same pr number
584+
item = launchpadItems.find(i => i.item.id === prUrlIdentity.prNumber);
585+
}
586+
if (item != null) {
587+
if (!item.alwaysShow) {
588+
item.alwaysShow = true;
589+
// Force quickpick to update by changing the items object:
590+
quickpick.items = [...quickpick.items];
591+
}
592+
}
593+
}
549594

550595
return true;
551596
},

0 commit comments

Comments
 (0)