@framers/agentos-ext-github 0.8.0 → 0.9.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.
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * @fileoverview A repository's documentation as account sources, read through a GitHub App's installation: each
3
- * repository read by its id at its default branch or at a named branch or commit, its tree read once a commit within a
4
- * request budget, the documentation kept by a filter, each file read as raw bytes under a cap, a push compared by the
5
- * tree at its `after` commit, and a link's ref settled where a branch name holds a slash. A selection, an item or a
6
- * call it cannot answer whole is refused before any request.
3
+ * repository read by its id at its default branch or at a named branch or commit, whole or within a folder, its tree
4
+ * read once a commit within a request budget, the documentation kept by a filter, each file read as raw bytes under a
5
+ * cap, a push compared by the tree at its `after` commit, and a link's ref settled as a branch (where its name holds a
6
+ * slash), a tag or a commit. A selection, an item or a call it cannot answer whole is refused before any request.
7
7
  * @module agentos-ext-github/documents/GitHubDocumentsConnector
8
8
  */
9
9
  import { type AccountSourceConnector, type SourceChanges, type SourceGone, type SourceItem, type SourceRead, type SourceSelection } from '@framers/agentos-ext-account-sources';
@@ -17,13 +17,27 @@ export interface LinkedRepository {
17
17
  fullName: string;
18
18
  defaultBranch: string;
19
19
  }
20
- /** What `list` reads: the repositories (`ids`), and the branch or commit a listing is made at. */
20
+ /** What `list` reads: the repositories (`ids`), the branch or commit a listing is made at, and a folder or a file. */
21
21
  export interface GitHubListSelection extends SourceSelection {
22
22
  /**
23
23
  * A branch name, or a commit as 40 hexadecimal characters in either case, the listing is made at: the branch's head,
24
- * or the commit itself. The default branch's head when absent. Given, the selection names one repository.
24
+ * or the commit itself. A tag or a short commit is followed by the commit `resolveRef` settles it to. The default
25
+ * branch's head when absent. Given, the selection names one repository.
25
26
  */
26
27
  at?: string;
28
+ /**
29
+ * A folder of the repository, or a file, by its path with no leading or trailing slash (`resolveRef`'s `path`): the
30
+ * listing keeps the files at it or under it before `maxFiles` counts them, so a folder's files are listed whatever
31
+ * sorts before them, and a `/tree/` address that names a file lists that file. The whole repository when absent or
32
+ * `''`. Given, the selection names one repository.
33
+ */
34
+ path?: string;
35
+ }
36
+ /** What `listing` reads within, and its abort signal. */
37
+ export interface GitHubListingOptions {
38
+ /** A folder or a file, as a selection's `path` names it; the whole repository when absent or `''`. */
39
+ path?: string;
40
+ signal?: AbortSignal;
27
41
  }
28
42
  /**
29
43
  * What `changesSince` compares: one repository (`ids` holds its id alone), the branch or commit the comparison follows
@@ -45,12 +59,19 @@ export interface GitHubChangesSelection extends GitHubListSelection {
45
59
  }
46
60
  /** The ref and the path a link's `<ref>/<path>` text names, as `resolveRef` settles them. */
47
61
  export interface ResolvedRef {
48
- /** The branch's name, or the commit in lower case. */
62
+ /**
63
+ * The branch's name, or a commit of 40 hexadecimal characters in lower case: the commit the link names, whole or by
64
+ * its first characters, or the commit a tag names. `list`, `head` and `changesSince` take it as `at`.
65
+ */
49
66
  ref: string;
50
67
  /** The path under the ref, with no leading or trailing slash; `''` for the ref's root. */
51
68
  path: string;
52
- /** True when `ref` is a commit, which never moves. */
69
+ /** True when `ref` is a commit, which never moves: for a commit, and for a tag, read at its commit. */
53
70
  commit: boolean;
71
+ /** What the link's ref names: a branch (`HEAD` the default branch), a tag or a commit. */
72
+ kind: 'branch' | 'tag' | 'commit';
73
+ /** The ref as the link writes it: the branch's or the tag's name, `HEAD`, or the commit, whole or short, as written. */
74
+ name: string;
54
75
  }
55
76
  /**
56
77
  * A repository's documentation as account sources: an item's id is `<repositoryId>:<path>`, its version
@@ -59,12 +80,14 @@ export interface ResolvedRef {
59
80
  * `GitHubNoAnswer` (`cancelled`) before it checks anything else. What a call cannot answer whole then throws before any
60
81
  * request: a `TypeError` for an id that is not a repository id, a repository named twice, an item id that names no
61
82
  * file, a change set of other than one repository, an `at` that is neither a branch name nor a commit or that names
62
- * other than one repository, and a link's text with an empty, `.` or `..` segment, and an `Error` for a repository
63
- * outside the link. An installation is removed by the app's own call (`uninstallInstallation`), never by a link's
64
- * connector.
83
+ * other than one repository, a `path` with an empty, `.` or `..` segment or beside other than one repository, a link's
84
+ * text with an empty, `.` or `..` segment, and a link's ref that no ref name can begin with, and an `Error` for a
85
+ * repository outside the link. An installation is removed by the app's own call (`uninstallInstallation`), never by a
86
+ * link's connector.
65
87
  *
66
88
  * A listing and a comparison follow the default branch, or the branch or commit a selection's `at` names: a branch's
67
- * head is read when the call is made, a commit is read as it is and never changes.
89
+ * head is read when the call is made, a commit is read as it is and never changes. A selection's `path` narrows them to
90
+ * a folder, or a file, before `maxFiles` counts the files.
68
91
  *
69
92
  * Every read addresses a linked repository by its id (`/repositories/<id>/...`, where GitHub's own redirects for a
70
93
  * renamed or moved repository lead), never by its name. A name can come to mean another repository (a renamed
@@ -112,13 +135,22 @@ export declare class GitHubDocumentsConnector implements AccountSourceConnector
112
135
  * as `after`.
113
136
  */
114
137
  listing(repositoryId: number, commit: string, signal?: AbortSignal): Promise<TreeBlob[]>;
138
+ /**
139
+ * The documentation files of a repository at a commit within a folder, or the one file a path names
140
+ * (`options.path`), kept before `maxFiles` counts them, under `options.signal`; the whole repository for no path or
141
+ * `''`. A path with an empty, `.` or `..` segment, or one that is not text, throws a `TypeError` before any request.
142
+ * A caller keeps it for that commit and hands it to `changesSince` with that commit as `after` and the same `path`.
143
+ */
144
+ listing(repositoryId: number, commit: string, options: GitHubListingOptions): Promise<TreeBlob[]>;
115
145
  /**
116
146
  * The documentation of each selected repository (its id in `ids`) at its default branch's head, or of one repository
117
- * at the branch or commit `at` names: the branch's head, or the commit itself with no branch read. Every id and `at`
118
- * are checked before any repository is read, so a selection is listed whole or not at all: an id that is not a
119
- * repository id, a repository named twice, an `at` that is neither a branch name nor a commit, and an `at` beside
120
- * other than one repository throw a `TypeError`, and a repository outside the link an `Error`. A selection's other
121
- * fields are `changesSince`'s, and a listing reads none of them.
147
+ * at the branch or commit `at` names: the branch's head, or the commit itself with no branch read. With a `path`, the
148
+ * files of one repository at that folder or under it, or the one file it names, kept before `maxFiles` counts them.
149
+ * Every id, `at` and `path` are checked before any repository is read, so a selection is listed whole or not at all:
150
+ * an id that is not a repository id, a repository named twice, an `at` that is neither a branch name nor a commit, a
151
+ * `path` with an empty, `.` or `..` segment or that is not text, and an `at` or a `path` beside other than one
152
+ * repository throw a `TypeError`, and a repository outside the link an `Error`. A selection's other fields are
153
+ * `changesSince`'s, and a listing reads none of them.
122
154
  */
123
155
  list(selection: GitHubListSelection, signal?: AbortSignal): Promise<{
124
156
  items: SourceItem[];
@@ -148,21 +180,60 @@ export declare class GitHubDocumentsConnector implements AccountSourceConnector
148
180
  * followed branch's head, or at the commit. A push to another branch than the one followed answers no change and the
149
181
  * cursor it was given, and so does any push when `at` is a commit, which never changes; without a push, an `after`
150
182
  * naming another commit than `at`'s throws a `TypeError` before any request.
183
+ *
184
+ * With a `path` the comparison is made within that folder, or of the one file it names: the listing at `after` keeps
185
+ * the files there before `maxFiles` counts them, so the folder's files are never answered gone for the files sorting
186
+ * before it, and a `listing` handed in is narrowed to the path (read it with `listing(repositoryId, after, { path })`
187
+ * so the count is the folder's). A listing handed in that holds `maxFiles` files may have been cut before the folder's
188
+ * files, so with a path the folder is read afresh instead of it. A cursor is compared under the path it was answered under: a path in it outside the
189
+ * one given is answered gone.
151
190
  */
152
191
  changesSince(cursor: string | null, selection: GitHubChangesSelection, signal?: AbortSignal): Promise<SourceChanges>;
153
192
  /**
154
193
  * The ref and the path of a folder's or a file's `<ref>/<path>` text (`parseGitHubLink`'s `refAndPath`), whose
155
- * branch name may hold a slash. A first segment of 40 hexadecimal characters is a commit, in lower case, and `HEAD`
156
- * the default branch (git names no branch `HEAD`), each settled with no request. Otherwise one request to GitHub's
157
- * matching references (`/repositories/<id>/git/matching-refs/heads/<first segment>`, under an installation token
158
- * narrowed to the repository), which answers every branch whose name starts with that segment
159
- * (https://docs.github.com/en/rest/git/refs#list-matching-references): the longest branch name that is the text, or
160
- * starts it at a slash, is the ref and the rest the path, and the first segment alone when none is longer. A first
161
- * segment no branch has is answered as the ref all the same, and a listing at it then answers GitHub's 404. A text
162
- * with an empty, `.` or `..` segment throws a `TypeError` that quotes none of it, before any request; GitHub's
163
- * refusal of the read, such as 409 for a repository with no commit, throws `GitHubApiError`.
194
+ * branch or tag name may hold a slash, answered with what the ref is (`kind`) and the name the text writes it by
195
+ * (`name`). A first segment of 40 hexadecimal characters is a commit, in lower case, and `HEAD` the default branch
196
+ * (git names no branch `HEAD`), each settled with no request. Otherwise, under an installation token narrowed to the
197
+ * repository, GitHub's matching references (https://docs.github.com/en/rest/git/refs#list-matching-references), which
198
+ * answer every reference whose name starts with the first segment, are read in turn until one settles it:
199
+ *
200
+ * 1. The branches (`/repositories/<id>/git/matching-refs/heads/<first segment>`) and the tags
201
+ * (`.../matching-refs/tags/<first segment>`): the longest name of either that is the text, or starts it at a slash,
202
+ * is the ref and the rest the path, a branch and a tag of one name settling as the branch. Git keeps a tag under a
203
+ * branch's name beside the branch (`release/2.0` beside `release`), and the longer name is the more of the link it
204
+ * names. The tags are not read when a branch's name is the whole text, since no name is longer. A tag is answered
205
+ * at the commit it names (`commit: true`), as GitHub serves a `/tree/<tag>/` address: a lightweight tag names it,
206
+ * and an annotated tag's object is read for it (https://docs.github.com/en/rest/git/tags#get-a-tag). A tag that
207
+ * names no commit is read as no tag, and the branch, if one matched, settles it.
208
+ * 2. A first segment of 4 to 39 hexadecimal characters, a short commit, through GitHub's commits under its sha media
209
+ * type (https://docs.github.com/en/rest/commits/commits#get-a-commit), which answers the whole commit, or 422 for
210
+ * a prefix no commit has or several have.
211
+ *
212
+ * A first segment neither settles is answered as a branch all the same, and a listing at it then throws GitHub's
213
+ * 404 as `GitHubApiError`; one that is no branch name by git's rules (such as `-x`, `@` or `end.`) throws a
214
+ * `TypeError` instead. A text with an empty, `.` or `..` segment, and one whose first segment no ref name can begin
215
+ * (a control character, a space, `~`, `^`, `:`, `?`, `*`, `[`, `\`, `..` or `@{`, a leading dot or a trailing
216
+ * `.lock`), throw a `TypeError` that quotes none of it, before any request; GitHub's refusal of a read, such as 409 for
217
+ * a repository with no commit, throws `GitHubApiError`.
164
218
  */
165
219
  resolveRef(repositoryId: number, refAndPath: string, signal?: AbortSignal): Promise<ResolvedRef>;
220
+ /**
221
+ * The longest branch or tag (`space`) whose name is `text` or starts it at a slash, from GitHub's matching references
222
+ * for `first`, the text's first segment, with the reference's fields; null for none.
223
+ */
224
+ private longestRef;
225
+ /**
226
+ * The commit a tag's reference names: a lightweight tag's own object, or the object an annotated tag names, read
227
+ * through GitHub's tags and followed through a tag of a tag up to {@link MAX_TAG_DEPTH}, in lower case. Null for a
228
+ * tag that names a tree or a blob, or a longer chain of tags.
229
+ */
230
+ private tagCommit;
231
+ /**
232
+ * The whole commit a short commit names, in lower case, through GitHub's commits under the sha media type, which
233
+ * answers the commit's sha alone; null when GitHub answers 422 or 404, as it does for a prefix no commit has or
234
+ * several have. An answer that is not a commit starting with the prefix throws.
235
+ */
236
+ private commitOf;
166
237
  /** Nothing to end here: an installation is removed by the app's own call (`uninstallInstallation`). */
167
238
  revoke(): Promise<void>;
168
239
  }
@@ -1 +1 @@
1
- {"version":3,"file":"GitHubDocumentsConnector.d.ts","sourceRoot":"","sources":["../../src/documents/GitHubDocumentsConnector.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"GitHubDocumentsConnector.d.ts","sourceRoot":"","sources":["../../src/documents/GitHubDocumentsConnector.ts"],"names":[],"mappings":"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"}
@@ -1,16 +1,24 @@
1
1
  /**
2
2
  * @fileoverview A repository's documentation as account sources, read through a GitHub App's installation: each
3
- * repository read by its id at its default branch or at a named branch or commit, its tree read once a commit within a
4
- * request budget, the documentation kept by a filter, each file read as raw bytes under a cap, a push compared by the
5
- * tree at its `after` commit, and a link's ref settled where a branch name holds a slash. A selection, an item or a
6
- * call it cannot answer whole is refused before any request.
3
+ * repository read by its id at its default branch or at a named branch or commit, whole or within a folder, its tree
4
+ * read once a commit within a request budget, the documentation kept by a filter, each file read as raw bytes under a
5
+ * cap, a push compared by the tree at its `after` commit, and a link's ref settled as a branch (where its name holds a
6
+ * slash), a tag or a commit. A selection, an item or a call it cannot answer whole is refused before any request.
7
7
  * @module agentos-ext-github/documents/GitHubDocumentsConnector
8
8
  */
9
9
  import { ReadCapExceeded, } from '@framers/agentos-ext-account-sources';
10
10
  import { arrayOf, GITHUB_API_BASE, GitHubApiError, githubFetch, GitHubNoAnswer, githubSend, objectOf, repositoryPath, stringIn } from '../app/http.js';
11
- import { DEFAULT_MAX_FILE_BYTES, extensionOf, pathFilter, readTree, SKIPPED_FOLDERS } from './tree.js';
11
+ import { DEFAULT_MAX_FILE_BYTES, DEFAULT_MAX_FILES, extensionOf, pathFilter, pathOf, readTree, SKIPPED_FOLDERS, withinPath, } from './tree.js';
12
12
  /** A commit as a link or a caller names it: 40 hexadecimal characters, in either case. */
13
13
  const COMMIT = /^[0-9a-f]{40}$/i;
14
+ /** A commit named by its first characters, as git shortens it: 4 to 39 hexadecimal characters, in either case. */
15
+ const SHORT_COMMIT = /^[0-9a-f]{4,39}$/i;
16
+ /** GitHub's media type for a commit's sha alone (https://docs.github.com/en/rest/commits/commits#get-a-commit). */
17
+ const COMMIT_SHA_MEDIA_TYPE = 'application/vnd.github.sha';
18
+ /** The most bytes read of a commit's sha alone: 40 characters, and room to spare. */
19
+ const COMMIT_SHA_BYTES = 1_024;
20
+ /** The most annotated tags followed from a tag to the commit it names: a tag of a tag is rare. */
21
+ const MAX_TAG_DEPTH = 8;
14
22
  /** The characters no branch name holds, beside the control characters and the space (`git check-ref-format`). */
15
23
  const NOT_IN_BRANCH = '~^:?*[\\';
16
24
  /**
@@ -30,6 +38,22 @@ function isBranchName(name) {
30
38
  }
31
39
  return name.split('/').every((part) => part !== '' && !part.startsWith('.') && !part.endsWith('.lock'));
32
40
  }
41
+ /**
42
+ * True for a text that may begin a ref's name by git's rules (https://git-scm.com/docs/git-check-ref-format): no
43
+ * control character, space, `~`, `^`, `:`, `?`, `*`, `[` or `\`, no `..` or `@{`, not starting with a dot and not
44
+ * ending in `.lock`. A part of a longer name may end with a dot, and a tag's may start with a dash, which a branch's
45
+ * whole name may not: `isBranchName` reads those.
46
+ */
47
+ function mayBeginRef(segment) {
48
+ if (segment.includes('..') || segment.includes('@{') || segment.startsWith('.') || segment.endsWith('.lock'))
49
+ return false;
50
+ for (let index = 0; index < segment.length; index += 1) {
51
+ const code = segment.charCodeAt(index);
52
+ if (code <= 0x20 || code === 0x7f || NOT_IN_BRANCH.includes(segment[index]))
53
+ return false;
54
+ }
55
+ return true;
56
+ }
33
57
  /**
34
58
  * The branch or the commit a caller's `at` names, a commit in lower case. Anything else throws a `TypeError` that
35
59
  * quotes none of it, before any request.
@@ -97,6 +121,10 @@ function itemPathOf(id) {
97
121
  }
98
122
  return { repositoryId: repositoryIdOf(id.slice(0, colon)), path };
99
123
  }
124
+ /** True for an abort signal, as `listing` tells it from its options: a signal carries `aborted`, an options object does not. */
125
+ function isSignal(value) {
126
+ return typeof value.aborted === 'boolean';
127
+ }
100
128
  /**
101
129
  * Throws `GitHubNoAnswer` (`cancelled`) for a call whose signal is already aborted. Each call runs it first, before it
102
130
  * checks its inputs or makes any request, so an aborted call answers alike whatever its inputs, as the DOM standard
@@ -113,12 +141,14 @@ function notCancelled(signal) {
113
141
  * `GitHubNoAnswer` (`cancelled`) before it checks anything else. What a call cannot answer whole then throws before any
114
142
  * request: a `TypeError` for an id that is not a repository id, a repository named twice, an item id that names no
115
143
  * file, a change set of other than one repository, an `at` that is neither a branch name nor a commit or that names
116
- * other than one repository, and a link's text with an empty, `.` or `..` segment, and an `Error` for a repository
117
- * outside the link. An installation is removed by the app's own call (`uninstallInstallation`), never by a link's
118
- * connector.
144
+ * other than one repository, a `path` with an empty, `.` or `..` segment or beside other than one repository, a link's
145
+ * text with an empty, `.` or `..` segment, and a link's ref that no ref name can begin with, and an `Error` for a
146
+ * repository outside the link. An installation is removed by the app's own call (`uninstallInstallation`), never by a
147
+ * link's connector.
119
148
  *
120
149
  * A listing and a comparison follow the default branch, or the branch or commit a selection's `at` names: a branch's
121
- * head is read when the call is made, a commit is read as it is and never changes.
150
+ * head is read when the call is made, a commit is read as it is and never changes. A selection's `path` narrows them to
151
+ * a folder, or a file, before `maxFiles` counts the files.
122
152
  *
123
153
  * Every read addresses a linked repository by its id (`/repositories/<id>/...`, where GitHub's own redirects for a
124
154
  * renamed or moved repository lead), never by its name. A name can come to mean another repository (a renamed
@@ -182,28 +212,28 @@ export class GitHubDocumentsConnector {
182
212
  return followed.commit;
183
213
  return this.branchHead(repository, followed?.branch ?? repository.defaultBranch, signal);
184
214
  }
185
- /**
186
- * The documentation files of a repository at a commit: its tree read once within the request budget, never walking
187
- * a skipped folder, then filtered. A caller keeps it for that commit and hands it to `changesSince` with that commit
188
- * as `after`.
189
- */
190
- async listing(repositoryId, commit, signal) {
215
+ async listing(repositoryId, commit, signalOrOptions) {
216
+ const options = signalOrOptions === undefined || signalOrOptions === null || isSignal(signalOrOptions) ? { signal: signalOrOptions ?? undefined } : signalOrOptions;
217
+ const { signal } = options;
191
218
  notCancelled(signal);
219
+ const path = pathOf(options.path);
192
220
  const repository = this.repository(repositoryId);
193
221
  const tree = await readTree(this.fetchImpl, await this.token(repositoryId, signal), repository.id, commit, this.apiBase, {
194
222
  maxRequests: this.filter.maxTreeRequests,
195
223
  skipFolders: this.filter.skipFolders ?? SKIPPED_FOLDERS,
196
224
  signal,
197
225
  });
198
- return pathFilter(this.filter)(tree);
226
+ return pathFilter(this.filter)(withinPath(tree, path));
199
227
  }
200
228
  /**
201
229
  * The documentation of each selected repository (its id in `ids`) at its default branch's head, or of one repository
202
- * at the branch or commit `at` names: the branch's head, or the commit itself with no branch read. Every id and `at`
203
- * are checked before any repository is read, so a selection is listed whole or not at all: an id that is not a
204
- * repository id, a repository named twice, an `at` that is neither a branch name nor a commit, and an `at` beside
205
- * other than one repository throw a `TypeError`, and a repository outside the link an `Error`. A selection's other
206
- * fields are `changesSince`'s, and a listing reads none of them.
230
+ * at the branch or commit `at` names: the branch's head, or the commit itself with no branch read. With a `path`, the
231
+ * files of one repository at that folder or under it, or the one file it names, kept before `maxFiles` counts them.
232
+ * Every id, `at` and `path` are checked before any repository is read, so a selection is listed whole or not at all:
233
+ * an id that is not a repository id, a repository named twice, an `at` that is neither a branch name nor a commit, a
234
+ * `path` with an empty, `.` or `..` segment or that is not text, and an `at` or a `path` beside other than one
235
+ * repository throw a `TypeError`, and a repository outside the link an `Error`. A selection's other fields are
236
+ * `changesSince`'s, and a listing reads none of them.
207
237
  */
208
238
  async list(selection, signal) {
209
239
  notCancelled(signal);
@@ -213,12 +243,15 @@ export class GitHubDocumentsConnector {
213
243
  const followed = selection.at === undefined ? undefined : followedOf(selection.at);
214
244
  if (followed !== undefined && ids.length !== 1)
215
245
  throw new TypeError('a listing at a named branch or commit is of one repository');
246
+ const path = pathOf(selection.path);
247
+ if (path !== '' && ids.length !== 1)
248
+ throw new TypeError('a listing of a folder or a file is of one repository');
216
249
  for (const id of ids)
217
250
  this.repository(id);
218
251
  const items = [];
219
252
  for (const id of ids) {
220
253
  const commit = followed === undefined ? await this.head(id, signal) : 'commit' in followed ? followed.commit : await this.head(id, followed.branch, signal);
221
- for (const blob of await this.listing(id, commit, signal)) {
254
+ for (const blob of await this.listing(id, commit, { path, signal })) {
222
255
  items.push({ id: `${id}:${blob.path}`, title: blob.path, size: blob.size, version: `${blob.sha}@${commit}` });
223
256
  }
224
257
  }
@@ -286,6 +319,13 @@ export class GitHubDocumentsConnector {
286
319
  * followed branch's head, or at the commit. A push to another branch than the one followed answers no change and the
287
320
  * cursor it was given, and so does any push when `at` is a commit, which never changes; without a push, an `after`
288
321
  * naming another commit than `at`'s throws a `TypeError` before any request.
322
+ *
323
+ * With a `path` the comparison is made within that folder, or of the one file it names: the listing at `after` keeps
324
+ * the files there before `maxFiles` counts them, so the folder's files are never answered gone for the files sorting
325
+ * before it, and a `listing` handed in is narrowed to the path (read it with `listing(repositoryId, after, { path })`
326
+ * so the count is the folder's). A listing handed in that holds `maxFiles` files may have been cut before the folder's
327
+ * files, so with a path the folder is read afresh instead of it. A cursor is compared under the path it was answered under: a path in it outside the
328
+ * one given is answered gone.
289
329
  */
290
330
  async changesSince(cursor, selection, signal) {
291
331
  notCancelled(signal);
@@ -299,6 +339,7 @@ export class GitHubDocumentsConnector {
299
339
  throw new TypeError('a listing is compared only with the after commit it was read at');
300
340
  }
301
341
  const named = selection.at === undefined ? undefined : followedOf(selection.at);
342
+ const folder = pathOf(selection.path);
302
343
  const repository = this.repository(id);
303
344
  const followed = named ?? { branch: repository.defaultBranch };
304
345
  if (selection.ref !== undefined && ('commit' in followed || selection.ref !== `refs/heads/${followed.branch}`)) {
@@ -308,7 +349,10 @@ export class GitHubDocumentsConnector {
308
349
  throw new TypeError('after names another commit than at');
309
350
  }
310
351
  const after = 'commit' in followed ? followed.commit : (selection.after ?? (await this.branchHead(repository, followed.branch, signal)));
311
- const blobs = selection.listing ?? (await this.listing(id, after, signal));
352
+ // a listing at the file count may have been cut before the folder's files: narrowed, it would answer them gone
353
+ const handed = selection.listing;
354
+ const cut = handed !== undefined && folder !== '' && handed.length >= (this.filter.maxFiles ?? DEFAULT_MAX_FILES);
355
+ const blobs = handed === undefined || cut ? await this.listing(id, after, { path: folder, signal }) : withinPath(handed, folder);
312
356
  const now = new Map(blobs.map((blob) => [blob.path, blob.sha]));
313
357
  const changed = blobs
314
358
  .filter((blob) => before.get(blob.path) !== blob.sha)
@@ -318,15 +362,30 @@ export class GitHubDocumentsConnector {
318
362
  }
319
363
  /**
320
364
  * The ref and the path of a folder's or a file's `<ref>/<path>` text (`parseGitHubLink`'s `refAndPath`), whose
321
- * branch name may hold a slash. A first segment of 40 hexadecimal characters is a commit, in lower case, and `HEAD`
322
- * the default branch (git names no branch `HEAD`), each settled with no request. Otherwise one request to GitHub's
323
- * matching references (`/repositories/<id>/git/matching-refs/heads/<first segment>`, under an installation token
324
- * narrowed to the repository), which answers every branch whose name starts with that segment
325
- * (https://docs.github.com/en/rest/git/refs#list-matching-references): the longest branch name that is the text, or
326
- * starts it at a slash, is the ref and the rest the path, and the first segment alone when none is longer. A first
327
- * segment no branch has is answered as the ref all the same, and a listing at it then answers GitHub's 404. A text
328
- * with an empty, `.` or `..` segment throws a `TypeError` that quotes none of it, before any request; GitHub's
329
- * refusal of the read, such as 409 for a repository with no commit, throws `GitHubApiError`.
365
+ * branch or tag name may hold a slash, answered with what the ref is (`kind`) and the name the text writes it by
366
+ * (`name`). A first segment of 40 hexadecimal characters is a commit, in lower case, and `HEAD` the default branch
367
+ * (git names no branch `HEAD`), each settled with no request. Otherwise, under an installation token narrowed to the
368
+ * repository, GitHub's matching references (https://docs.github.com/en/rest/git/refs#list-matching-references), which
369
+ * answer every reference whose name starts with the first segment, are read in turn until one settles it:
370
+ *
371
+ * 1. The branches (`/repositories/<id>/git/matching-refs/heads/<first segment>`) and the tags
372
+ * (`.../matching-refs/tags/<first segment>`): the longest name of either that is the text, or starts it at a slash,
373
+ * is the ref and the rest the path, a branch and a tag of one name settling as the branch. Git keeps a tag under a
374
+ * branch's name beside the branch (`release/2.0` beside `release`), and the longer name is the more of the link it
375
+ * names. The tags are not read when a branch's name is the whole text, since no name is longer. A tag is answered
376
+ * at the commit it names (`commit: true`), as GitHub serves a `/tree/<tag>/` address: a lightweight tag names it,
377
+ * and an annotated tag's object is read for it (https://docs.github.com/en/rest/git/tags#get-a-tag). A tag that
378
+ * names no commit is read as no tag, and the branch, if one matched, settles it.
379
+ * 2. A first segment of 4 to 39 hexadecimal characters, a short commit, through GitHub's commits under its sha media
380
+ * type (https://docs.github.com/en/rest/commits/commits#get-a-commit), which answers the whole commit, or 422 for
381
+ * a prefix no commit has or several have.
382
+ *
383
+ * A first segment neither settles is answered as a branch all the same, and a listing at it then throws GitHub's
384
+ * 404 as `GitHubApiError`; one that is no branch name by git's rules (such as `-x`, `@` or `end.`) throws a
385
+ * `TypeError` instead. A text with an empty, `.` or `..` segment, and one whose first segment no ref name can begin
386
+ * (a control character, a space, `~`, `^`, `:`, `?`, `*`, `[`, `\`, `..` or `@{`, a leading dot or a trailing
387
+ * `.lock`), throw a `TypeError` that quotes none of it, before any request; GitHub's refusal of a read, such as 409 for
388
+ * a repository with no commit, throws `GitHubApiError`.
330
389
  */
331
390
  async resolveRef(repositoryId, refAndPath, signal) {
332
391
  notCancelled(signal);
@@ -335,23 +394,102 @@ export class GitHubDocumentsConnector {
335
394
  }
336
395
  const repository = this.repository(repositoryId);
337
396
  const first = refAndPath.split('/')[0];
338
- const pathAfter = (ref) => refAndPath.slice(ref.length + 1);
397
+ const settled = (name, ref, kind) => ({
398
+ ref,
399
+ path: refAndPath.slice(name.length + 1),
400
+ commit: kind !== 'branch',
401
+ kind,
402
+ name,
403
+ });
339
404
  if (COMMIT.test(first))
340
- return { ref: first.toLowerCase(), path: pathAfter(first), commit: true };
405
+ return settled(first, first.toLowerCase(), 'commit');
341
406
  if (first === 'HEAD')
342
- return { ref: repository.defaultBranch, path: pathAfter(first), commit: false };
343
- const url = `${this.address(repository)}/git/matching-refs/heads/${encodeURIComponent(first)}`;
344
- const references = arrayOf(await githubFetch(this.fetchImpl, url, { token: await this.token(repository.id, signal), signal }), 'a list of references');
345
- let ref = first;
407
+ return settled(first, repository.defaultBranch, 'branch');
408
+ if (!mayBeginRef(first))
409
+ throw new TypeError('the ref of a link holds what no branch, tag or commit holds');
410
+ const token = await this.token(repository.id, signal);
411
+ const branch = await this.longestRef(repository, 'heads', first, refAndPath, token, signal);
412
+ // a tag names more of the text than the branch when its name is longer; a branch named by the whole text has no longer
413
+ const tag = branch !== null && branch.name === refAndPath ? null : await this.longestRef(repository, 'tags', first, refAndPath, token, signal);
414
+ const longer = tag !== null && (branch === null || tag.name.length > branch.name.length) ? tag : null;
415
+ const tagged = longer === null ? null : await this.tagCommit(repository, longer.fields, token, signal);
416
+ if (longer !== null && tagged !== null)
417
+ return settled(longer.name, tagged, 'tag');
418
+ if (branch !== null)
419
+ return settled(branch.name, branch.name, 'branch');
420
+ const commit = SHORT_COMMIT.test(first) ? await this.commitOf(repository, first, token, signal) : null;
421
+ if (commit !== null)
422
+ return settled(first, commit, 'commit');
423
+ if (!isBranchName(first))
424
+ throw new TypeError('the ref of a link names no branch, tag or commit of the repository');
425
+ return settled(first, first, 'branch');
426
+ }
427
+ /**
428
+ * The longest branch or tag (`space`) whose name is `text` or starts it at a slash, from GitHub's matching references
429
+ * for `first`, the text's first segment, with the reference's fields; null for none.
430
+ */
431
+ async longestRef(repository, space, first, text, token, signal) {
432
+ const url = `${this.address(repository)}/git/matching-refs/${space}/${encodeURIComponent(first)}`;
433
+ const references = arrayOf(await githubFetch(this.fetchImpl, url, { token, signal }), 'a list of references');
434
+ const prefix = `refs/${space}/`;
435
+ let found = null;
346
436
  for (const reference of references) {
347
- const name = stringIn(objectOf(reference, 'a reference'), 'ref', 'a reference');
348
- if (!name.startsWith('refs/heads/'))
437
+ const fields = objectOf(reference, 'a reference');
438
+ const full = stringIn(fields, 'ref', 'a reference');
439
+ if (!full.startsWith(prefix))
349
440
  continue;
350
- const branch = name.slice('refs/heads/'.length);
351
- if (branch.length > ref.length && (refAndPath === branch || refAndPath.startsWith(`${branch}/`)))
352
- ref = branch;
441
+ const name = full.slice(prefix.length);
442
+ if ((found === null || name.length > found.name.length) && (text === name || text.startsWith(`${name}/`)))
443
+ found = { name, fields };
444
+ }
445
+ return found;
446
+ }
447
+ /**
448
+ * The commit a tag's reference names: a lightweight tag's own object, or the object an annotated tag names, read
449
+ * through GitHub's tags and followed through a tag of a tag up to {@link MAX_TAG_DEPTH}, in lower case. Null for a
450
+ * tag that names a tree or a blob, or a longer chain of tags.
451
+ */
452
+ async tagCommit(repository, reference, token, signal) {
453
+ let object = objectOf(reference.object, "a reference's object");
454
+ for (let depth = 0; stringIn(object, 'type', "a reference's object") === 'tag'; depth += 1) {
455
+ if (depth === MAX_TAG_DEPTH)
456
+ return null;
457
+ const url = `${this.address(repository)}/git/tags/${encodeURIComponent(stringIn(object, 'sha', "a reference's object"))}`;
458
+ object = objectOf(objectOf(await githubFetch(this.fetchImpl, url, { token, signal }), 'a tag').object, "a tag's object");
459
+ }
460
+ const sha = stringIn(object, 'sha', "a reference's object");
461
+ return stringIn(object, 'type', "a reference's object") === 'commit' && COMMIT.test(sha) ? sha.toLowerCase() : null;
462
+ }
463
+ /**
464
+ * The whole commit a short commit names, in lower case, through GitHub's commits under the sha media type, which
465
+ * answers the commit's sha alone; null when GitHub answers 422 or 404, as it does for a prefix no commit has or
466
+ * several have. An answer that is not a commit starting with the prefix throws.
467
+ */
468
+ async commitOf(repository, prefix, token, signal) {
469
+ const short = prefix.toLowerCase();
470
+ const url = `${this.address(repository)}/commits/${encodeURIComponent(short)}`;
471
+ let answer;
472
+ try {
473
+ answer = await githubSend(this.fetchImpl, url, { token, accept: COMMIT_SHA_MEDIA_TYPE, signal });
474
+ }
475
+ catch (error) {
476
+ if (error instanceof GitHubApiError && (error.status === 404 || error.status === 422))
477
+ return null;
478
+ throw error;
479
+ }
480
+ let bytes;
481
+ try {
482
+ bytes = await answer.bytes(COMMIT_SHA_BYTES);
483
+ }
484
+ catch (error) {
485
+ if (error instanceof ReadCapExceeded)
486
+ throw new Error('GitHub answered a commit that is not one');
487
+ throw error;
353
488
  }
354
- return { ref, path: pathAfter(ref), commit: false };
489
+ const commit = new TextDecoder().decode(bytes).trim().toLowerCase();
490
+ if (!COMMIT.test(commit) || !commit.startsWith(short))
491
+ throw new Error('GitHub answered a commit that is not one');
492
+ return commit;
355
493
  }
356
494
  /** Nothing to end here: an installation is removed by the app's own call (`uninstallInstallation`). */
357
495
  async revoke() {