@gitkraken/core-gitlens 0.20.1 → 0.21.0

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/dist/git/models/issue.d.ts +30 -1
  3. package/dist/git/models/issue.js +3 -1
  4. package/dist/git/models/issue.js.map +1 -1
  5. package/dist/git/providers/config.d.ts +4 -1
  6. package/dist/git/providers/remotes.d.ts +18 -0
  7. package/dist/git/utils/issue.utils.js +2 -1
  8. package/dist/git/utils/issue.utils.js.map +1 -1
  9. package/dist/git-cli/exec/git.js +1 -1
  10. package/dist/git-cli/exec/git.js.map +1 -1
  11. package/dist/git-cli/providers/config.d.ts +6 -2
  12. package/dist/git-cli/providers/config.js +47 -7
  13. package/dist/git-cli/providers/config.js.map +1 -1
  14. package/dist/git-cli/providers/remotes.d.ts +9 -1
  15. package/dist/git-cli/providers/remotes.js +79 -4
  16. package/dist/git-cli/providers/remotes.js.map +1 -1
  17. package/dist/plus/integrations/models/integration.d.ts +13 -1
  18. package/dist/plus/integrations/models/integration.js +8 -1
  19. package/dist/plus/integrations/models/integration.js.map +1 -1
  20. package/dist/plus/integrations/providers/issueSorts.d.ts +1 -0
  21. package/dist/plus/integrations/providers/linearIssuesEtag.d.ts +20 -4
  22. package/dist/plus/integrations/providers/linearIssuesEtag.js +42 -6
  23. package/dist/plus/integrations/providers/linearIssuesEtag.js.map +1 -1
  24. package/dist/plus/integrations/providers/models.js +25 -13
  25. package/dist/plus/integrations/providers/models.js.map +1 -1
  26. package/dist/plus/integrations/providers/providerErrors.d.ts +2 -0
  27. package/dist/plus/integrations/providers/providerErrors.js +5 -2
  28. package/dist/plus/integrations/providers/providerErrors.js.map +1 -1
  29. package/dist/plus/integrations/reads/drains.d.ts +1 -0
  30. package/dist/plus/integrations/reads/etag.js +3 -1
  31. package/dist/plus/integrations/reads/etag.js.map +1 -1
  32. package/docs/integrations.md +12 -1
  33. package/package.json +2 -2
@@ -1,3 +1,5 @@
1
+ import { isLinearRateLimitedBody } from "./providerErrors.js";
2
+ import { linearSyncedWithEtagKeys } from "../models/integration.js";
1
3
  //#region ../plus/integrations/src/providers/linearIssuesEtag.ts
2
4
  const linearGraphQLUrl = "https://api.linear.app/graphql";
3
5
  /** The most issue numbers one cheap check asks of one team. */
@@ -24,6 +26,17 @@ const linearIssuesEtagQuery = `
24
26
  state {
25
27
  type
26
28
  }
29
+ syncedWith {
30
+ id
31
+ service
32
+ metadata {
33
+ ... on ExternalEntityInfoGithubMetadata {
34
+ owner
35
+ repo
36
+ number
37
+ }
38
+ }
39
+ }
27
40
  }
28
41
  }
29
42
  }
@@ -35,8 +48,12 @@ const linearIssuesEtagQuery = `
35
48
  *
36
49
  * Sent as provider-apis sends every Linear request: the token as the whole `Authorization` header, with no scheme.
37
50
  * GraphQL `errors` reject the whole request, as provider-apis rejects them, but keep the response so a throttling
38
- * code is still classified as one. A next page also rejects it: with every number unique within a team, more
39
- * issues than were asked for means the filter was not applied as written.
51
+ * code is still classified as one. The exception is the one provider-apis makes too: errors that all sit under
52
+ * `syncedWith`, a nullable list, leave every other field whole. An issue whose list Linear failed to resolve falls
53
+ * through to its full read (see {@link toLinearIssueEtagFields}); one whose list resolved but a link's coordinates
54
+ * didn't keys that link without them, as the full read's row carries it. Unlike provider-apis, a throttled response
55
+ * still throws here, so it is classified as a rate limit. A next page also rejects it: with every number unique within
56
+ * a team, more issues than were asked for means the filter was not applied as written.
40
57
  */
41
58
  async function requestLinearIssuesEtagFields(request, accessToken, teamKey, numbers) {
42
59
  const response = await request({
@@ -55,7 +72,8 @@ async function requestLinearIssuesEtagFields(request, accessToken, teamKey, numb
55
72
  })
56
73
  });
57
74
  const body = response.body;
58
- if (body?.errors != null) {
75
+ const syncedWithOnly = body?.data != null && Array.isArray(body.errors) && body.errors.every((e) => typeof e === "object" && e != null && "path" in e && Array.isArray(e.path) && e.path.includes("syncedWith")) && !isLinearRateLimitedBody(body);
76
+ if (body?.errors != null && !syncedWithOnly) {
59
77
  const error = /* @__PURE__ */ new Error(`Linear GraphQL errors: ${JSON.stringify(body.errors)}`);
60
78
  Object.assign(error, { response });
61
79
  throw error;
@@ -67,25 +85,43 @@ async function requestLinearIssuesEtagFields(request, accessToken, teamKey, numb
67
85
  }
68
86
  /**
69
87
  * A cheap-checked issue's change state, as the fields the full row's etag reads. The full read's row is provider-apis'
70
- * Linear issue normalizer (0.65.0) then `fromProviderIssue`, so this mirrors both:
88
+ * Linear issue normalizer (0.65.0, `syncedWith` since GKDEV-3662) then `fromProviderIssue`, so this mirrors both:
71
89
  *
72
90
  * - provider-apis maps `state.type` to a category — `completed` to `DONE`, `canceled` and `duplicate` to `CANCELED`,
73
91
  * `started` to `IN_PROGRESS`, anything else (`backlog`, `unstarted`, `triage`) to `TO_DO`, and no state to none —
74
92
  * and sets `closedDate` from `archivedAt` (a falsy value reads as none);
75
93
  * - `fromProviderIssue` closes an issue that has a `closedDate` or a terminal category, so an archived issue is closed
76
94
  * whatever its state.
95
+ * - provider-apis keeps a `syncedWith` entry for a GitHub link with an id, with its coordinates only when owner, repo
96
+ * and a positive integer number are all there, so the etag lists each such link the same way.
77
97
  *
78
98
  * Throws on a missing or unparseable `updatedAt`, so the target falls through to the full read rather than matching an
79
99
  * etag the full row would compute from a fallback date (`fromProviderIssue` falls back to `closedDate`, then
80
- * `createdDate`).
100
+ * `createdDate`). Throws on a `syncedWith` Linear failed to resolve too: an unknown list must not match a full row
101
+ * read while Linear could resolve it, nor one read while it couldn't, which would keep that row unknown.
81
102
  */
82
103
  function toLinearIssueEtagFields(issue) {
83
104
  const updatedDate = new Date(issue.updatedAt || NaN);
84
105
  if (Number.isNaN(updatedDate.getTime())) throw new Error(`Linear returned no update time for ${issue.identifier}`);
106
+ if (issue.syncedWith == null) throw new Error(`Linear could not resolve the sync links of ${issue.identifier}`);
85
107
  const type = issue.state?.type;
86
108
  return {
87
109
  state: Boolean(issue.archivedAt) || type === "completed" || type === "canceled" || type === "duplicate" ? "closed" : "opened",
88
- updatedDate
110
+ updatedDate,
111
+ syncedWithKeys: linearSyncedWithEtagKeys(issue.syncedWith.flatMap((synced) => {
112
+ if (synced.service !== "github" || !synced.id) return [];
113
+ const { owner, repo, number } = synced.metadata ?? {};
114
+ return [owner && repo && typeof number === "number" && Number.isInteger(number) && number > 0 ? {
115
+ provider: "github",
116
+ id: synced.id,
117
+ owner,
118
+ repo,
119
+ number
120
+ } : {
121
+ provider: "github",
122
+ id: synced.id
123
+ }];
124
+ }))
89
125
  };
90
126
  }
91
127
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"linearIssuesEtag.js","names":[],"sources":["../../../../../plus/integrations/src/providers/linearIssuesEtag.ts"],"sourcesContent":["import type { IssueEtagFields } from '../models/integration.js';\nimport type { ProviderRequestFunction } from './models.js';\n\nconst linearGraphQLUrl = 'https://api.linear.app/graphql';\n\n/** The most issue numbers one cheap check asks of one team. */\nexport const linearIssuesEtagMaxNumbers = 50;\n\n/**\n * Archived issues are left out of Linear's `issues` connection unless asked for, so `includeArchived` keeps an archived\n * issue answerable here as the full read's `issue(id:)` answers it.\n */\nconst linearIssuesEtagQuery = `\n\tquery GetIssuesEtagFields($teamKey: String!, $numbers: [Float!]!) {\n\t\tissues(\n\t\t\tfirst: ${linearIssuesEtagMaxNumbers}\n\t\t\tincludeArchived: true\n\t\t\tfilter: { team: { key: { eq: $teamKey } }, number: { in: $numbers } }\n\t\t) {\n\t\t\tpageInfo {\n\t\t\t\thasNextPage\n\t\t\t}\n\t\t\tnodes {\n\t\t\t\tidentifier\n\t\t\t\tnumber\n\t\t\t\tupdatedAt\n\t\t\t\tarchivedAt\n\t\t\t\tstate {\n\t\t\t\t\ttype\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n`;\n\nexport type LinearIssueEtagNode = {\n\tidentifier: string;\n\tnumber: number;\n\tupdatedAt?: string | null;\n\tarchivedAt?: string | null;\n\tstate?: { type?: string | null } | null;\n};\n\ntype LinearIssuesEtagResponse = {\n\tdata?: {\n\t\tissues?: { pageInfo?: { hasNextPage?: boolean }; nodes?: LinearIssueEtagNode[] } | null;\n\t} | null;\n\terrors?: unknown[];\n};\n\n/**\n * The cheap check behind the batch issue read's etags: the change state of up to {@link linearIssuesEtagMaxNumbers}\n * issues of one team in ONE GraphQL request, where the full read sends provider-apis' `issue(id:)` once per issue.\n * Linear returns only the issues it found, in its own order, so a caller matches them by number, never by position.\n *\n * Sent as provider-apis sends every Linear request: the token as the whole `Authorization` header, with no scheme.\n * GraphQL `errors` reject the whole request, as provider-apis rejects them, but keep the response so a throttling\n * code is still classified as one. A next page also rejects it: with every number unique within a team, more\n * issues than were asked for means the filter was not applied as written.\n */\nexport async function requestLinearIssuesEtagFields(\n\trequest: ProviderRequestFunction,\n\taccessToken: string,\n\tteamKey: string,\n\tnumbers: readonly number[],\n): Promise<LinearIssueEtagNode[]> {\n\tconst response = await request<LinearIssuesEtagResponse | null>({\n\t\turl: linearGraphQLUrl,\n\t\tmethod: 'POST',\n\t\theaders: { Authorization: accessToken, 'Content-Type': 'application/json' },\n\t\tbody: JSON.stringify({ query: linearIssuesEtagQuery, variables: { teamKey: teamKey, numbers: numbers } }),\n\t});\n\n\tconst body = response.body;\n\tif (body?.errors != null) {\n\t\tconst error = new Error(`Linear GraphQL errors: ${JSON.stringify(body.errors)}`);\n\t\tObject.assign(error, { response: response });\n\t\tthrow error;\n\t}\n\n\tconst issues = body?.data?.issues;\n\tif (!Array.isArray(issues?.nodes)) throw new Error('Linear returned no issues');\n\tif (issues.pageInfo?.hasNextPage !== false) throw new Error('Linear returned more issues than were asked for');\n\n\treturn issues.nodes;\n}\n\n/**\n * A cheap-checked issue's change state, as the fields the full row's etag reads. The full read's row is provider-apis'\n * Linear issue normalizer (0.65.0) then `fromProviderIssue`, so this mirrors both:\n *\n * - provider-apis maps `state.type` to a category — `completed` to `DONE`, `canceled` and `duplicate` to `CANCELED`,\n * `started` to `IN_PROGRESS`, anything else (`backlog`, `unstarted`, `triage`) to `TO_DO`, and no state to none —\n * and sets `closedDate` from `archivedAt` (a falsy value reads as none);\n * - `fromProviderIssue` closes an issue that has a `closedDate` or a terminal category, so an archived issue is closed\n * whatever its state.\n *\n * Throws on a missing or unparseable `updatedAt`, so the target falls through to the full read rather than matching an\n * etag the full row would compute from a fallback date (`fromProviderIssue` falls back to `closedDate`, then\n * `createdDate`).\n */\nexport function toLinearIssueEtagFields(issue: LinearIssueEtagNode): IssueEtagFields {\n\tconst updatedDate = new Date(issue.updatedAt || Number.NaN);\n\tif (Number.isNaN(updatedDate.getTime())) {\n\t\tthrow new Error(`Linear returned no update time for ${issue.identifier}`);\n\t}\n\n\tconst type = issue.state?.type;\n\tconst closed = Boolean(issue.archivedAt) || type === 'completed' || type === 'canceled' || type === 'duplicate';\n\treturn { state: closed ? 'closed' : 'opened', updatedDate: updatedDate };\n}\n"],"mappings":";AAGA,MAAM,mBAAmB;;AAGzB,MAAa,6BAA6B;;;;;AAM1C,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgD9B,eAAsB,8BACrB,SACA,aACA,SACA,SACiC;CACjC,MAAM,WAAW,MAAM,QAAyC;EAC/D,KAAK;EACL,QAAQ;EACR,SAAS;GAAE,eAAe;GAAa,gBAAgB;EAAmB;EAC1E,MAAM,KAAK,UAAU;GAAE,OAAO;GAAuB,WAAW;IAAW;IAAkB;GAAQ;EAAE,CAAC;CACzG,CAAC;CAED,MAAM,OAAO,SAAS;CACtB,IAAI,MAAM,UAAU,MAAM;EACzB,MAAM,wBAAQ,IAAI,MAAM,0BAA0B,KAAK,UAAU,KAAK,MAAM,GAAG;EAC/E,OAAO,OAAO,OAAO,EAAY,SAAS,CAAC;EAC3C,MAAM;CACP;CAEA,MAAM,SAAS,MAAM,MAAM;CAC3B,IAAI,CAAC,MAAM,QAAQ,QAAQ,KAAK,GAAG,MAAM,IAAI,MAAM,2BAA2B;CAC9E,IAAI,OAAO,UAAU,gBAAgB,OAAO,MAAM,IAAI,MAAM,iDAAiD;CAE7G,OAAO,OAAO;AACf;;;;;;;;;;;;;;;AAgBA,SAAgB,wBAAwB,OAA6C;CACpF,MAAM,cAAc,IAAI,KAAK,MAAM,aAAa,GAAU;CAC1D,IAAI,OAAO,MAAM,YAAY,QAAQ,CAAC,GACrC,MAAM,IAAI,MAAM,sCAAsC,MAAM,YAAY;CAGzE,MAAM,OAAO,MAAM,OAAO;CAE1B,OAAO;EAAE,OADM,QAAQ,MAAM,UAAU,KAAK,SAAS,eAAe,SAAS,cAAc,SAAS,cAC3E,WAAW;EAAuB;CAAY;AACxE"}
1
+ {"version":3,"file":"linearIssuesEtag.js","names":[],"sources":["../../../../../plus/integrations/src/providers/linearIssuesEtag.ts"],"sourcesContent":["import type { IssueSyncedWith } from '@gitlens/git/models/issue.js';\nimport type { IssueEtagFields } from '../models/integration.js';\nimport { linearSyncedWithEtagKeys } from '../models/integration.js';\nimport type { ProviderRequestFunction } from './models.js';\nimport { isLinearRateLimitedBody } from './providerErrors.js';\n\nconst linearGraphQLUrl = 'https://api.linear.app/graphql';\n\n/** The most issue numbers one cheap check asks of one team. */\nexport const linearIssuesEtagMaxNumbers = 50;\n\n/**\n * Archived issues are left out of Linear's `issues` connection unless asked for, so `includeArchived` keeps an archived\n * issue answerable here as the full read's `issue(id:)` answers it.\n */\nconst linearIssuesEtagQuery = `\n\tquery GetIssuesEtagFields($teamKey: String!, $numbers: [Float!]!) {\n\t\tissues(\n\t\t\tfirst: ${linearIssuesEtagMaxNumbers}\n\t\t\tincludeArchived: true\n\t\t\tfilter: { team: { key: { eq: $teamKey } }, number: { in: $numbers } }\n\t\t) {\n\t\t\tpageInfo {\n\t\t\t\thasNextPage\n\t\t\t}\n\t\t\tnodes {\n\t\t\t\tidentifier\n\t\t\t\tnumber\n\t\t\t\tupdatedAt\n\t\t\t\tarchivedAt\n\t\t\t\tstate {\n\t\t\t\t\ttype\n\t\t\t\t}\n\t\t\t\tsyncedWith {\n\t\t\t\t\tid\n\t\t\t\t\tservice\n\t\t\t\t\tmetadata {\n\t\t\t\t\t\t... on ExternalEntityInfoGithubMetadata {\n\t\t\t\t\t\t\towner\n\t\t\t\t\t\t\trepo\n\t\t\t\t\t\t\tnumber\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n`;\n\nexport type LinearIssueEtagNode = {\n\tidentifier: string;\n\tnumber: number;\n\tupdatedAt?: string | null;\n\tarchivedAt?: string | null;\n\tstate?: { type?: string | null } | null;\n\tsyncedWith?:\n\t\t| {\n\t\t\t\tid?: string | null;\n\t\t\t\tservice?: string | null;\n\t\t\t\tmetadata?: { owner?: string | null; repo?: string | null; number?: number | null } | null;\n\t\t }[]\n\t\t| null;\n};\n\ntype LinearIssuesEtagResponse = {\n\tdata?: {\n\t\tissues?: { pageInfo?: { hasNextPage?: boolean }; nodes?: LinearIssueEtagNode[] } | null;\n\t} | null;\n\terrors?: unknown[];\n};\n\n/**\n * The cheap check behind the batch issue read's etags: the change state of up to {@link linearIssuesEtagMaxNumbers}\n * issues of one team in ONE GraphQL request, where the full read sends provider-apis' `issue(id:)` once per issue.\n * Linear returns only the issues it found, in its own order, so a caller matches them by number, never by position.\n *\n * Sent as provider-apis sends every Linear request: the token as the whole `Authorization` header, with no scheme.\n * GraphQL `errors` reject the whole request, as provider-apis rejects them, but keep the response so a throttling\n * code is still classified as one. The exception is the one provider-apis makes too: errors that all sit under\n * `syncedWith`, a nullable list, leave every other field whole. An issue whose list Linear failed to resolve falls\n * through to its full read (see {@link toLinearIssueEtagFields}); one whose list resolved but a link's coordinates\n * didn't keys that link without them, as the full read's row carries it. Unlike provider-apis, a throttled response\n * still throws here, so it is classified as a rate limit. A next page also rejects it: with every number unique within\n * a team, more issues than were asked for means the filter was not applied as written.\n */\nexport async function requestLinearIssuesEtagFields(\n\trequest: ProviderRequestFunction,\n\taccessToken: string,\n\tteamKey: string,\n\tnumbers: readonly number[],\n): Promise<LinearIssueEtagNode[]> {\n\tconst response = await request<LinearIssuesEtagResponse | null>({\n\t\turl: linearGraphQLUrl,\n\t\tmethod: 'POST',\n\t\theaders: { Authorization: accessToken, 'Content-Type': 'application/json' },\n\t\tbody: JSON.stringify({ query: linearIssuesEtagQuery, variables: { teamKey: teamKey, numbers: numbers } }),\n\t});\n\n\tconst body = response.body;\n\tconst syncedWithOnly =\n\t\tbody?.data != null &&\n\t\tArray.isArray(body.errors) &&\n\t\tbody.errors.every(\n\t\t\te =>\n\t\t\t\ttypeof e === 'object' &&\n\t\t\t\te != null &&\n\t\t\t\t'path' in e &&\n\t\t\t\tArray.isArray(e.path) &&\n\t\t\t\te.path.includes('syncedWith'),\n\t\t) &&\n\t\t!isLinearRateLimitedBody(body);\n\tif (body?.errors != null && !syncedWithOnly) {\n\t\tconst error = new Error(`Linear GraphQL errors: ${JSON.stringify(body.errors)}`);\n\t\tObject.assign(error, { response: response });\n\t\tthrow error;\n\t}\n\n\tconst issues = body?.data?.issues;\n\tif (!Array.isArray(issues?.nodes)) throw new Error('Linear returned no issues');\n\tif (issues.pageInfo?.hasNextPage !== false) throw new Error('Linear returned more issues than were asked for');\n\n\treturn issues.nodes;\n}\n\n/**\n * A cheap-checked issue's change state, as the fields the full row's etag reads. The full read's row is provider-apis'\n * Linear issue normalizer (0.65.0, `syncedWith` since GKDEV-3662) then `fromProviderIssue`, so this mirrors both:\n *\n * - provider-apis maps `state.type` to a category — `completed` to `DONE`, `canceled` and `duplicate` to `CANCELED`,\n * `started` to `IN_PROGRESS`, anything else (`backlog`, `unstarted`, `triage`) to `TO_DO`, and no state to none —\n * and sets `closedDate` from `archivedAt` (a falsy value reads as none);\n * - `fromProviderIssue` closes an issue that has a `closedDate` or a terminal category, so an archived issue is closed\n * whatever its state.\n * - provider-apis keeps a `syncedWith` entry for a GitHub link with an id, with its coordinates only when owner, repo\n * and a positive integer number are all there, so the etag lists each such link the same way.\n *\n * Throws on a missing or unparseable `updatedAt`, so the target falls through to the full read rather than matching an\n * etag the full row would compute from a fallback date (`fromProviderIssue` falls back to `closedDate`, then\n * `createdDate`). Throws on a `syncedWith` Linear failed to resolve too: an unknown list must not match a full row\n * read while Linear could resolve it, nor one read while it couldn't, which would keep that row unknown.\n */\nexport function toLinearIssueEtagFields(issue: LinearIssueEtagNode): IssueEtagFields {\n\tconst updatedDate = new Date(issue.updatedAt || Number.NaN);\n\tif (Number.isNaN(updatedDate.getTime())) {\n\t\tthrow new Error(`Linear returned no update time for ${issue.identifier}`);\n\t}\n\n\tif (issue.syncedWith == null) {\n\t\tthrow new Error(`Linear could not resolve the sync links of ${issue.identifier}`);\n\t}\n\n\tconst type = issue.state?.type;\n\tconst closed = Boolean(issue.archivedAt) || type === 'completed' || type === 'canceled' || type === 'duplicate';\n\treturn {\n\t\tstate: closed ? 'closed' : 'opened',\n\t\tupdatedDate: updatedDate,\n\t\tsyncedWithKeys: linearSyncedWithEtagKeys(\n\t\t\tissue.syncedWith.flatMap((synced): IssueSyncedWith[] => {\n\t\t\t\tif (synced.service !== 'github' || !synced.id) return [];\n\n\t\t\t\tconst { owner, repo, number } = synced.metadata ?? {};\n\t\t\t\treturn [\n\t\t\t\t\towner && repo && typeof number === 'number' && Number.isInteger(number) && number > 0\n\t\t\t\t\t\t? { provider: 'github', id: synced.id, owner: owner, repo: repo, number: number }\n\t\t\t\t\t\t: { provider: 'github', id: synced.id },\n\t\t\t\t];\n\t\t\t}),\n\t\t),\n\t};\n}\n"],"mappings":";;;AAMA,MAAM,mBAAmB;;AAGzB,MAAa,6BAA6B;;;;;AAM1C,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsE9B,eAAsB,8BACrB,SACA,aACA,SACA,SACiC;CACjC,MAAM,WAAW,MAAM,QAAyC;EAC/D,KAAK;EACL,QAAQ;EACR,SAAS;GAAE,eAAe;GAAa,gBAAgB;EAAmB;EAC1E,MAAM,KAAK,UAAU;GAAE,OAAO;GAAuB,WAAW;IAAW;IAAkB;GAAQ;EAAE,CAAC;CACzG,CAAC;CAED,MAAM,OAAO,SAAS;CACtB,MAAM,iBACL,MAAM,QAAQ,QACd,MAAM,QAAQ,KAAK,MAAM,KACzB,KAAK,OAAO,OACX,MACC,OAAO,MAAM,YACb,KAAK,QACL,UAAU,KACV,MAAM,QAAQ,EAAE,IAAI,KACpB,EAAE,KAAK,SAAS,YAAY,CAC9B,KACA,CAAC,wBAAwB,IAAI;CAC9B,IAAI,MAAM,UAAU,QAAQ,CAAC,gBAAgB;EAC5C,MAAM,wBAAQ,IAAI,MAAM,0BAA0B,KAAK,UAAU,KAAK,MAAM,GAAG;EAC/E,OAAO,OAAO,OAAO,EAAY,SAAS,CAAC;EAC3C,MAAM;CACP;CAEA,MAAM,SAAS,MAAM,MAAM;CAC3B,IAAI,CAAC,MAAM,QAAQ,QAAQ,KAAK,GAAG,MAAM,IAAI,MAAM,2BAA2B;CAC9E,IAAI,OAAO,UAAU,gBAAgB,OAAO,MAAM,IAAI,MAAM,iDAAiD;CAE7G,OAAO,OAAO;AACf;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,wBAAwB,OAA6C;CACpF,MAAM,cAAc,IAAI,KAAK,MAAM,aAAa,GAAU;CAC1D,IAAI,OAAO,MAAM,YAAY,QAAQ,CAAC,GACrC,MAAM,IAAI,MAAM,sCAAsC,MAAM,YAAY;CAGzE,IAAI,MAAM,cAAc,MACvB,MAAM,IAAI,MAAM,8CAA8C,MAAM,YAAY;CAGjF,MAAM,OAAO,MAAM,OAAO;CAE1B,OAAO;EACN,OAFc,QAAQ,MAAM,UAAU,KAAK,SAAS,eAAe,SAAS,cAAc,SAAS,cAEnF,WAAW;EACd;EACb,gBAAgB,yBACf,MAAM,WAAW,SAAS,WAA8B;GACvD,IAAI,OAAO,YAAY,YAAY,CAAC,OAAO,IAAI,OAAO,CAAC;GAEvD,MAAM,EAAE,OAAO,MAAM,WAAW,OAAO,YAAY,CAAC;GACpD,OAAO,CACN,SAAS,QAAQ,OAAO,WAAW,YAAY,OAAO,UAAU,MAAM,KAAK,SAAS,IACjF;IAAE,UAAU;IAAU,IAAI,OAAO;IAAW;IAAa;IAAc;GAAO,IAC9E;IAAE,UAAU;IAAU,IAAI,OAAO;GAAG,CACxC;EACD,CAAC,CACF;CACD;AACD"}
@@ -625,12 +625,7 @@ function toIssueShape(issue, provider, options) {
625
625
  avatarUrl: assignee.avatarUrl ?? void 0,
626
626
  url: assignee.url ?? void 0
627
627
  })) ?? [],
628
- project: issue.project?.id && issue.project.resourceId && issue.project.namespace ? {
629
- id: issue.project.id,
630
- name: issue.project.name,
631
- resourceId: issue.project.resourceId,
632
- resourceName: issue.project.namespace
633
- } : void 0,
628
+ project: toIssueProject(issue.project, provider),
634
629
  repository: issue.repository?.owner?.login != null ? {
635
630
  owner: issue.repository.owner.login,
636
631
  repo: issue.repository.name,
@@ -648,9 +643,31 @@ function toIssueShape(issue, provider, options) {
648
643
  issueType: issue.type ?? void 0,
649
644
  iterations: toIssueIterations(issue),
650
645
  ...options?.projection != null ? { projection: options.projection } : {},
651
- ...toIssueViewer(issue, options?.viewer)
646
+ ...toIssueViewer(issue, options?.viewer),
647
+ ...issue.syncedWith != null ? { syncedWith: toIssueSyncedWith(issue.syncedWith) } : {}
648
+ };
649
+ }
650
+ function toIssueProject(project, provider) {
651
+ if (!project?.id || !project.resourceId || !project.namespace && !isJiraIntegrationId(provider.id)) return;
652
+ return {
653
+ id: project.id,
654
+ name: project.name,
655
+ resourceId: project.resourceId,
656
+ resourceName: project.namespace ?? ""
652
657
  };
653
658
  }
659
+ function toIssueSyncedWith(syncedWith) {
660
+ return syncedWith.map((synced) => synced.owner != null ? {
661
+ provider: synced.service,
662
+ id: synced.id,
663
+ owner: synced.owner,
664
+ repo: synced.repo,
665
+ number: synced.number
666
+ } : {
667
+ provider: synced.service,
668
+ id: synced.id
669
+ });
670
+ }
654
671
  function toIssueBodyField(field) {
655
672
  return {
656
673
  id: field.referenceName,
@@ -1027,12 +1044,7 @@ function fromProviderIssue(issue, integration, options) {
1027
1044
  name: options.project.name,
1028
1045
  resourceId: options.project.resourceId,
1029
1046
  resourceName: options.project.resourceName
1030
- } : issue.project?.id && issue.project?.resourceId && issue.project?.namespace ? {
1031
- id: issue.project.id,
1032
- name: issue.project.name,
1033
- resourceId: issue.project.resourceId,
1034
- resourceName: issue.project.namespace
1035
- } : void 0, identifier, issue.type ?? void 0, toIssueProviderState(issue.state), integration.id === "jira" ? "jira-wiki" : void 0, toIssueIterations(issue), options?.projection, issue.descriptionField != null ? toIssueBodyField(issue.descriptionField) : void 0, viewer?.viewer, viewer?.viewerUnresolved);
1047
+ } : toIssueProject(issue.project, integration), identifier, issue.type ?? void 0, toIssueProviderState(issue.state), integration.id === "jira" ? "jira-wiki" : void 0, toIssueIterations(issue), options?.projection, issue.descriptionField != null ? toIssueBodyField(issue.descriptionField) : void 0, viewer?.viewer, viewer?.viewerUnresolved, issue.syncedWith != null ? toIssueSyncedWith(issue.syncedWith) : void 0);
1036
1048
  }
1037
1049
  function toProviderPullRequestWithUniqueId(pr) {
1038
1050
  const { reviews, mergeableState, ...providerPr } = toProviderPullRequest(pr);