@gitkraken/core-gitlens 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -2
- package/dist/git/remotes/azure-devops.js +3 -3
- package/dist/git/remotes/azure-devops.js.map +1 -1
- package/dist/plus/integrations/authentication/configuredIntegrationService.js +9 -2
- package/dist/plus/integrations/authentication/configuredIntegrationService.js.map +1 -1
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +1 -1
- package/dist/plus/integrations/collectionMetadata.d.ts +48 -3
- package/dist/plus/integrations/collectionMetadata.js +138 -8
- package/dist/plus/integrations/collectionMetadata.js.map +1 -1
- package/dist/plus/integrations/errors.d.ts +3 -1
- package/dist/plus/integrations/errors.js +7 -3
- package/dist/plus/integrations/errors.js.map +1 -1
- package/dist/plus/integrations/index.d.ts +3 -3
- package/dist/plus/integrations/index.js.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +35 -1
- package/dist/plus/integrations/integrationService.js +90 -43
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/lite.js +5 -4
- package/dist/plus/integrations/lite.js.map +1 -1
- package/dist/plus/integrations/manager.d.ts +68 -23
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +31 -15
- package/dist/plus/integrations/models/gitHostIntegration.js +71 -22
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +70 -12
- package/dist/plus/integrations/models/integration.js +56 -1
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/models/issueReads.d.ts +17 -0
- package/dist/plus/integrations/models/issuesIntegration.js +2 -0
- package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
- package/dist/plus/integrations/models/pullRequestReads.d.ts +26 -0
- package/dist/plus/integrations/providers/azure/azure.d.ts +66 -1
- package/dist/plus/integrations/providers/azure/azure.js +192 -12
- package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
- package/dist/plus/integrations/providers/azure/search.d.ts +144 -0
- package/dist/plus/integrations/providers/azure/search.js +250 -0
- package/dist/plus/integrations/providers/azure/search.js.map +1 -0
- package/dist/plus/integrations/providers/azureDevOps.d.ts +73 -36
- package/dist/plus/integrations/providers/azureDevOps.js +191 -122
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOpsServer.d.ts +176 -0
- package/dist/plus/integrations/providers/azureDevOpsServer.js +560 -0
- package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -0
- package/dist/plus/integrations/providers/bitbucket-server/discovery.d.ts +12 -0
- package/dist/plus/integrations/providers/bitbucket-server/discovery.js +128 -0
- package/dist/plus/integrations/providers/bitbucket-server/discovery.js.map +1 -0
- package/dist/plus/integrations/providers/bitbucket-server/models.d.ts +10 -1
- package/dist/plus/integrations/providers/bitbucket-server/models.js +3 -2
- package/dist/plus/integrations/providers/bitbucket-server/models.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.d.ts +58 -0
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js +485 -0
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js.map +1 -0
- package/dist/plus/integrations/providers/bitbucket-server.d.ts +61 -3
- package/dist/plus/integrations/providers/bitbucket-server.js +114 -20
- package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket.d.ts +9 -1
- package/dist/plus/integrations/providers/bitbucket.js +13 -2
- package/dist/plus/integrations/providers/bitbucket.js.map +1 -1
- package/dist/plus/integrations/providers/github.d.ts +6 -6
- package/dist/plus/integrations/providers/github.js +35 -34
- package/dist/plus/integrations/providers/github.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab.d.ts +6 -4
- package/dist/plus/integrations/providers/gitlab.js +32 -28
- package/dist/plus/integrations/providers/gitlab.js.map +1 -1
- package/dist/plus/integrations/providers/issueSorts.d.ts +1 -0
- package/dist/plus/integrations/providers/jira-server.d.ts +9 -0
- package/dist/plus/integrations/providers/jira-server.js +15 -7
- package/dist/plus/integrations/providers/jira-server.js.map +1 -1
- package/dist/plus/integrations/providers/jira.d.ts +9 -0
- package/dist/plus/integrations/providers/jira.js +14 -2
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/linear.js +2 -2
- package/dist/plus/integrations/providers/models.d.ts +10 -1
- package/dist/plus/integrations/providers/models.js +86 -0
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/providers/providersApi.d.ts +10 -0
- package/dist/plus/integrations/providers/providersApi.js +18 -3
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/providers/utils.d.ts +12 -1
- package/dist/plus/integrations/providers/utils.js +36 -6
- package/dist/plus/integrations/providers/utils.js.map +1 -1
- package/dist/plus/integrations/reads/broaden.js +6 -6
- package/dist/plus/integrations/reads/broaden.js.map +1 -1
- package/dist/plus/integrations/reads/context.d.ts +1 -1
- package/dist/plus/integrations/reads/counts.d.ts +18 -4
- package/dist/plus/integrations/reads/counts.js +66 -15
- package/dist/plus/integrations/reads/counts.js.map +1 -1
- package/dist/plus/integrations/reads/cursors.js +1 -1
- package/dist/plus/integrations/reads/drains.d.ts +2 -1
- package/dist/plus/integrations/reads/drains.js +1 -1
- package/dist/plus/integrations/reads/filters.d.ts +1 -1
- package/dist/plus/integrations/reads/filters.js +17 -12
- package/dist/plus/integrations/reads/filters.js.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.js +2 -2
- package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.utils.js +1 -1
- package/dist/plus/integrations/reads/issueBatch.js +1 -1
- package/dist/plus/integrations/reads/issueTracker.js +2 -2
- package/dist/plus/integrations/reads/issues.js +2 -2
- package/dist/plus/integrations/reads/paging.js +1 -1
- package/dist/plus/integrations/reads/resolveRepository.d.ts +12 -11
- package/dist/plus/integrations/reads/resolveRepository.js +110 -19
- package/dist/plus/integrations/reads/resolveRepository.js.map +1 -1
- package/dist/plus/integrations/reads/searchIssues.js +4 -2
- package/dist/plus/integrations/reads/searchIssues.js.map +1 -1
- package/dist/plus/integrations/reads/searchPullRequests.js +9 -3
- package/dist/plus/integrations/reads/searchPullRequests.js.map +1 -1
- package/dist/plus/integrations/reads/sweeps.js +1 -1
- package/dist/plus/integrations/reads/trackerIssue.d.ts +9 -5
- package/dist/plus/integrations/reads/trackerIssue.js +15 -9
- package/dist/plus/integrations/reads/trackerIssue.js.map +1 -1
- package/dist/plus/integrations/reads/warnings.d.ts +6 -2
- package/dist/plus/integrations/reads/warnings.js +15 -19
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/dist/plus/integrations/results.d.ts +84 -6
- package/dist/plus/integrations/results.js +9 -2
- package/dist/plus/integrations/results.js.map +1 -1
- package/dist/plus/integrations/utils/domain.utils.d.ts +19 -0
- package/dist/plus/integrations/utils/domain.utils.js +36 -1
- package/dist/plus/integrations/utils/domain.utils.js.map +1 -1
- package/dist/plus/integrations/utils/integration.utils.d.ts +1 -0
- package/dist/plus/integrations/utils/integration.utils.js +12 -1
- package/dist/plus/integrations/utils/integration.utils.js.map +1 -1
- package/docs/integrations.md +263 -74
- package/docs/kepler-read-api-parity.md +19 -6
- package/package.json +1 -1
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { fnv1aHash64 } from "../../../../utils/hash.js";
|
|
2
|
+
import { parsePageCursor } from "../utils/providerPaging.js";
|
|
3
|
+
import { escapeWiqlString } from "@gitkraken/provider-apis";
|
|
4
|
+
//#region ../plus/integrations/src/providers/azure/search.ts
|
|
5
|
+
/**
|
|
6
|
+
* The most work items one filtered search can reach, however it is paged.
|
|
7
|
+
*
|
|
8
|
+
* Azure DevOps documents 20,000 work items as its query result limit, and may refuse a larger WIQL result set
|
|
9
|
+
* (`VS402337`) rather than truncate it. The page read therefore asks for a window bounded by `$top`, the first
|
|
10
|
+
* 20,000 matches under the requested order. Azure DevOps Server 2020 answers that bounded query with 20,014 matches
|
|
11
|
+
* (it even answered one without `$top`), so the ceiling is this read's, kept at the documented limit.
|
|
12
|
+
*/
|
|
13
|
+
const azureWorkItemSearchResultLimit = 2e4;
|
|
14
|
+
/** The work items one detail read can resolve: `workitemsbatch` rejects more than 200 ids. */
|
|
15
|
+
const azureWorkItemBatchLimit = 200;
|
|
16
|
+
/**
|
|
17
|
+
* A search page size from a caller's `itemsPerPage`, within `[1, max]`. A value that isn't a finite number takes the
|
|
18
|
+
* default: `NaN` would survive the clamp and serve empty pages that never advance.
|
|
19
|
+
*/
|
|
20
|
+
function toAzureSearchPageSize(value, fallback, max) {
|
|
21
|
+
return Math.min(max, Math.max(1, value != null && Number.isFinite(value) ? Math.trunc(value) : fallback));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Each filtered-search relationship as the WIQL clause that expresses it.
|
|
25
|
+
*
|
|
26
|
+
* `@Me` rather than an identity looked up per read: Azure resolves the macro to the identity the credential
|
|
27
|
+
* authenticates as, which is exactly the "current user" the relationship names, and needs no account read.
|
|
28
|
+
* `mentioned` is absent. `@RecentMentions` only covers the last 30 days, so declaring it would serve an arbitrary
|
|
29
|
+
* window under a relationship that promises every mention.
|
|
30
|
+
*/
|
|
31
|
+
const relationshipClauses = {
|
|
32
|
+
authored: "[System.CreatedBy] = @Me",
|
|
33
|
+
assigned: "[System.AssignedTo] = @Me",
|
|
34
|
+
"any-assignee": "[System.AssignedTo] <> ''",
|
|
35
|
+
unassigned: "[System.AssignedTo] = ''"
|
|
36
|
+
};
|
|
37
|
+
/** The relationships {@link toAzureWorkItemSearchWiql} can express. */
|
|
38
|
+
const azureWorkItemSearchRelationships = Object.keys(relationshipClauses);
|
|
39
|
+
/**
|
|
40
|
+
* Each orderable key as its WIQL `ORDER BY` column.
|
|
41
|
+
*
|
|
42
|
+
* A strict subset of the SDK's WIQL columns: `priority` and `resolved` are left out because their columns are
|
|
43
|
+
* process-template fields. On an on-premises collection running the XML process model, a field its process never
|
|
44
|
+
* declared fails the whole query with `TF51005` rather than ordering nothing, so declaring them would turn a
|
|
45
|
+
* working search into a refused one on exactly the installations this search exists for. Every key kept orders a
|
|
46
|
+
* system field, except `closed`, whose column the open/closed filter below already depends on.
|
|
47
|
+
*/
|
|
48
|
+
const sortColumns = {
|
|
49
|
+
"updated:desc": "[System.ChangedDate] Desc",
|
|
50
|
+
"updated:asc": "[System.ChangedDate] Asc",
|
|
51
|
+
"created:desc": "[System.CreatedDate] Desc",
|
|
52
|
+
"created:asc": "[System.CreatedDate] Asc",
|
|
53
|
+
"closed:desc": "[Microsoft.VSTS.Common.ClosedDate] Desc",
|
|
54
|
+
"closed:asc": "[Microsoft.VSTS.Common.ClosedDate] Asc",
|
|
55
|
+
"comments:desc": "[System.CommentCount] Desc",
|
|
56
|
+
"comments:asc": "[System.CommentCount] Asc",
|
|
57
|
+
"title:desc": "[System.Title] Desc",
|
|
58
|
+
"title:asc": "[System.Title] Asc"
|
|
59
|
+
};
|
|
60
|
+
/** The sort keys {@link toAzureWorkItemSearchWiql} can express. */
|
|
61
|
+
const azureWorkItemSearchSorts = Object.keys(sortColumns);
|
|
62
|
+
/**
|
|
63
|
+
* Translates filtered-search criteria into ONE WIQL query over a collection.
|
|
64
|
+
*
|
|
65
|
+
* Relationships are OR-ed inside one parenthesized group rather than run as separate queries, which is what lets a
|
|
66
|
+
* single query both page and count their union exactly: an item matching two relationships is one row, never two.
|
|
67
|
+
* The projects bound the query by name through `[System.TeamProject]`, so a repository scope reaches the work items
|
|
68
|
+
* of its project; with no project the collection itself is the scope.
|
|
69
|
+
*
|
|
70
|
+
* The open/closed split mirrors the SDK's WIQL, so this search and the ordinary project read agree on which work
|
|
71
|
+
* items are open. Free text is a `Contains` substring match on the title, and each label a `Contains` over the
|
|
72
|
+
* tags, which Azure matches against whole tags — AND-ed, like the criteria model's labels.
|
|
73
|
+
*
|
|
74
|
+
* Throws for a relationship or sort key it cannot express. The facade validates both against the declared
|
|
75
|
+
* capability first, so reaching here with one means the declaration and this translation disagree.
|
|
76
|
+
*/
|
|
77
|
+
function toAzureWorkItemSearchWiql(criteria, sort, projects) {
|
|
78
|
+
const clauses = [];
|
|
79
|
+
if (projects?.length) clauses.push(`[System.TeamProject] IN (${projects.map((p) => `'${escapeWiqlString(p)}'`).join(", ")})`);
|
|
80
|
+
switch (criteria?.state ?? "open") {
|
|
81
|
+
case "open":
|
|
82
|
+
clauses.push("[Microsoft.VSTS.Common.ClosedDate] = ''", "[Microsoft.VSTS.Common.ResolvedDate] = ''");
|
|
83
|
+
break;
|
|
84
|
+
case "closed": clauses.push("([Microsoft.VSTS.Common.ClosedDate] <> '' OR [Microsoft.VSTS.Common.ResolvedDate] <> '')");
|
|
85
|
+
}
|
|
86
|
+
const relationships = [...new Set(criteria?.relationships ?? [])];
|
|
87
|
+
if (relationships.length > 0) {
|
|
88
|
+
const expressions = relationships.map((r) => {
|
|
89
|
+
const clause = relationshipClauses[r];
|
|
90
|
+
if (clause == null) throw new Error(`Azure DevOps cannot search work items by the '${r}' relationship`);
|
|
91
|
+
return clause;
|
|
92
|
+
});
|
|
93
|
+
clauses.push(`(${expressions.join(" OR ")})`);
|
|
94
|
+
}
|
|
95
|
+
const text = criteria?.text?.trim();
|
|
96
|
+
if (text) clauses.push(`[System.Title] Contains '${escapeWiqlString(text)}'`);
|
|
97
|
+
for (const label of criteria?.labels ?? []) clauses.push(`[System.Tags] Contains '${escapeWiqlString(label)}'`);
|
|
98
|
+
if (criteria?.updatedAfter != null) clauses.push(`[System.ChangedDate] >= '${toWiqlDate(criteria.updatedAfter, "updatedAfter")}'`);
|
|
99
|
+
if (criteria?.createdAfter != null) clauses.push(`[System.CreatedDate] >= '${toWiqlDate(criteria.createdAfter, "createdAfter")}'`);
|
|
100
|
+
const column = sortColumns[sort];
|
|
101
|
+
if (column == null) throw new Error(`Azure DevOps cannot order a work item search by '${sort}'`);
|
|
102
|
+
return `SELECT [System.Id] FROM WorkItems${clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : ""} ORDER BY ${column}, [System.Id] Desc`;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* A criteria date as the UTC-midnight literal WIQL compares against. A literal with a time is only accepted by a
|
|
106
|
+
* query sent with `timePrecision=true`, which is how the search sends every query: without it WIQL compares whole
|
|
107
|
+
* days in the server's time zone, and the instant would differ from the one the pull request filter applies.
|
|
108
|
+
*/
|
|
109
|
+
function toWiqlDate(value, name) {
|
|
110
|
+
return parseCriteriaDate(value, name).toISOString().replace(".000Z", "Z");
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* A criteria date as UTC midnight.
|
|
114
|
+
*
|
|
115
|
+
* Validated to the `YYYY-MM-DD` the criteria model documents rather than parsed leniently: the WIQL value lands
|
|
116
|
+
* inside a quoted literal, where any other shape is either refused or read in the server's locale, and the pull
|
|
117
|
+
* request filter must apply the same instant the work-item query does.
|
|
118
|
+
*/
|
|
119
|
+
function parseCriteriaDate(value, name) {
|
|
120
|
+
const date = toCriteriaDate(value);
|
|
121
|
+
if (date == null) throw new Error(`Invalid ${name} '${value}'; expected an ISO date (YYYY-MM-DD)`);
|
|
122
|
+
return date;
|
|
123
|
+
}
|
|
124
|
+
function toCriteriaDate(value) {
|
|
125
|
+
const trimmed = value.trim();
|
|
126
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
|
|
127
|
+
const date = match != null ? new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]))) : null;
|
|
128
|
+
return date == null || Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== trimmed ? void 0 : date;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Why a search's dates can't be applied, or `undefined` when they can. The search checks this before any request,
|
|
132
|
+
* so a date it would refuse costs nothing, and in a count refuses only its own scope.
|
|
133
|
+
*/
|
|
134
|
+
function getInvalidAzureSearchDate(criteria) {
|
|
135
|
+
for (const [name, value] of [["updatedAfter", criteria?.updatedAfter], ["createdAfter", criteria?.createdAfter]]) if (value != null && toCriteriaDate(value) == null) return `invalid ${name} '${value}'; expected an ISO date (YYYY-MM-DD)`;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The criteria a pull-request search applies AFTER the provider read, as one predicate.
|
|
139
|
+
*
|
|
140
|
+
* Azure's pull request query takes a creator, a reviewer, a status and a repository, and nothing else, so text,
|
|
141
|
+
* draft state and dates are matched here against each fully drained facet. Applying them to a complete facet is
|
|
142
|
+
* exact rather than a post-filter of a page: nothing the predicate removes was ever counted toward a page or a
|
|
143
|
+
* total, so a count and its list always agree.
|
|
144
|
+
*
|
|
145
|
+
* What each criterion matches is bounded by what the list endpoint returns, which is narrower than the web UI:
|
|
146
|
+
* - text is a case-insensitive substring of the title or of the description — which Azure truncates to its first
|
|
147
|
+
* 400 characters in a pull request list, so a term only found past that is not matched;
|
|
148
|
+
* - `updatedAfter` compares Azure's `updatedDate`, which the SDK derives from the close date, or the creation date
|
|
149
|
+
* for a pull request still open, since Azure reports no last-activity date. An old but active pull request is
|
|
150
|
+
* therefore matched by its creation date.
|
|
151
|
+
*/
|
|
152
|
+
function toAzurePullRequestSearchFilter(criteria) {
|
|
153
|
+
const text = criteria?.text?.trim().toLowerCase();
|
|
154
|
+
const draft = criteria?.draft;
|
|
155
|
+
const updatedAfter = criteria?.updatedAfter != null ? parseCriteriaDate(criteria.updatedAfter, "updatedAfter") : void 0;
|
|
156
|
+
const createdAfter = criteria?.createdAfter != null ? parseCriteriaDate(criteria.createdAfter, "createdAfter") : void 0;
|
|
157
|
+
return (pr) => (!text || pr.title.toLowerCase().includes(text) || (pr.description?.toLowerCase().includes(text) ?? false)) && (draft == null || pr.isDraft === draft) && (updatedAfter == null || pr.updatedDate >= updatedAfter) && (createdAfter == null || pr.createdDate >= createdAfter);
|
|
158
|
+
}
|
|
159
|
+
/** The pull-request sort keys the search can order its merged facets by. */
|
|
160
|
+
const azurePullRequestSearchSorts = [
|
|
161
|
+
"updated:desc",
|
|
162
|
+
"updated:asc",
|
|
163
|
+
"created:desc",
|
|
164
|
+
"created:asc"
|
|
165
|
+
];
|
|
166
|
+
function toAzurePullRequestSearchPosition(pr, sort, identity) {
|
|
167
|
+
return {
|
|
168
|
+
time: (sort.startsWith("created") ? pr.createdDate : pr.updatedDate).getTime(),
|
|
169
|
+
identity
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/** Orders two positions under `sort`, ties broken by identity so no two rows ever compare equal. */
|
|
173
|
+
function compareAzurePullRequestSearchPositions(a, b, sort) {
|
|
174
|
+
if (a.time !== b.time) return sort.endsWith(":asc") ? a.time - b.time : b.time - a.time;
|
|
175
|
+
return a.identity < b.identity ? -1 : a.identity > b.identity ? 1 : 0;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The fingerprint {@link AzurePullRequestSearchCursor.key} carries for a query.
|
|
179
|
+
*
|
|
180
|
+
* It only guards a caller threading a cursor under a different query, and keeps the query itself out of the opaque
|
|
181
|
+
* cursor. It never selects cached results: those are keyed by the full query (see the callers), so a fingerprint
|
|
182
|
+
* collision can at worst accept a cursor, never serve another query's rows.
|
|
183
|
+
*/
|
|
184
|
+
function toAzurePullRequestSearchCursorKey(query) {
|
|
185
|
+
return fnv1aHash64(JSON.stringify(query));
|
|
186
|
+
}
|
|
187
|
+
/** Reads a caller-supplied cursor for the query fingerprinted as `key`; see {@link parseAzureWorkItemSearchCursor}. */
|
|
188
|
+
function parseAzurePullRequestSearchCursor(cursor, key) {
|
|
189
|
+
if (cursor == null) return void 0;
|
|
190
|
+
let parsed;
|
|
191
|
+
try {
|
|
192
|
+
parsed = JSON.parse(cursor);
|
|
193
|
+
} catch {}
|
|
194
|
+
const after = parsed?.after;
|
|
195
|
+
if (parsed?.key !== key || typeof parsed.drain !== "string" || !parsed.drain || !Number.isSafeInteger(parsed.page) || parsed.page < 1 || typeof after?.time !== "number" || !Number.isFinite(after.time) || typeof after.identity !== "string") throw new Error("Pull request search cursor was produced by a different query; restart the read without a cursor");
|
|
196
|
+
return {
|
|
197
|
+
key,
|
|
198
|
+
drain: parsed.drain,
|
|
199
|
+
page: parsed.page,
|
|
200
|
+
after: {
|
|
201
|
+
time: after.time,
|
|
202
|
+
identity: after.identity
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/** The fingerprint {@link AzureWorkItemSearchCursor.key} carries for a query; see {@link toAzurePullRequestSearchCursorKey}. */
|
|
207
|
+
function toAzureWorkItemSearchCursorKey(collection, wiql) {
|
|
208
|
+
return fnv1aHash64(JSON.stringify([collection, wiql]));
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Whether `cursor` starts a work-item search's first page: none at all, or the facade's page-1 marker, which
|
|
212
|
+
* {@link parseAzureWorkItemSearchCursor} reads the same way.
|
|
213
|
+
*/
|
|
214
|
+
function isAzureWorkItemSearchFirstPage(cursor) {
|
|
215
|
+
return cursor == null || parsePageCursor(cursor) === 1;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Reads a caller-supplied cursor for the query fingerprinted as `key`.
|
|
219
|
+
*
|
|
220
|
+
* Returns `undefined` when there is no cursor, and throws when there is one this query can't resume, since treating
|
|
221
|
+
* a foreign or malformed cursor as page 1 would publish the first page's rows as the page the caller asked for.
|
|
222
|
+
*
|
|
223
|
+
* The one cursor it accepts without a fingerprint is the facade's own page marker (`{ value, type: 'page' }`), which
|
|
224
|
+
* `broadenIssues` sends to retry or reach a numbered page it holds no cursor for. It names a position rather than a
|
|
225
|
+
* query, so it is honored as the offset of that page at `pageSize` — page 1 being no cursor at all.
|
|
226
|
+
*/
|
|
227
|
+
function parseAzureWorkItemSearchCursor(cursor, key, pageSize) {
|
|
228
|
+
if (cursor == null) return void 0;
|
|
229
|
+
const pageMarker = parsePageCursor(cursor);
|
|
230
|
+
if (pageMarker != null && Number.isSafeInteger(pageMarker) && pageMarker >= 1) return pageMarker === 1 ? void 0 : {
|
|
231
|
+
key,
|
|
232
|
+
offset: (pageMarker - 1) * pageSize,
|
|
233
|
+
page: pageMarker
|
|
234
|
+
};
|
|
235
|
+
let parsed;
|
|
236
|
+
try {
|
|
237
|
+
parsed = JSON.parse(cursor);
|
|
238
|
+
} catch {}
|
|
239
|
+
if (parsed?.key !== key || typeof parsed.snapshot !== "string" || !parsed.snapshot || !Number.isSafeInteger(parsed.offset) || parsed.offset < 0 || !Number.isSafeInteger(parsed.page) || parsed.page < 1) throw new Error("Work item search cursor was produced by a different query; restart the read without a cursor");
|
|
240
|
+
return {
|
|
241
|
+
key,
|
|
242
|
+
snapshot: parsed.snapshot,
|
|
243
|
+
offset: parsed.offset,
|
|
244
|
+
page: parsed.page
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
//#endregion
|
|
248
|
+
export { azurePullRequestSearchSorts, azureWorkItemBatchLimit, azureWorkItemSearchRelationships, azureWorkItemSearchResultLimit, azureWorkItemSearchSorts, compareAzurePullRequestSearchPositions, getInvalidAzureSearchDate, isAzureWorkItemSearchFirstPage, parseAzurePullRequestSearchCursor, parseAzureWorkItemSearchCursor, toAzurePullRequestSearchCursorKey, toAzurePullRequestSearchFilter, toAzurePullRequestSearchPosition, toAzureSearchPageSize, toAzureWorkItemSearchCursorKey, toAzureWorkItemSearchWiql };
|
|
249
|
+
|
|
250
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.js","names":[],"sources":["../../../../../../plus/integrations/src/providers/azure/search.ts"],"sourcesContent":["import { escapeWiqlString } from '@gitkraken/provider-apis';\nimport type { IssueSearchCriteria, IssueSearchRelationship, IssueSorting } from '@gitlens/git/models/issue.js';\nimport type { PullRequestSearchCriteria, PullRequestSorting } from '@gitlens/git/models/pullRequest.js';\nimport { fnv1aHash64 } from '@gitlens/utils/hash.js';\nimport type { ProviderPullRequest } from '../models.js';\nimport { parsePageCursor } from '../utils/providerPaging.js';\n\n/**\n * The most work items one filtered search can reach, however it is paged.\n *\n * Azure DevOps documents 20,000 work items as its query result limit, and may refuse a larger WIQL result set\n * (`VS402337`) rather than truncate it. The page read therefore asks for a window bounded by `$top`, the first\n * 20,000 matches under the requested order. Azure DevOps Server 2020 answers that bounded query with 20,014 matches\n * (it even answered one without `$top`), so the ceiling is this read's, kept at the documented limit.\n */\nexport const azureWorkItemSearchResultLimit = 20000;\n\n/** The work items one detail read can resolve: `workitemsbatch` rejects more than 200 ids. */\nexport const azureWorkItemBatchLimit = 200;\n\n/**\n * A search page size from a caller's `itemsPerPage`, within `[1, max]`. A value that isn't a finite number takes the\n * default: `NaN` would survive the clamp and serve empty pages that never advance.\n */\nexport function toAzureSearchPageSize(value: number | undefined, fallback: number, max: number): number {\n\tconst size = value != null && Number.isFinite(value) ? Math.trunc(value) : fallback;\n\treturn Math.min(max, Math.max(1, size));\n}\n\n/**\n * Each filtered-search relationship as the WIQL clause that expresses it.\n *\n * `@Me` rather than an identity looked up per read: Azure resolves the macro to the identity the credential\n * authenticates as, which is exactly the \"current user\" the relationship names, and needs no account read.\n * `mentioned` is absent. `@RecentMentions` only covers the last 30 days, so declaring it would serve an arbitrary\n * window under a relationship that promises every mention.\n */\nconst relationshipClauses: Partial<Record<IssueSearchRelationship, string>> = {\n\tauthored: '[System.CreatedBy] = @Me',\n\tassigned: '[System.AssignedTo] = @Me',\n\t'any-assignee': \"[System.AssignedTo] <> ''\",\n\tunassigned: \"[System.AssignedTo] = ''\",\n};\n\n/** The relationships {@link toAzureWorkItemSearchWiql} can express. */\nexport const azureWorkItemSearchRelationships = Object.keys(relationshipClauses) as IssueSearchRelationship[];\n\n/**\n * Each orderable key as its WIQL `ORDER BY` column.\n *\n * A strict subset of the SDK's WIQL columns: `priority` and `resolved` are left out because their columns are\n * process-template fields. On an on-premises collection running the XML process model, a field its process never\n * declared fails the whole query with `TF51005` rather than ordering nothing, so declaring them would turn a\n * working search into a refused one on exactly the installations this search exists for. Every key kept orders a\n * system field, except `closed`, whose column the open/closed filter below already depends on.\n */\nconst sortColumns: Partial<Record<IssueSorting, string>> = {\n\t'updated:desc': '[System.ChangedDate] Desc',\n\t'updated:asc': '[System.ChangedDate] Asc',\n\t'created:desc': '[System.CreatedDate] Desc',\n\t'created:asc': '[System.CreatedDate] Asc',\n\t'closed:desc': '[Microsoft.VSTS.Common.ClosedDate] Desc',\n\t'closed:asc': '[Microsoft.VSTS.Common.ClosedDate] Asc',\n\t'comments:desc': '[System.CommentCount] Desc',\n\t'comments:asc': '[System.CommentCount] Asc',\n\t'title:desc': '[System.Title] Desc',\n\t'title:asc': '[System.Title] Asc',\n};\n\n/** The sort keys {@link toAzureWorkItemSearchWiql} can express. */\nexport const azureWorkItemSearchSorts = Object.keys(sortColumns) as IssueSorting[];\n\n/**\n * Translates filtered-search criteria into ONE WIQL query over a collection.\n *\n * Relationships are OR-ed inside one parenthesized group rather than run as separate queries, which is what lets a\n * single query both page and count their union exactly: an item matching two relationships is one row, never two.\n * The projects bound the query by name through `[System.TeamProject]`, so a repository scope reaches the work items\n * of its project; with no project the collection itself is the scope.\n *\n * The open/closed split mirrors the SDK's WIQL, so this search and the ordinary project read agree on which work\n * items are open. Free text is a `Contains` substring match on the title, and each label a `Contains` over the\n * tags, which Azure matches against whole tags — AND-ed, like the criteria model's labels.\n *\n * Throws for a relationship or sort key it cannot express. The facade validates both against the declared\n * capability first, so reaching here with one means the declaration and this translation disagree.\n */\nexport function toAzureWorkItemSearchWiql(\n\tcriteria: IssueSearchCriteria | undefined,\n\tsort: IssueSorting,\n\tprojects: readonly string[] | undefined,\n): string {\n\tconst clauses: string[] = [];\n\tif (projects?.length) {\n\t\tclauses.push(`[System.TeamProject] IN (${projects.map(p => `'${escapeWiqlString(p)}'`).join(', ')})`);\n\t}\n\n\tswitch (criteria?.state ?? 'open') {\n\t\tcase 'open':\n\t\t\tclauses.push(\"[Microsoft.VSTS.Common.ClosedDate] = ''\", \"[Microsoft.VSTS.Common.ResolvedDate] = ''\");\n\t\t\tbreak;\n\t\tcase 'closed':\n\t\t\tclauses.push(\"([Microsoft.VSTS.Common.ClosedDate] <> '' OR [Microsoft.VSTS.Common.ResolvedDate] <> '')\");\n\t\t\tbreak;\n\t\tcase 'all':\n\t\t\tbreak;\n\t}\n\n\tconst relationships = [...new Set(criteria?.relationships ?? [])];\n\tif (relationships.length > 0) {\n\t\tconst expressions = relationships.map(r => {\n\t\t\tconst clause = relationshipClauses[r];\n\t\t\tif (clause == null) throw new Error(`Azure DevOps cannot search work items by the '${r}' relationship`);\n\n\t\t\treturn clause;\n\t\t});\n\t\tclauses.push(`(${expressions.join(' OR ')})`);\n\t}\n\n\tconst text = criteria?.text?.trim();\n\tif (text) {\n\t\tclauses.push(`[System.Title] Contains '${escapeWiqlString(text)}'`);\n\t}\n\tfor (const label of criteria?.labels ?? []) {\n\t\tclauses.push(`[System.Tags] Contains '${escapeWiqlString(label)}'`);\n\t}\n\tif (criteria?.updatedAfter != null) {\n\t\tclauses.push(`[System.ChangedDate] >= '${toWiqlDate(criteria.updatedAfter, 'updatedAfter')}'`);\n\t}\n\tif (criteria?.createdAfter != null) {\n\t\tclauses.push(`[System.CreatedDate] >= '${toWiqlDate(criteria.createdAfter, 'createdAfter')}'`);\n\t}\n\n\tconst column = sortColumns[sort];\n\tif (column == null) throw new Error(`Azure DevOps cannot order a work item search by '${sort}'`);\n\n\t// `[System.Id]` makes the order total, so a work item tying on the sort column can't cross a page boundary\n\t// between two requests and be served twice or never.\n\tconst where = clauses.length > 0 ? ` WHERE ${clauses.join(' AND ')}` : '';\n\treturn `SELECT [System.Id] FROM WorkItems${where} ORDER BY ${column}, [System.Id] Desc`;\n}\n\n/**\n * A criteria date as the UTC-midnight literal WIQL compares against. A literal with a time is only accepted by a\n * query sent with `timePrecision=true`, which is how the search sends every query: without it WIQL compares whole\n * days in the server's time zone, and the instant would differ from the one the pull request filter applies.\n */\nfunction toWiqlDate(value: string, name: string): string {\n\treturn parseCriteriaDate(value, name).toISOString().replace('.000Z', 'Z');\n}\n\n/**\n * A criteria date as UTC midnight.\n *\n * Validated to the `YYYY-MM-DD` the criteria model documents rather than parsed leniently: the WIQL value lands\n * inside a quoted literal, where any other shape is either refused or read in the server's locale, and the pull\n * request filter must apply the same instant the work-item query does.\n */\nfunction parseCriteriaDate(value: string, name: string): Date {\n\tconst date = toCriteriaDate(value);\n\tif (date == null) throw new Error(`Invalid ${name} '${value}'; expected an ISO date (YYYY-MM-DD)`);\n\n\treturn date;\n}\n\nfunction toCriteriaDate(value: string): Date | undefined {\n\tconst trimmed = value.trim();\n\tconst match = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(trimmed);\n\tconst date = match != null ? new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]))) : null;\n\treturn date == null || Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== trimmed\n\t\t? undefined\n\t\t: date;\n}\n\n/**\n * Why a search's dates can't be applied, or `undefined` when they can. The search checks this before any request,\n * so a date it would refuse costs nothing, and in a count refuses only its own scope.\n */\nexport function getInvalidAzureSearchDate(\n\tcriteria: { updatedAfter?: string; createdAfter?: string } | undefined,\n): string | undefined {\n\tfor (const [name, value] of [\n\t\t['updatedAfter', criteria?.updatedAfter],\n\t\t['createdAfter', criteria?.createdAfter],\n\t] as const) {\n\t\tif (value != null && toCriteriaDate(value) == null) {\n\t\t\treturn `invalid ${name} '${value}'; expected an ISO date (YYYY-MM-DD)`;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * The criteria a pull-request search applies AFTER the provider read, as one predicate.\n *\n * Azure's pull request query takes a creator, a reviewer, a status and a repository, and nothing else, so text,\n * draft state and dates are matched here against each fully drained facet. Applying them to a complete facet is\n * exact rather than a post-filter of a page: nothing the predicate removes was ever counted toward a page or a\n * total, so a count and its list always agree.\n *\n * What each criterion matches is bounded by what the list endpoint returns, which is narrower than the web UI:\n * - text is a case-insensitive substring of the title or of the description — which Azure truncates to its first\n * 400 characters in a pull request list, so a term only found past that is not matched;\n * - `updatedAfter` compares Azure's `updatedDate`, which the SDK derives from the close date, or the creation date\n * for a pull request still open, since Azure reports no last-activity date. An old but active pull request is\n * therefore matched by its creation date.\n */\nexport function toAzurePullRequestSearchFilter(\n\tcriteria: PullRequestSearchCriteria | undefined,\n): (pr: ProviderPullRequest) => boolean {\n\tconst text = criteria?.text?.trim().toLowerCase();\n\tconst draft = criteria?.draft;\n\tconst updatedAfter =\n\t\tcriteria?.updatedAfter != null ? parseCriteriaDate(criteria.updatedAfter, 'updatedAfter') : undefined;\n\tconst createdAfter =\n\t\tcriteria?.createdAfter != null ? parseCriteriaDate(criteria.createdAfter, 'createdAfter') : undefined;\n\n\treturn pr =>\n\t\t(!text || pr.title.toLowerCase().includes(text) || (pr.description?.toLowerCase().includes(text) ?? false)) &&\n\t\t(draft == null || pr.isDraft === draft) &&\n\t\t(updatedAfter == null || pr.updatedDate >= updatedAfter) &&\n\t\t(createdAfter == null || pr.createdDate >= createdAfter);\n}\n\n/** The pull-request sort keys the search can order its merged facets by. */\nexport const azurePullRequestSearchSorts: PullRequestSorting[] = [\n\t'updated:desc',\n\t'updated:asc',\n\t'created:desc',\n\t'created:asc',\n];\n\n/**\n * A pull request's position in the merged search result: the sort date plus the identity that breaks ties.\n *\n * The identity makes the order TOTAL, which is what lets a cursor resume strictly after the last row served rather\n * than at a numeric offset. An offset would skip or repeat rows whenever a pull request changes state or date\n * between two pages; a keyset position only moves past rows that sort before it.\n */\nexport interface AzurePullRequestSearchPosition {\n\ttime: number;\n\tidentity: string;\n}\n\nexport function toAzurePullRequestSearchPosition(\n\tpr: ProviderPullRequest,\n\tsort: PullRequestSorting,\n\tidentity: string,\n): AzurePullRequestSearchPosition {\n\treturn { time: (sort.startsWith('created') ? pr.createdDate : pr.updatedDate).getTime(), identity: identity };\n}\n\n/** Orders two positions under `sort`, ties broken by identity so no two rows ever compare equal. */\nexport function compareAzurePullRequestSearchPositions(\n\ta: AzurePullRequestSearchPosition,\n\tb: AzurePullRequestSearchPosition,\n\tsort: PullRequestSorting,\n): number {\n\tif (a.time !== b.time) return sort.endsWith(':asc') ? a.time - b.time : b.time - a.time;\n\n\treturn a.identity < b.identity ? -1 : a.identity > b.identity ? 1 : 0;\n}\n\n/**\n * Where a filtered pull-request search resumes: after `after`, in the drain its first page read, under the query\n * `key` fingerprints. Bound to its query and served only from its own drain for the same reasons\n * {@link AzureWorkItemSearchCursor} is bound to its query and snapshot.\n */\nexport interface AzurePullRequestSearchCursor {\n\tkey: string;\n\tdrain: string;\n\tpage: number;\n\tafter: AzurePullRequestSearchPosition;\n}\n\n/**\n * The fingerprint {@link AzurePullRequestSearchCursor.key} carries for a query.\n *\n * It only guards a caller threading a cursor under a different query, and keeps the query itself out of the opaque\n * cursor. It never selects cached results: those are keyed by the full query (see the callers), so a fingerprint\n * collision can at worst accept a cursor, never serve another query's rows.\n */\nexport function toAzurePullRequestSearchCursorKey(query: unknown): string {\n\treturn fnv1aHash64(JSON.stringify(query));\n}\n\n/** Reads a caller-supplied cursor for the query fingerprinted as `key`; see {@link parseAzureWorkItemSearchCursor}. */\nexport function parseAzurePullRequestSearchCursor(\n\tcursor: string | undefined,\n\tkey: string,\n): AzurePullRequestSearchCursor | undefined {\n\tif (cursor == null) return undefined;\n\n\tlet parsed: Partial<AzurePullRequestSearchCursor> | undefined;\n\ttry {\n\t\tparsed = JSON.parse(cursor) as Partial<AzurePullRequestSearchCursor>;\n\t} catch {}\n\n\tconst after = parsed?.after;\n\tif (\n\t\tparsed?.key !== key ||\n\t\ttypeof parsed.drain !== 'string' ||\n\t\t!parsed.drain ||\n\t\t!Number.isSafeInteger(parsed.page) ||\n\t\tparsed.page! < 1 ||\n\t\ttypeof after?.time !== 'number' ||\n\t\t!Number.isFinite(after.time) ||\n\t\ttypeof after.identity !== 'string'\n\t) {\n\t\tthrow new Error(\n\t\t\t'Pull request search cursor was produced by a different query; restart the read without a cursor',\n\t\t);\n\t}\n\n\treturn { key: key, drain: parsed.drain, page: parsed.page!, after: { time: after.time, identity: after.identity } };\n}\n\n/**\n * Where a filtered work-item search resumes: an offset into the id snapshot its first page queried, the id of that\n * snapshot, and the fingerprint of the query that produced it.\n *\n * A continuation is only served from ITS snapshot, where the offset is exact. Each first page queries a snapshot of\n * its own, so a pagination of the same query started later never replaces the one an earlier pagination is reading.\n * The snapshot is kept for as long as the pagination keeps reading it; once it is gone the continuation is REFUSED\n * rather than resumed against a fresh query, since under an `updated` order a re-query can move an unseen work item\n * ahead of the offset, where it would be skipped without anything saying so. A cursor without a `snapshot` is the\n * facade's own page marker, which names a page by number with no snapshot behind it and is read against a fresh\n * query as exactly that.\n *\n * The fingerprint binds the cursor to its query (scope, criteria and order) without publishing any of them inside\n * the opaque cursor. A cursor for another query is REFUSED rather than silently restarted: this read is cursor-only,\n * so a restart would be served under the page number the caller supplied alongside it — page 1's rows labelled as\n * page N.\n */\nexport interface AzureWorkItemSearchCursor {\n\tkey: string;\n\tsnapshot?: string;\n\toffset: number;\n\tpage: number;\n}\n\n/** The fingerprint {@link AzureWorkItemSearchCursor.key} carries for a query; see {@link toAzurePullRequestSearchCursorKey}. */\nexport function toAzureWorkItemSearchCursorKey(collection: string, wiql: string): string {\n\treturn fnv1aHash64(JSON.stringify([collection, wiql]));\n}\n\n/**\n * Whether `cursor` starts a work-item search's first page: none at all, or the facade's page-1 marker, which\n * {@link parseAzureWorkItemSearchCursor} reads the same way.\n */\nexport function isAzureWorkItemSearchFirstPage(cursor: string | undefined): boolean {\n\treturn cursor == null || parsePageCursor(cursor) === 1;\n}\n\n/**\n * Reads a caller-supplied cursor for the query fingerprinted as `key`.\n *\n * Returns `undefined` when there is no cursor, and throws when there is one this query can't resume, since treating\n * a foreign or malformed cursor as page 1 would publish the first page's rows as the page the caller asked for.\n *\n * The one cursor it accepts without a fingerprint is the facade's own page marker (`{ value, type: 'page' }`), which\n * `broadenIssues` sends to retry or reach a numbered page it holds no cursor for. It names a position rather than a\n * query, so it is honored as the offset of that page at `pageSize` — page 1 being no cursor at all.\n */\nexport function parseAzureWorkItemSearchCursor(\n\tcursor: string | undefined,\n\tkey: string,\n\tpageSize: number,\n): AzureWorkItemSearchCursor | undefined {\n\tif (cursor == null) return undefined;\n\n\tconst pageMarker = parsePageCursor(cursor);\n\tif (pageMarker != null && Number.isSafeInteger(pageMarker) && pageMarker >= 1) {\n\t\treturn pageMarker === 1 ? undefined : { key: key, offset: (pageMarker - 1) * pageSize, page: pageMarker };\n\t}\n\n\tlet parsed: Partial<AzureWorkItemSearchCursor> | undefined;\n\ttry {\n\t\tparsed = JSON.parse(cursor) as Partial<AzureWorkItemSearchCursor>;\n\t} catch {}\n\n\tif (\n\t\tparsed?.key !== key ||\n\t\ttypeof parsed.snapshot !== 'string' ||\n\t\t!parsed.snapshot ||\n\t\t!Number.isSafeInteger(parsed.offset) ||\n\t\tparsed.offset! < 0 ||\n\t\t!Number.isSafeInteger(parsed.page) ||\n\t\tparsed.page! < 1\n\t) {\n\t\tthrow new Error('Work item search cursor was produced by a different query; restart the read without a cursor');\n\t}\n\n\treturn { key: key, snapshot: parsed.snapshot, offset: parsed.offset!, page: parsed.page! };\n}\n"],"mappings":"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"}
|
|
@@ -1,27 +1,37 @@
|
|
|
1
|
-
import { Emitter } from "../../../utils/event.js";
|
|
2
1
|
import { ResourceDescriptor } from "../../../git/models/resourceDescriptor.js";
|
|
3
2
|
import { IssueOrPullRequest, IssueOrPullRequestState, IssueOrPullRequestType } from "../../../git/models/issueOrPullRequest.js";
|
|
4
|
-
import { Issue, IssueShape } from "../../../git/models/issue.js";
|
|
3
|
+
import { Issue as Issue$1, IssueShape } from "../../../git/models/issue.js";
|
|
5
4
|
import { PullRequest, PullRequestFilter, PullRequestMergeMethod, PullRequestStateFilter } from "../../../git/models/pullRequest.js";
|
|
6
|
-
import { Account, UnidentifiedAuthor } from "../../../git/models/author.js";
|
|
5
|
+
import { Account as Account$1, UnidentifiedAuthor } from "../../../git/models/author.js";
|
|
7
6
|
import { DefaultBranch } from "../../../git/models/defaultBranch.js";
|
|
8
7
|
import { RepositoryMetadata } from "../../../git/models/repositoryMetadata.js";
|
|
9
8
|
import { AuthenticationSessionLike, ProviderAuthenticationSession, TokenWithInfo } from "../authentication/models.js";
|
|
10
9
|
import { GitCloudHostIntegrationId, GitSelfManagedHostIntegrationId } from "../constants.js";
|
|
11
|
-
import {
|
|
10
|
+
import { ProviderOrganization, ProviderWarningCause, ProviderWarningScope } from "../results.js";
|
|
11
|
+
import { ProviderRefusal } from "../collectionMetadata.js";
|
|
12
12
|
import { AccountWideIssuesResult, SearchMyIssuesOptions } from "../models/issueReads.js";
|
|
13
13
|
import { GitHostIntegration, SearchMyPullRequestsOptions, SearchPullRequestsOptions } from "../models/gitHostIntegration.js";
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { AzureProjectDescriptor, AzureProjectInputDescriptor, AzureRemoteRepositoryDescriptor, AzureRepositoryDescriptor } from "./azure/models.js";
|
|
18
|
-
import { IntegrationConnectionChangeEvent } from "../integrationService.js";
|
|
19
|
-
import { IntegrationAuthenticationService } from "../authentication/integrationAuthenticationService.js";
|
|
14
|
+
import "../models/integration.js";
|
|
15
|
+
import { ProviderApiCollectionResult, ProviderApiPagedResult, ProviderAzureResource, ProviderHierarchyResult, ProviderPullRequest, ProviderRepository } from "./models.js";
|
|
16
|
+
import { AzureOrganizationDescriptor, AzureProjectDescriptor, AzureProjectInputDescriptor, AzureRemoteRepositoryDescriptor, AzureRepositoryDescriptor } from "./azure/models.js";
|
|
20
17
|
import { IntegrationAuthenticationProviderDescriptor } from "../authentication/integrationAuthenticationProvider.js";
|
|
21
|
-
import {
|
|
18
|
+
import { CollectionScopeFailure } from "@gitkraken/provider-apis";
|
|
22
19
|
//#region ../plus/integrations/src/providers/azureDevOps.d.ts
|
|
20
|
+
export declare function getAzureRepositoryIdentity(repo: Pick<AzureRepositoryDescriptor, 'owner' | 'name' | 'project'>): {
|
|
21
|
+
resourceName: string;
|
|
22
|
+
projectName?: string;
|
|
23
|
+
repositoryName: string;
|
|
24
|
+
};
|
|
25
|
+
export declare function getAzureRepositoryApiBaseUrl(baseUrl: string, repo: Pick<AzureRepositoryDescriptor, 'owner' | 'virtualDirectory'>): string;
|
|
26
|
+
/**
|
|
27
|
+
* Whether two collection or project names name the same scope. Azure DevOps treats them case-insensitively, so a
|
|
28
|
+
* name typed by hand must still select the collection or project discovery reported.
|
|
29
|
+
*/
|
|
30
|
+
export declare function sameAzureName(a: string, b: string): boolean;
|
|
31
|
+
/** The distinct names in `names`, compared as Azure compares them, keeping the first spelling of each. */
|
|
32
|
+
export declare function uniqueAzureNames(names: readonly string[]): string[];
|
|
23
33
|
export declare abstract class AzureDevOpsIntegrationBase<TIntegrationId extends GitCloudHostIntegrationId.AzureDevOps | GitSelfManagedHostIntegrationId.AzureDevOpsServer, TRepositoryDescriptor extends AzureRepositoryDescriptor = AzureRepositoryDescriptor> extends GitHostIntegration<TIntegrationId, TRepositoryDescriptor> {
|
|
24
|
-
protected abstract
|
|
34
|
+
protected abstract apiBaseUrlFor(session: ProviderAuthenticationSession): string;
|
|
25
35
|
protected getApiOptions(session: ProviderAuthenticationSession, doNotConvertToPat?: boolean): {
|
|
26
36
|
tokenWithInfo: TokenWithInfo<TIntegrationId>;
|
|
27
37
|
options: {
|
|
@@ -29,11 +39,55 @@ export declare abstract class AzureDevOpsIntegrationBase<TIntegrationId extends
|
|
|
29
39
|
baseUrl?: string;
|
|
30
40
|
};
|
|
31
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* The options for a request addressed below one collection (an Azure DevOps Services organization).
|
|
44
|
+
*
|
|
45
|
+
* The same as {@link getApiOptions} unless a self-managed address already names the collection, which requests
|
|
46
|
+
* append themselves: see the server override, which keeps it from being applied twice.
|
|
47
|
+
*/
|
|
48
|
+
protected getCollectionApiOptions(session: ProviderAuthenticationSession, _collection: string): {
|
|
49
|
+
isPAT: boolean;
|
|
50
|
+
baseUrl?: string;
|
|
51
|
+
};
|
|
52
|
+
/** The base a request or link below `collection` appends the collection to; see {@link getCollectionApiOptions}. */
|
|
53
|
+
protected collectionApiBaseUrl(session: ProviderAuthenticationSession, _collection: string): string;
|
|
54
|
+
/** Reads the organizations (Azure DevOps Server: the collections) the account can see. */
|
|
55
|
+
protected requestResourcesForUser(session: ProviderAuthenticationSession, userId: string): Promise<ProviderAzureResource[] | undefined>;
|
|
56
|
+
/**
|
|
57
|
+
* The identity id a pull-request creator/reviewer filter must carry for `collection`.
|
|
58
|
+
*
|
|
59
|
+
* On Azure DevOps Services that is the account id, one per user. The server override resolves it per collection,
|
|
60
|
+
* since Azure DevOps Server gives one person a different id in each collection than at the server level.
|
|
61
|
+
*/
|
|
62
|
+
protected getFilterUserId(session: ProviderAuthenticationSession, _collection: string): Promise<string | undefined>;
|
|
63
|
+
/**
|
|
64
|
+
* What the discovery caches (account, organizations, projects, and their stored copies) are keyed by. Azure DevOps
|
|
65
|
+
* Services has a single address, so the credential alone identifies what it discovers; the server override adds
|
|
66
|
+
* the installation address.
|
|
67
|
+
*/
|
|
68
|
+
protected discoveryKey(session: ProviderAuthenticationSession): string;
|
|
32
69
|
private _accounts;
|
|
33
|
-
protected getProviderCurrentAccount(session: ProviderAuthenticationSession): Promise<Account | undefined>;
|
|
34
|
-
protected _requestForCurrentUser(session: ProviderAuthenticationSession): Promise<Account | undefined>;
|
|
70
|
+
protected getProviderCurrentAccount(session: ProviderAuthenticationSession): Promise<Account$1 | undefined>;
|
|
71
|
+
protected _requestForCurrentUser(session: ProviderAuthenticationSession): Promise<Account$1 | undefined>;
|
|
72
|
+
/**
|
|
73
|
+
* The profile request the account cache would otherwise answer; see `IntegrationBase.validateCredential`. No
|
|
74
|
+
* account proves nothing, since Azure DevOps Server's request answers every failure but a refusal that way.
|
|
75
|
+
*/
|
|
76
|
+
protected validateCredential(session: ProviderAuthenticationSession): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Names a confirmed credential's refusal from the answers Azure DevOps gives, captured live (#5890):
|
|
79
|
+
* - `VS403463` in the explanation is a Conditional Access policy;
|
|
80
|
+
* - a typed `UnauthorizedRequestException` body is an organization or project the account has no access to;
|
|
81
|
+
* - a `401` to an OAuth token that explains nothing (sent as Basic), or only `TF400813` (sent as Bearer), is an
|
|
82
|
+
* organization whose "Third-party application access via OAuth" policy is off. It is the same answer an
|
|
83
|
+
* expired token gets, which is why this is only asked once the credential passed. The policy does not govern
|
|
84
|
+
* personal access tokens, so a PAT is never given it.
|
|
85
|
+
*
|
|
86
|
+
* Any other explanation is left unnamed, so the warning keeps Azure's own words rather than a guess.
|
|
87
|
+
*/
|
|
88
|
+
protected describeRefusal(session: ProviderAuthenticationSession, refusal: ProviderRefusal, scope: ProviderWarningScope): ProviderWarningCause | undefined;
|
|
35
89
|
private _organizations;
|
|
36
|
-
|
|
90
|
+
protected getProviderResourcesForUser(session: ProviderAuthenticationSession, force?: boolean): Promise<AzureOrganizationDescriptor[] | undefined>;
|
|
37
91
|
private _projects;
|
|
38
92
|
/**
|
|
39
93
|
* Discovers (and caches) each resource's projects. Only resources whose drain completed cleanly are cached;
|
|
@@ -42,7 +96,7 @@ export declare abstract class AzureDevOpsIntegrationBase<TIntegrationId extends
|
|
|
42
96
|
* caller can surface the incomplete project set (a whole org's PRs/issues silently missing otherwise) as a
|
|
43
97
|
* scope-aware warning + `fetchFailed` rather than an all-pages success over a hole.
|
|
44
98
|
*/
|
|
45
|
-
|
|
99
|
+
protected getProviderProjectsForResources(session: ProviderAuthenticationSession, resources: AzureOrganizationDescriptor[], force?: boolean, failures?: CollectionScopeFailure[]): Promise<ProviderApiCollectionResult<AzureProjectDescriptor>>;
|
|
46
100
|
private getRepoDescriptorsForProjects;
|
|
47
101
|
protected getProviderOrganizationsForUser(session: ProviderAuthenticationSession): Promise<ProviderHierarchyResult<ProviderOrganization> | undefined>;
|
|
48
102
|
protected getProviderProjectsForOrg(session: ProviderAuthenticationSession, org?: string): Promise<ProviderHierarchyResult<ProviderOrganization> | undefined>;
|
|
@@ -66,13 +120,13 @@ export declare abstract class AzureDevOpsIntegrationBase<TIntegrationId extends
|
|
|
66
120
|
}): Promise<UnidentifiedAuthor | undefined>;
|
|
67
121
|
protected getProviderAccountForEmail(_session: AuthenticationSessionLike, _repo: AzureRepositoryDescriptor, _email: string, _options?: {
|
|
68
122
|
avatarSize?: number;
|
|
69
|
-
}): Promise<Account | undefined>;
|
|
123
|
+
}): Promise<Account$1 | undefined>;
|
|
70
124
|
protected getProviderDefaultBranch(session: ProviderAuthenticationSession, repo: AzureRepositoryDescriptor, cancellation?: AbortSignal): Promise<DefaultBranch | undefined>;
|
|
71
125
|
protected getProviderLinkedIssueOrPullRequest(session: ProviderAuthenticationSession, repo: AzureRepositoryDescriptor, { id }: {
|
|
72
126
|
id: string;
|
|
73
127
|
key: string;
|
|
74
128
|
}, type: undefined | IssueOrPullRequestType): Promise<IssueOrPullRequest | undefined>;
|
|
75
|
-
protected getProviderIssue(session: ProviderAuthenticationSession, project: AzureProjectInputDescriptor, id: string): Promise<Issue | undefined>;
|
|
129
|
+
protected getProviderIssue(session: ProviderAuthenticationSession, project: AzureProjectInputDescriptor, id: string): Promise<Issue$1 | undefined>;
|
|
76
130
|
protected getProviderPullRequestForBranch(session: ProviderAuthenticationSession, repo: AzureRepositoryDescriptor, branch: string, _options?: {
|
|
77
131
|
avatarSize?: number;
|
|
78
132
|
include?: IssueOrPullRequestState[];
|
|
@@ -118,24 +172,7 @@ export declare class AzureDevOpsIntegration extends AzureDevOpsIntegrationBase<G
|
|
|
118
172
|
protected readonly key = GitCloudHostIntegrationId.AzureDevOps;
|
|
119
173
|
readonly name: string;
|
|
120
174
|
get domain(): string;
|
|
121
|
-
protected
|
|
122
|
-
}
|
|
123
|
-
export declare class AzureDevOpsServerIntegration extends AzureDevOpsIntegrationBase<GitSelfManagedHostIntegrationId.AzureDevOpsServer> {
|
|
124
|
-
readonly domain: string;
|
|
125
|
-
readonly authProvider: IntegrationAuthenticationProviderDescriptor;
|
|
126
|
-
readonly id = GitSelfManagedHostIntegrationId.AzureDevOpsServer;
|
|
127
|
-
protected readonly key: IntegrationKey<GitSelfManagedHostIntegrationId.AzureDevOpsServer>;
|
|
128
|
-
readonly name: string;
|
|
129
|
-
constructor(ctx: IntegrationServiceContext, authenticationService: IntegrationAuthenticationService, getProvidersApi: () => Promise<ProvidersApi>, didChangeConnection: Emitter<IntegrationConnectionChangeEvent>, domain: string);
|
|
130
|
-
protected get apiBaseUrl(): string;
|
|
131
|
-
protected getApiOptions(session: ProviderAuthenticationSession, doNotConvertToPat?: boolean): {
|
|
132
|
-
tokenWithInfo: TokenWithInfo<GitSelfManagedHostIntegrationId.AzureDevOpsServer>;
|
|
133
|
-
options: {
|
|
134
|
-
isPAT: boolean;
|
|
135
|
-
baseUrl?: string;
|
|
136
|
-
};
|
|
137
|
-
};
|
|
138
|
-
protected _requestForCurrentUser(session: ProviderAuthenticationSession): Promise<Account | undefined>;
|
|
175
|
+
protected apiBaseUrlFor(_session: ProviderAuthenticationSession): string;
|
|
139
176
|
}
|
|
140
177
|
//#endregion
|
|
141
178
|
//# sourceMappingURL=azureDevOps.d.ts.map
|