@gitkraken/core-gitlens 0.24.0 → 0.26.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 (176) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/README.md +23 -15
  3. package/dist/git/errors.d.ts +6 -5
  4. package/dist/git/errors.js +7 -6
  5. package/dist/git/errors.js.map +1 -1
  6. package/dist/git/features.d.ts +1 -1
  7. package/dist/git/features.js +1 -1
  8. package/dist/git/features.js.map +1 -1
  9. package/dist/git/models/remoteProvider.d.ts +4 -1
  10. package/dist/git/models/remoteProvider.js.map +1 -1
  11. package/dist/git/providers/operations.d.ts +15 -0
  12. package/dist/git/providers/provider.d.ts +4 -2
  13. package/dist/git/utils/branch.utils.d.ts +4 -1
  14. package/dist/git/utils/branch.utils.js +5 -2
  15. package/dist/git/utils/branch.utils.js.map +1 -1
  16. package/dist/git/utils/mergeConflicts.utils.js +2 -2
  17. package/dist/git/utils/mergeConflicts.utils.js.map +1 -1
  18. package/dist/git/watching/worktreeSummary.d.ts +15 -3
  19. package/dist/git/watching/worktreeSummary.js +179 -31
  20. package/dist/git/watching/worktreeSummary.js.map +1 -1
  21. package/dist/git-cli/cliGitProvider.d.ts +30 -3
  22. package/dist/git-cli/cliGitProvider.js +102 -40
  23. package/dist/git-cli/cliGitProvider.js.map +1 -1
  24. package/dist/git-cli/exec/git.d.ts +9 -1
  25. package/dist/git-cli/exec/git.js +23 -4
  26. package/dist/git-cli/exec/git.js.map +1 -1
  27. package/dist/git-cli/exec/gitDirResolver.d.ts +26 -0
  28. package/dist/git-cli/exec/gitDirResolver.js +181 -0
  29. package/dist/git-cli/exec/gitDirResolver.js.map +1 -0
  30. package/dist/git-cli/exec/headState.js +3 -3
  31. package/dist/git-cli/exec/headState.js.map +1 -1
  32. package/dist/git-cli/exec/indexRefresh.d.ts +78 -0
  33. package/dist/git-cli/exec/indexRefresh.js +159 -0
  34. package/dist/git-cli/exec/indexRefresh.js.map +1 -0
  35. package/dist/git-cli/providers/branchRemote.js +2 -5
  36. package/dist/git-cli/providers/branchRemote.js.map +1 -1
  37. package/dist/git-cli/providers/branches.js +4 -4
  38. package/dist/git-cli/providers/branches.js.map +1 -1
  39. package/dist/git-cli/providers/config.d.ts +2 -0
  40. package/dist/git-cli/providers/config.js +26 -4
  41. package/dist/git-cli/providers/config.js.map +1 -1
  42. package/dist/git-cli/providers/maintenance.js +1 -1
  43. package/dist/git-cli/providers/operations.d.ts +4 -1
  44. package/dist/git-cli/providers/operations.js +33 -25
  45. package/dist/git-cli/providers/operations.js.map +1 -1
  46. package/dist/git-cli/providers/pausedOperations.d.ts +3 -1
  47. package/dist/git-cli/providers/pausedOperations.js +10 -7
  48. package/dist/git-cli/providers/pausedOperations.js.map +1 -1
  49. package/dist/git-cli/providers/refs.js +25 -1
  50. package/dist/git-cli/providers/refs.js.map +1 -1
  51. package/dist/git-cli/providers/staging.js +1 -1
  52. package/dist/git-cli/providers/stash.d.ts +3 -1
  53. package/dist/git-cli/providers/stash.js +8 -5
  54. package/dist/git-cli/providers/stash.js.map +1 -1
  55. package/dist/git-cli/providers/worktrees.d.ts +6 -1
  56. package/dist/git-cli/providers/worktrees.js +6 -2
  57. package/dist/git-cli/providers/worktrees.js.map +1 -1
  58. package/dist/git-cli/service.d.ts +2 -0
  59. package/dist/git-cli/service.js +2 -1
  60. package/dist/git-cli/service.js.map +1 -1
  61. package/dist/ipc/discovery.js +1 -1
  62. package/dist/plus/agents/providers/claudeCodeTranscript.js +6 -6
  63. package/dist/plus/agents/providers/claudeCodeTranscript.js.map +1 -1
  64. package/dist/plus/ai/errors.d.ts +4 -1
  65. package/dist/plus/ai/errors.js +4 -0
  66. package/dist/plus/ai/errors.js.map +1 -1
  67. package/dist/plus/ai/providers/mistralProvider.js +1 -1
  68. package/dist/plus/ai/providers/mistralProvider.js.map +1 -1
  69. package/dist/plus/ai/providers/openAICompatibleProviderBase.js +2 -1
  70. package/dist/plus/ai/providers/openAICompatibleProviderBase.js.map +1 -1
  71. package/dist/plus/git-github/api/github.js +12 -13
  72. package/dist/plus/git-github/api/github.js.map +1 -1
  73. package/dist/plus/git-github/api/token.d.ts +2 -0
  74. package/dist/plus/integrations/authentication/cloudIntegrationService.d.ts +1 -1
  75. package/dist/plus/integrations/authentication/cloudIntegrationService.js +6 -1
  76. package/dist/plus/integrations/authentication/cloudIntegrationService.js.map +1 -1
  77. package/dist/plus/integrations/authentication/configuredIntegrationService.js +2 -2
  78. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +18 -5
  79. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js.map +1 -1
  80. package/dist/plus/integrations/authentication/integrationAuthenticationService.d.ts +3 -1
  81. package/dist/plus/integrations/authentication/integrationAuthenticationService.js +4 -0
  82. package/dist/plus/integrations/authentication/integrationAuthenticationService.js.map +1 -1
  83. package/dist/plus/integrations/authentication/models.d.ts +2 -0
  84. package/dist/plus/integrations/authentication/models.js +2 -1
  85. package/dist/plus/integrations/authentication/models.js.map +1 -1
  86. package/dist/plus/integrations/authentication/observedAuthenticationProvider.js +1 -1
  87. package/dist/plus/integrations/authentication/rejectedTokenTracker.d.ts +5 -4
  88. package/dist/plus/integrations/authentication/rejectedTokenTracker.js +5 -4
  89. package/dist/plus/integrations/authentication/rejectedTokenTracker.js.map +1 -1
  90. package/dist/plus/integrations/index.d.ts +2 -2
  91. package/dist/plus/integrations/index.js +1 -1
  92. package/dist/plus/integrations/index.js.map +1 -1
  93. package/dist/plus/integrations/integrationService.d.ts +6 -0
  94. package/dist/plus/integrations/integrationService.js +3 -3
  95. package/dist/plus/integrations/integrationService.js.map +1 -1
  96. package/dist/plus/integrations/lite.js +1 -1
  97. package/dist/plus/integrations/manager.d.ts +51 -3
  98. package/dist/plus/integrations/models/gitHostIntegration.d.ts +14 -0
  99. package/dist/plus/integrations/models/gitHostIntegration.js +50 -41
  100. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  101. package/dist/plus/integrations/models/integration.d.ts +27 -5
  102. package/dist/plus/integrations/models/integration.js +57 -15
  103. package/dist/plus/integrations/models/integration.js.map +1 -1
  104. package/dist/plus/integrations/models/issueReads.d.ts +6 -0
  105. package/dist/plus/integrations/models/issuesIntegration.js +11 -11
  106. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
  107. package/dist/plus/integrations/providers/azure/azure.js +2 -2
  108. package/dist/plus/integrations/providers/azureDevOps.js +1 -1
  109. package/dist/plus/integrations/providers/azureDevOpsServer.js +1 -1
  110. package/dist/plus/integrations/providers/bitbucket/bitbucket.js +1 -1
  111. package/dist/plus/integrations/providers/bitbucket/bitbucket.js.map +1 -1
  112. package/dist/plus/integrations/providers/bitbucket-server.js +1 -1
  113. package/dist/plus/integrations/providers/bitbucket.js +1 -1
  114. package/dist/plus/integrations/providers/github.js +1 -1
  115. package/dist/plus/integrations/providers/gitlab/gitlab.js +2 -2
  116. package/dist/plus/integrations/providers/gitlab/gitlab.js.map +1 -1
  117. package/dist/plus/integrations/providers/gitlab.js +1 -1
  118. package/dist/plus/integrations/providers/jira-server.js +2 -2
  119. package/dist/plus/integrations/providers/jira.js +1 -1
  120. package/dist/plus/integrations/providers/linear.js +2 -2
  121. package/dist/plus/integrations/providers/linear.js.map +1 -1
  122. package/dist/plus/integrations/providers/models.js +2 -1
  123. package/dist/plus/integrations/providers/models.js.map +1 -1
  124. package/dist/plus/integrations/providers/providerErrors.js +9 -0
  125. package/dist/plus/integrations/providers/providerErrors.js.map +1 -1
  126. package/dist/plus/integrations/providers/providersApi.js +1 -1
  127. package/dist/plus/integrations/providers/trello.js +1 -1
  128. package/dist/plus/integrations/providers/utils.js +1 -1
  129. package/dist/plus/integrations/reads/counts.js +1 -1
  130. package/dist/plus/integrations/reads/cursors.js +1 -1
  131. package/dist/plus/integrations/reads/drains.d.ts +5 -4
  132. package/dist/plus/integrations/reads/drains.js +28 -8
  133. package/dist/plus/integrations/reads/drains.js.map +1 -1
  134. package/dist/plus/integrations/reads/hierarchy.js +1 -1
  135. package/dist/plus/integrations/reads/issueBatch.d.ts +1 -1
  136. package/dist/plus/integrations/reads/issueBatch.js.map +1 -1
  137. package/dist/plus/integrations/reads/issueTracker.js +1 -1
  138. package/dist/plus/integrations/reads/issues.d.ts +2 -0
  139. package/dist/plus/integrations/reads/issues.js +5 -3
  140. package/dist/plus/integrations/reads/issues.js.map +1 -1
  141. package/dist/plus/integrations/reads/paging.d.ts +5 -6
  142. package/dist/plus/integrations/reads/paging.js +5 -6
  143. package/dist/plus/integrations/reads/paging.js.map +1 -1
  144. package/dist/plus/integrations/reads/pullRequests.d.ts +4 -0
  145. package/dist/plus/integrations/reads/pullRequests.js +4 -2
  146. package/dist/plus/integrations/reads/pullRequests.js.map +1 -1
  147. package/dist/plus/integrations/reads/resolveRepository.js +1 -1
  148. package/dist/plus/integrations/reads/sweeps.js +42 -10
  149. package/dist/plus/integrations/reads/sweeps.js.map +1 -1
  150. package/dist/plus/integrations/results.d.ts +23 -3
  151. package/dist/plus/integrations/results.js.map +1 -1
  152. package/dist/plus/notifications/api.d.ts +20 -0
  153. package/dist/plus/notifications/api.js +89 -0
  154. package/dist/plus/notifications/api.js.map +1 -0
  155. package/dist/plus/notifications/context.d.ts +28 -0
  156. package/dist/plus/notifications/context.js +0 -0
  157. package/dist/plus/notifications/errors.d.ts +13 -0
  158. package/dist/plus/notifications/errors.js +22 -0
  159. package/dist/plus/notifications/errors.js.map +1 -0
  160. package/dist/plus/notifications/models.d.ts +94 -0
  161. package/dist/plus/notifications/models.js +0 -0
  162. package/dist/plus/notifications/notificationsService.d.ts +115 -0
  163. package/dist/plus/notifications/notificationsService.js +381 -0
  164. package/dist/plus/notifications/notificationsService.js.map +1 -0
  165. package/dist/plus/notifications/parse.d.ts +12 -0
  166. package/dist/plus/notifications/parse.js +152 -0
  167. package/dist/plus/notifications/parse.js.map +1 -0
  168. package/dist/plus/notifications/presentation.d.ts +28 -0
  169. package/dist/plus/notifications/presentation.js +55 -0
  170. package/dist/plus/notifications/presentation.js.map +1 -0
  171. package/dist/utils/env/node/exec.d.ts +10 -1
  172. package/dist/utils/env/node/exec.js +41 -4
  173. package/dist/utils/env/node/exec.js.map +1 -1
  174. package/docs/integrations.md +21 -2
  175. package/docs/notifications.md +239 -0
  176. package/package.json +21 -1
@@ -0,0 +1,26 @@
1
+ //#region ../git-cli/src/exec/gitDirResolver.d.ts
2
+ /**
3
+ * Whether git run with `env` might find another git dir or working tree than {@link readGitDirs} reads: one set
4
+ * through the environment, or through `-c` overrides passed in it
5
+ */
6
+ export declare function isGitDirMovedByEnv(env: Readonly<Record<string, string | undefined>>): boolean;
7
+ /** The path a `.git` file names, as written (absolute, or relative to the file's folder); `undefined` when git would reject it */
8
+ export declare function parseGitFile(content: string): string | undefined;
9
+ /** The path a linked worktree's `commondir` file names, as written (absolute, or relative to its git dir) */
10
+ export declare function parseCommonDir(content: string): string | undefined;
11
+ /**
12
+ * The git dir and common git dir `git rev-parse --git-dir --git-common-dir` reports from the working tree `repoPath`,
13
+ * read without running git, and only when that working tree has no superproject. `undefined` whenever the layout is
14
+ * one git might answer differently for: no `.git` at `repoPath` (a subfolder, a bare repository), a git dir a `.git`
15
+ * file names without a `commondir` (a submodule, `--separate-git-dir`), a `core.bare`, `core.worktree` or `include`
16
+ * in its config, a folder owned by another user (git's `safe.directory` check, made only where the process has a
17
+ * uid: Windows has none, so there a folder git would refuse is still read), or a repository above it whose working
18
+ * tree might track it as a submodule. The caller rules out the environment (see {@link isGitDirMovedByEnv}).
19
+ * Never rejects.
20
+ */
21
+ export declare function readGitDirs(repoPath: string): Promise<{
22
+ gitDir: string;
23
+ commonGitDir: string;
24
+ } | undefined>;
25
+ //#endregion
26
+ //# sourceMappingURL=gitDirResolver.d.ts.map
@@ -0,0 +1,181 @@
1
+ import { normalizePath } from "../../utils/path.js";
2
+ import { dirname, isAbsolute, join } from "node:path";
3
+ import * as nodeProcess from "node:process";
4
+ import { lstat, readFile, realpath, stat } from "node:fs/promises";
5
+ //#region ../git-cli/src/exec/gitDirResolver.ts
6
+ /** Environment variables that change which repository, git dir or working tree git finds from a `cwd` */
7
+ const locationEnvKeys = [
8
+ "GIT_DIR",
9
+ "GIT_COMMON_DIR",
10
+ "GIT_WORK_TREE",
11
+ "GIT_CEILING_DIRECTORIES",
12
+ "GIT_DISCOVERY_ACROSS_FILESYSTEM"
13
+ ];
14
+ /** `-c` overrides, passed through the environment, that move or remove the working tree */
15
+ const locationConfigRegex = /core\.(?:worktree|bare)|include/i;
16
+ const gitFilePrefix = "gitdir: ";
17
+ const trailingNewlinesRegex = /[\r\n]+$/;
18
+ const newlineRegex = /[\r\n]/;
19
+ const headRegex = /^(?:ref:\s*refs\/|[0-9a-f]{40}(?:[0-9a-f]{24})?\s*$)/;
20
+ const worktreeKeyRegex = /^(?:[ \t]*\[[^\]\r\n]*\])?[ \t]*worktree[ \t]*=/im;
21
+ const includeSectionRegex = /^(?:[ \t]*\[[^\]\r\n]*\])?[ \t]*\[[ \t]*include/im;
22
+ const bareKeyRegex = /^(?:[ \t]*\[[^\]\r\n]*\])?[ \t]*bare[ \t]*(?:=[ \t]*(.*?))?[ \t\r]*$/gim;
23
+ const boolTrueRegex = /^(?:true|yes|on|1)$/i;
24
+ const boolFalseRegex = /^(?:false|no|off|0)$/i;
25
+ /**
26
+ * Whether git run with `env` might find another git dir or working tree than {@link readGitDirs} reads: one set
27
+ * through the environment, or through `-c` overrides passed in it
28
+ */
29
+ function isGitDirMovedByEnv(env) {
30
+ if (locationEnvKeys.some((k) => env[k] != null)) return true;
31
+ if (env.GIT_CONFIG_PARAMETERS != null && locationConfigRegex.test(env.GIT_CONFIG_PARAMETERS)) return true;
32
+ if (env.GIT_CONFIG_COUNT == null) return false;
33
+ const count = Number(env.GIT_CONFIG_COUNT);
34
+ if (!Number.isInteger(count)) return true;
35
+ for (let i = 0; i < count; i++) if (locationConfigRegex.test(env[`GIT_CONFIG_KEY_${i}`] ?? "")) return true;
36
+ return false;
37
+ }
38
+ /** The path a `.git` file names, as written (absolute, or relative to the file's folder); `undefined` when git would reject it */
39
+ function parseGitFile(content) {
40
+ if (!content.startsWith(gitFilePrefix)) return void 0;
41
+ const path = content.slice(8).replace(trailingNewlinesRegex, "");
42
+ return path && !newlineRegex.test(path) ? path : void 0;
43
+ }
44
+ /** The path a linked worktree's `commondir` file names, as written (absolute, or relative to its git dir) */
45
+ function parseCommonDir(content) {
46
+ return content.replace(trailingNewlinesRegex, "") || void 0;
47
+ }
48
+ /**
49
+ * The git dir and common git dir `git rev-parse --git-dir --git-common-dir` reports from the working tree `repoPath`,
50
+ * read without running git, and only when that working tree has no superproject. `undefined` whenever the layout is
51
+ * one git might answer differently for: no `.git` at `repoPath` (a subfolder, a bare repository), a git dir a `.git`
52
+ * file names without a `commondir` (a submodule, `--separate-git-dir`), a `core.bare`, `core.worktree` or `include`
53
+ * in its config, a folder owned by another user (git's `safe.directory` check, made only where the process has a
54
+ * uid: Windows has none, so there a folder git would refuse is still read), or a repository above it whose working
55
+ * tree might track it as a submodule. The caller rules out the environment (see {@link isGitDirMovedByEnv}).
56
+ * Never rejects.
57
+ */
58
+ async function readGitDirs(repoPath) {
59
+ try {
60
+ return await readGitDirsCore(repoPath);
61
+ } catch {
62
+ return;
63
+ }
64
+ }
65
+ async function readGitDirsCore(repoPath) {
66
+ if (!isAbsolute(repoPath)) return void 0;
67
+ const realRepoPath = await realpath(repoPath);
68
+ const dotGit = join(repoPath, ".git");
69
+ const dotGitStats = await stat(dotGit);
70
+ let gitDir;
71
+ let commonGitDir;
72
+ let linked;
73
+ if (dotGitStats.isDirectory()) {
74
+ if (await statIfExists(join(dotGit, "commondir")) != null) return void 0;
75
+ gitDir = commonGitDir = dotGit;
76
+ linked = false;
77
+ } else if (dotGitStats.isFile()) {
78
+ const target = parseGitFile(await readFile(dotGit, "utf8"));
79
+ if (target == null) return void 0;
80
+ gitDir = await realpath(isAbsolute(target) ? target : `${realRepoPath}/${target}`);
81
+ const common = parseCommonDir(await readFile(join(gitDir, "commondir"), "utf8"));
82
+ if (common == null) return void 0;
83
+ commonGitDir = await realpath(isAbsolute(common) ? common : `${gitDir}/${common}`);
84
+ if (normalizePath(gitDir).includes("/modules/") || normalizePath(commonGitDir).includes("/modules/")) return;
85
+ linked = true;
86
+ } else return;
87
+ if (!headRegex.test(await readFile(join(gitDir, "HEAD"), "utf8"))) return void 0;
88
+ if (!(await stat(join(commonGitDir, "objects"))).isDirectory()) return void 0;
89
+ if (!(await stat(join(commonGitDir, "refs"))).isDirectory()) return void 0;
90
+ if (!isPlainConfig(await readFile(join(commonGitDir, "config"), "utf8"), !linked)) return void 0;
91
+ const worktreeConfig = await readFileIfExists(join(gitDir, "config.worktree"));
92
+ if (worktreeConfig != null && !isPlainConfig(worktreeConfig, true)) return void 0;
93
+ const uid = nodeProcess.geteuid?.();
94
+ if (uid != null) {
95
+ if ([
96
+ (await stat(repoPath)).uid,
97
+ (await lstat(dotGit)).uid,
98
+ (await stat(gitDir)).uid
99
+ ].some((o) => o !== uid)) return void 0;
100
+ }
101
+ if (await mayHaveSuperproject(realRepoPath)) return void 0;
102
+ return {
103
+ gitDir,
104
+ commonGitDir
105
+ };
106
+ }
107
+ /**
108
+ * Whether a repository above `realRepoPath` could list it as a submodule. `rev-parse --show-superproject-working-tree`
109
+ * asks the repository git finds from the parent folder; only a bare one, having no index, can't list it. Rejects when
110
+ * a folder above can't be read.
111
+ */
112
+ async function mayHaveSuperproject(realRepoPath) {
113
+ let folder = realRepoPath;
114
+ for (;;) {
115
+ const parent = dirname(folder);
116
+ if (parent === folder) return false;
117
+ folder = parent;
118
+ const dotGit = join(folder, ".git");
119
+ const dotGitStats = await statIfExists(dotGit);
120
+ if (dotGitStats?.isFile()) {
121
+ const target = parseGitFile(await readFile(dotGit, "utf8"));
122
+ if (target == null) return true;
123
+ const gitDir = isAbsolute(target) ? target : `${folder}/${target}`;
124
+ if (await statIfExists(join(gitDir, "commondir")) != null) return true;
125
+ return !isBareConfig(await readFile(join(gitDir, "config"), "utf8"));
126
+ }
127
+ if (dotGitStats?.isDirectory() && await isGitDirectory(dotGit)) return !isBareConfig(await readFile(join(dotGit, "config"), "utf8"));
128
+ if (await isGitDirectory(folder)) return !isBareConfig(await readFile(join(folder, "config"), "utf8"));
129
+ }
130
+ }
131
+ /** Whether a folder is a git dir: a valid `HEAD`, and the `objects` and `refs` folders (or a `commondir` to them) */
132
+ async function isGitDirectory(path) {
133
+ const head = await readFileIfExists(join(path, "HEAD"));
134
+ if (head == null || !headRegex.test(head)) return false;
135
+ if (await statIfExists(join(path, "commondir")) != null) return true;
136
+ return (await statIfExists(join(path, "objects")))?.isDirectory() === true && (await statIfExists(join(path, "refs")))?.isDirectory() === true;
137
+ }
138
+ /** Whether a config sets nothing that moves the working tree; `checkBare` also rules out a `core.bare` not plainly false */
139
+ function isPlainConfig(config, checkBare) {
140
+ if (worktreeKeyRegex.test(config) || includeSectionRegex.test(config)) return false;
141
+ if (!checkBare) return true;
142
+ const bare = getBare(config);
143
+ return bare == null || bare === false;
144
+ }
145
+ /** Whether a config is plainly a bare repository's, with no working tree set anywhere */
146
+ function isBareConfig(config) {
147
+ return !worktreeKeyRegex.test(config) && !includeSectionRegex.test(config) && getBare(config) === true;
148
+ }
149
+ /** The last `bare` value in a config: `undefined` when unset, `'unknown'` when not a plain boolean */
150
+ function getBare(config) {
151
+ let bare;
152
+ for (const match of config.matchAll(bareKeyRegex)) {
153
+ const value = match[1];
154
+ if (value == null || boolTrueRegex.test(value)) bare = true;
155
+ else if (boolFalseRegex.test(value)) bare = false;
156
+ else bare = "unknown";
157
+ }
158
+ return bare;
159
+ }
160
+ async function statIfExists(path) {
161
+ try {
162
+ return await stat(path);
163
+ } catch (ex) {
164
+ const code = ex.code;
165
+ if (code === "ENOENT" || code === "ENOTDIR") return void 0;
166
+ throw ex;
167
+ }
168
+ }
169
+ async function readFileIfExists(path) {
170
+ try {
171
+ return await readFile(path, "utf8");
172
+ } catch (ex) {
173
+ const code = ex.code;
174
+ if (code === "ENOENT" || code === "ENOTDIR") return void 0;
175
+ throw ex;
176
+ }
177
+ }
178
+ //#endregion
179
+ export { isGitDirMovedByEnv, parseCommonDir, parseGitFile, readGitDirs };
180
+
181
+ //# sourceMappingURL=gitDirResolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitDirResolver.js","names":[],"sources":["../../../../git-cli/src/exec/gitDirResolver.ts"],"sourcesContent":["import type { Stats } from 'node:fs';\nimport { lstat, readFile, realpath, stat } from 'node:fs/promises';\nimport { dirname, isAbsolute, join } from 'node:path';\nimport * as nodeProcess from 'node:process';\nimport { normalizePath } from '@gitlens/utils/path.js';\n\n/** Environment variables that change which repository, git dir or working tree git finds from a `cwd` */\nconst locationEnvKeys = [\n\t'GIT_DIR',\n\t'GIT_COMMON_DIR',\n\t'GIT_WORK_TREE',\n\t'GIT_CEILING_DIRECTORIES',\n\t'GIT_DISCOVERY_ACROSS_FILESYSTEM',\n];\n/** `-c` overrides, passed through the environment, that move or remove the working tree */\nconst locationConfigRegex = /core\\.(?:worktree|bare)|include/i;\n\nconst gitFilePrefix = 'gitdir: ';\nconst trailingNewlinesRegex = /[\\r\\n]+$/;\nconst newlineRegex = /[\\r\\n]/;\nconst headRegex = /^(?:ref:\\s*refs\\/|[0-9a-f]{40}(?:[0-9a-f]{24})?\\s*$)/;\n// git reads a key, or another section header, on the rest of a section header's line too (`[core] bare = true`)\nconst worktreeKeyRegex = /^(?:[ \\t]*\\[[^\\]\\r\\n]*\\])?[ \\t]*worktree[ \\t]*=/im;\nconst includeSectionRegex = /^(?:[ \\t]*\\[[^\\]\\r\\n]*\\])?[ \\t]*\\[[ \\t]*include/im;\nconst bareKeyRegex = /^(?:[ \\t]*\\[[^\\]\\r\\n]*\\])?[ \\t]*bare[ \\t]*(?:=[ \\t]*(.*?))?[ \\t\\r]*$/gim;\nconst boolTrueRegex = /^(?:true|yes|on|1)$/i;\nconst boolFalseRegex = /^(?:false|no|off|0)$/i;\n\n/**\n * Whether git run with `env` might find another git dir or working tree than {@link readGitDirs} reads: one set\n * through the environment, or through `-c` overrides passed in it\n */\nexport function isGitDirMovedByEnv(env: Readonly<Record<string, string | undefined>>): boolean {\n\tif (locationEnvKeys.some(k => env[k] != null)) return true;\n\tif (env.GIT_CONFIG_PARAMETERS != null && locationConfigRegex.test(env.GIT_CONFIG_PARAMETERS)) return true;\n\tif (env.GIT_CONFIG_COUNT == null) return false;\n\n\tconst count = Number(env.GIT_CONFIG_COUNT);\n\tif (!Number.isInteger(count)) return true;\n\n\tfor (let i = 0; i < count; i++) {\n\t\tif (locationConfigRegex.test(env[`GIT_CONFIG_KEY_${i}`] ?? '')) return true;\n\t}\n\treturn false;\n}\n\n/** The path a `.git` file names, as written (absolute, or relative to the file's folder); `undefined` when git would reject it */\nexport function parseGitFile(content: string): string | undefined {\n\tif (!content.startsWith(gitFilePrefix)) return undefined;\n\n\tconst path = content.slice(gitFilePrefix.length).replace(trailingNewlinesRegex, '');\n\treturn path && !newlineRegex.test(path) ? path : undefined;\n}\n\n/** The path a linked worktree's `commondir` file names, as written (absolute, or relative to its git dir) */\nexport function parseCommonDir(content: string): string | undefined {\n\treturn content.replace(trailingNewlinesRegex, '') || undefined;\n}\n\n/**\n * The git dir and common git dir `git rev-parse --git-dir --git-common-dir` reports from the working tree `repoPath`,\n * read without running git, and only when that working tree has no superproject. `undefined` whenever the layout is\n * one git might answer differently for: no `.git` at `repoPath` (a subfolder, a bare repository), a git dir a `.git`\n * file names without a `commondir` (a submodule, `--separate-git-dir`), a `core.bare`, `core.worktree` or `include`\n * in its config, a folder owned by another user (git's `safe.directory` check, made only where the process has a\n * uid: Windows has none, so there a folder git would refuse is still read), or a repository above it whose working\n * tree might track it as a submodule. The caller rules out the environment (see {@link isGitDirMovedByEnv}).\n * Never rejects.\n */\nexport async function readGitDirs(repoPath: string): Promise<{ gitDir: string; commonGitDir: string } | undefined> {\n\ttry {\n\t\treturn await readGitDirsCore(repoPath);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nasync function readGitDirsCore(repoPath: string): Promise<{ gitDir: string; commonGitDir: string } | undefined> {\n\tif (!isAbsolute(repoPath)) return undefined;\n\n\t// git searches from the physical `cwd`, so a relative path in a `.git` file resolves against it\n\tconst realRepoPath = await realpath(repoPath);\n\tconst dotGit = join(repoPath, '.git');\n\tconst dotGitStats = await stat(dotGit);\n\n\tlet gitDir: string;\n\tlet commonGitDir: string;\n\tlet linked: boolean;\n\tif (dotGitStats.isDirectory()) {\n\t\tif ((await statIfExists(join(dotGit, 'commondir'))) != null) return undefined;\n\n\t\t// rev-parse prints `.git` relative to the `cwd`, which is how the caller's path reaches it\n\t\tgitDir = commonGitDir = dotGit;\n\t\tlinked = false;\n\t} else if (dotGitStats.isFile()) {\n\t\tconst target = parseGitFile(await readFile(dotGit, 'utf8'));\n\t\tif (target == null) return undefined;\n\n\t\tgitDir = await realpath(isAbsolute(target) ? target : `${realRepoPath}/${target}`);\n\t\tconst common = parseCommonDir(await readFile(join(gitDir, 'commondir'), 'utf8'));\n\t\tif (common == null) return undefined;\n\n\t\tcommonGitDir = await realpath(isAbsolute(common) ? common : `${gitDir}/${common}`);\n\t\t// A submodule's git dir, which its linked worktrees share, is under its superproject's `modules` folder\n\t\tif (normalizePath(gitDir).includes('/modules/') || normalizePath(commonGitDir).includes('/modules/')) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tlinked = true;\n\t} else {\n\t\treturn undefined;\n\t}\n\n\t// What makes a folder a git dir to git; without it, git keeps searching the folders above\n\tif (!headRegex.test(await readFile(join(gitDir, 'HEAD'), 'utf8'))) return undefined;\n\tif (!(await stat(join(commonGitDir, 'objects'))).isDirectory()) return undefined;\n\tif (!(await stat(join(commonGitDir, 'refs'))).isDirectory()) return undefined;\n\n\t// A linked worktree ignores a `core.bare` in the shared config (a bare repository's worktrees), not in its own\n\tif (!isPlainConfig(await readFile(join(commonGitDir, 'config'), 'utf8'), !linked)) return undefined;\n\n\tconst worktreeConfig = await readFileIfExists(join(gitDir, 'config.worktree'));\n\tif (worktreeConfig != null && !isPlainConfig(worktreeConfig, true)) return undefined;\n\n\tconst uid = nodeProcess.geteuid?.();\n\tif (uid != null) {\n\t\tconst owners = [(await stat(repoPath)).uid, (await lstat(dotGit)).uid, (await stat(gitDir)).uid];\n\t\tif (owners.some(o => o !== uid)) return undefined;\n\t}\n\n\tif (await mayHaveSuperproject(realRepoPath)) return undefined;\n\n\treturn { gitDir: gitDir, commonGitDir: commonGitDir };\n}\n\n/**\n * Whether a repository above `realRepoPath` could list it as a submodule. `rev-parse --show-superproject-working-tree`\n * asks the repository git finds from the parent folder; only a bare one, having no index, can't list it. Rejects when\n * a folder above can't be read.\n */\nasync function mayHaveSuperproject(realRepoPath: string): Promise<boolean> {\n\tlet folder = realRepoPath;\n\tfor (;;) {\n\t\tconst parent = dirname(folder);\n\t\tif (parent === folder) return false;\n\n\t\tfolder = parent;\n\n\t\tconst dotGit = join(folder, '.git');\n\t\tconst dotGitStats = await statIfExists(dotGit);\n\t\tif (dotGitStats?.isFile()) {\n\t\t\tconst target = parseGitFile(await readFile(dotGit, 'utf8'));\n\t\t\tif (target == null) return true;\n\n\t\t\tconst gitDir = isAbsolute(target) ? target : `${folder}/${target}`;\n\t\t\tif ((await statIfExists(join(gitDir, 'commondir'))) != null) return true;\n\n\t\t\treturn !isBareConfig(await readFile(join(gitDir, 'config'), 'utf8'));\n\t\t}\n\n\t\t// git passes over a `.git` folder, or a folder, that isn't a git dir\n\t\tif (dotGitStats?.isDirectory() && (await isGitDirectory(dotGit))) {\n\t\t\treturn !isBareConfig(await readFile(join(dotGit, 'config'), 'utf8'));\n\t\t}\n\t\tif (await isGitDirectory(folder)) return !isBareConfig(await readFile(join(folder, 'config'), 'utf8'));\n\t}\n}\n\n/** Whether a folder is a git dir: a valid `HEAD`, and the `objects` and `refs` folders (or a `commondir` to them) */\nasync function isGitDirectory(path: string): Promise<boolean> {\n\tconst head = await readFileIfExists(join(path, 'HEAD'));\n\tif (head == null || !headRegex.test(head)) return false;\n\tif ((await statIfExists(join(path, 'commondir'))) != null) return true;\n\n\treturn (\n\t\t(await statIfExists(join(path, 'objects')))?.isDirectory() === true &&\n\t\t(await statIfExists(join(path, 'refs')))?.isDirectory() === true\n\t);\n}\n\n/** Whether a config sets nothing that moves the working tree; `checkBare` also rules out a `core.bare` not plainly false */\nfunction isPlainConfig(config: string, checkBare: boolean): boolean {\n\tif (worktreeKeyRegex.test(config) || includeSectionRegex.test(config)) return false;\n\tif (!checkBare) return true;\n\n\tconst bare = getBare(config);\n\treturn bare == null || bare === false;\n}\n\n/** Whether a config is plainly a bare repository's, with no working tree set anywhere */\nfunction isBareConfig(config: string): boolean {\n\treturn !worktreeKeyRegex.test(config) && !includeSectionRegex.test(config) && getBare(config) === true;\n}\n\n/** The last `bare` value in a config: `undefined` when unset, `'unknown'` when not a plain boolean */\nfunction getBare(config: string): boolean | 'unknown' | undefined {\n\tlet bare: boolean | 'unknown' | undefined;\n\tfor (const match of config.matchAll(bareKeyRegex)) {\n\t\tconst value = match[1];\n\t\tif (value == null || boolTrueRegex.test(value)) {\n\t\t\tbare = true;\n\t\t} else if (boolFalseRegex.test(value)) {\n\t\t\tbare = false;\n\t\t} else {\n\t\t\tbare = 'unknown';\n\t\t}\n\t}\n\treturn bare;\n}\n\nasync function statIfExists(path: string): Promise<Stats | undefined> {\n\ttry {\n\t\treturn await stat(path);\n\t} catch (ex) {\n\t\tconst code = (ex as NodeJS.ErrnoException).code;\n\t\tif (code === 'ENOENT' || code === 'ENOTDIR') return undefined;\n\n\t\tthrow ex;\n\t}\n}\n\nasync function readFileIfExists(path: string): Promise<string | undefined> {\n\ttry {\n\t\treturn await readFile(path, 'utf8');\n\t} catch (ex) {\n\t\tconst code = (ex as NodeJS.ErrnoException).code;\n\t\tif (code === 'ENOENT' || code === 'ENOTDIR') return undefined;\n\n\t\tthrow ex;\n\t}\n}\n"],"mappings":"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"}
@@ -1,10 +1,10 @@
1
1
  import { fileUri, joinUriPath } from "../../utils/uri.js";
2
2
  import { FileType } from "../../git/context.js";
3
+ import { parseCommonDir, parseGitFile } from "./gitDirResolver.js";
3
4
  import { resolve } from "node:path";
4
5
  //#region ../git-cli/src/exec/headState.ts
5
6
  const headBranchRegex = /^ref:\s*(refs\/heads\/\S+)\s*$/;
6
7
  const headShaRegex = /^(?:[0-9a-f]{40}|[0-9a-f]{64})\s*$/;
7
- const gitFileRegex = /^gitdir:\s*(.+?)\s*$/m;
8
8
  /**
9
9
  * Reads `repoPath`'s HEAD from the worktree's own `HEAD` file, without running git: read around a write, it says which
10
10
  * branch that write moved, whatever a switch outside core left in the caches. The git dir is the cached one, else the
@@ -62,12 +62,12 @@ async function findGitDir(repoPath, fs) {
62
62
  uri: dotGit,
63
63
  commonUri: void 0
64
64
  };
65
- const dir = gitFileRegex.exec(new TextDecoder().decode(await fs.readFile(dotGit)))?.[1];
65
+ const dir = parseGitFile(new TextDecoder().decode(await fs.readFile(dotGit)));
66
66
  if (dir == null) return void 0;
67
67
  const gitDir = resolve(repoPath, dir);
68
68
  let commonDir;
69
69
  try {
70
- commonDir = new TextDecoder().decode(await fs.readFile(joinUriPath(fileUri(gitDir), "commondir"))).trim();
70
+ commonDir = parseCommonDir(new TextDecoder().decode(await fs.readFile(joinUriPath(fileUri(gitDir), "commondir"))));
71
71
  } catch {}
72
72
  return {
73
73
  uri: fileUri(gitDir),
@@ -1 +1 @@
1
- {"version":3,"file":"headState.js","names":["resolvePath"],"sources":["../../../../git-cli/src/exec/headState.ts"],"sourcesContent":["import { resolve as resolvePath } from 'node:path';\nimport type { Cache } from '@gitlens/git/cache.js';\nimport type { FileSystemProvider } from '@gitlens/git/context.js';\nimport { FileType } from '@gitlens/git/context.js';\nimport type { Uri } from '@gitlens/utils/uri.js';\nimport { fileUri, joinUriPath } from '@gitlens/utils/uri.js';\n\n/**\n * What a worktree's HEAD is on: the full name of its branch (`refs/heads/main`), `'detached'`, or `undefined` when\n * unknown — unreadable, not a branch name git allows, or a HEAD kept as a symlink to its branch\n * (`core.preferSymlinkRefs`). Reading such a HEAD follows the link to the branch's object id, as a detached HEAD\n * reads, so it is unknown when the git dir's listing flags it as a link, and `'detached'` on a host whose listing\n * doesn't.\n */\nexport type HeadState = { readonly branch: string } | 'detached' | undefined;\n\nconst headBranchRegex = /^ref:\\s*(refs\\/heads\\/\\S+)\\s*$/;\nconst headShaRegex = /^(?:[0-9a-f]{40}|[0-9a-f]{64})\\s*$/;\nconst gitFileRegex = /^gitdir:\\s*(.+?)\\s*$/m;\n\n/**\n * Reads `repoPath`'s HEAD from the worktree's own `HEAD` file, without running git: read around a write, it says which\n * branch that write moved, whatever a switch outside core left in the caches. The git dir is the cached one, else the\n * one `<repoPath>/.git` is or points to, so a worktree core never registered is read without spawning git either.\n * A reftable repository's `HEAD` file holds the placeholder `refs/heads/.invalid` (its real HEAD is in the tables),\n * which is no branch name git allows, so it reads as unknown, as does a branch that is a symbolic ref to another (a\n * `master` kept pointing at `main`), since git follows it and a write moves the target. An object id is detached only\n * when the git dir's listing doesn't flag `HEAD` as a symlink (a host's `stat` may follow the link): read as detached,\n * a symlinked HEAD would say a write on its branch moved no branch. Never rejects.\n */\nexport async function readHeadState(repoPath: string, fs: FileSystemProvider, cache: Cache): Promise<HeadState> {\n\ttry {\n\t\tconst cached = cache.gitDir.get(repoPath);\n\t\tconst gitDir =\n\t\t\tcached != null ? { uri: cached.uri, commonUri: cached.commonUri } : await findGitDir(repoPath, fs);\n\t\tif (gitDir == null) return undefined;\n\n\t\tconst decoder = new TextDecoder();\n\t\tconst head = decoder.decode(await fs.readFile(joinUriPath(gitDir.uri, 'HEAD')));\n\t\tif (headShaRegex.test(head)) {\n\t\t\tconst entries = await fs.readDirectory(gitDir.uri);\n\t\t\tconst linked = entries.some(([name, type]) => name === 'HEAD' && (type & FileType.SymbolicLink) !== 0);\n\t\t\treturn linked ? undefined : 'detached';\n\t\t}\n\n\t\tconst ref = headBranchRegex.exec(head)?.[1];\n\t\tif (ref == null || ref.split('/').some(part => part.startsWith('.'))) return undefined;\n\n\t\t// Packed refs can't hold a symbolic ref, so a missing loose file is a plain branch\n\t\tlet target: string | undefined;\n\t\ttry {\n\t\t\ttarget = decoder.decode(await fs.readFile(joinUriPath(gitDir.commonUri ?? gitDir.uri, ref)));\n\t\t} catch {}\n\t\treturn target?.startsWith('ref:') ? undefined : { branch: ref };\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * The path of `repoPath`'s common git dir, without running git: the cached one, else the folder `<repoPath>/.git` is,\n * or the shared one a linked worktree's `gitdir:` file leads to. `undefined` when it can't be read. Never rejects.\n */\nexport async function readCommonGitDir(\n\trepoPath: string,\n\tfs: FileSystemProvider,\n\tcache: Cache,\n): Promise<string | undefined> {\n\ttry {\n\t\tconst gitDir = cache.gitDir.get(repoPath) ?? (await findGitDir(repoPath, fs));\n\t\tif (gitDir == null) return undefined;\n\n\t\treturn (gitDir.commonUri ?? gitDir.uri).fsPath;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * The git dir `<repoPath>/.git` is: the folder itself, or the one a linked worktree's `gitdir:` file names, whose\n * `commondir` file names the repository's shared one\n */\nasync function findGitDir(\n\trepoPath: string,\n\tfs: FileSystemProvider,\n): Promise<{ readonly uri: Uri; readonly commonUri: Uri | undefined } | undefined> {\n\tconst dotGit = joinUriPath(fileUri(repoPath), '.git');\n\tconst stat = await fs.stat(dotGit);\n\tif (stat == null) return undefined;\n\tif (stat.type & FileType.Directory) return { uri: dotGit, commonUri: undefined };\n\n\tconst dir = gitFileRegex.exec(new TextDecoder().decode(await fs.readFile(dotGit)))?.[1];\n\tif (dir == null) return undefined;\n\n\tconst gitDir = resolvePath(repoPath, dir);\n\tlet commonDir: string | undefined;\n\ttry {\n\t\tcommonDir = new TextDecoder().decode(await fs.readFile(joinUriPath(fileUri(gitDir), 'commondir'))).trim();\n\t} catch {}\n\treturn { uri: fileUri(gitDir), commonUri: commonDir ? fileUri(resolvePath(gitDir, commonDir)) : undefined };\n}\n\n/**\n * Which branch a write that moves only HEAD's branch moved, from HEAD read before and after it: that branch when HEAD\n * was on the same one both times, `'detached'` (no branch) when it was detached both times, otherwise `undefined` — a\n * hook or an outside switch moved HEAD meanwhile, or a read failed, so the moved branch is unknown.\n */\nexport function getMovedHead(before: HeadState, after: HeadState): HeadState {\n\tif (before == null || after == null) return undefined;\n\tif (before === 'detached') return after === 'detached' ? 'detached' : undefined;\n\tif (after === 'detached') return undefined;\n\n\treturn before.branch === after.branch ? before : undefined;\n}\n"],"mappings":";;;;AAgBA,MAAM,kBAAkB;AACxB,MAAM,eAAe;AACrB,MAAM,eAAe;;;;;;;;;;;AAYrB,eAAsB,cAAc,UAAkB,IAAwB,OAAkC;CAC/G,IAAI;EACH,MAAM,SAAS,MAAM,OAAO,IAAI,QAAQ;EACxC,MAAM,SACL,UAAU,OAAO;GAAE,KAAK,OAAO;GAAK,WAAW,OAAO;EAAU,IAAI,MAAM,WAAW,UAAU,EAAE;EAClG,IAAI,UAAU,MAAM,OAAO,KAAA;EAE3B,MAAM,UAAU,IAAI,YAAY;EAChC,MAAM,OAAO,QAAQ,OAAO,MAAM,GAAG,SAAS,YAAY,OAAO,KAAK,MAAM,CAAC,CAAC;EAC9E,IAAI,aAAa,KAAK,IAAI,GAGzB,QADe,MADO,GAAG,cAAc,OAAO,GAAG,EAAA,CAC1B,MAAM,CAAC,MAAM,UAAU,SAAS,WAAW,OAAO,SAAS,kBAAkB,CACxF,IAAI,KAAA,IAAY;EAG7B,MAAM,MAAM,gBAAgB,KAAK,IAAI,CAAC,GAAG;EACzC,IAAI,OAAO,QAAQ,IAAI,MAAM,GAAG,CAAC,CAAC,MAAK,SAAQ,KAAK,WAAW,GAAG,CAAC,GAAG,OAAO,KAAA;EAG7E,IAAI;EACJ,IAAI;GACH,SAAS,QAAQ,OAAO,MAAM,GAAG,SAAS,YAAY,OAAO,aAAa,OAAO,KAAK,GAAG,CAAC,CAAC;EAC5F,QAAQ,CAAC;EACT,OAAO,QAAQ,WAAW,MAAM,IAAI,KAAA,IAAY,EAAE,QAAQ,IAAI;CAC/D,QAAQ;EACP;CACD;AACD;;;;;AAMA,eAAsB,iBACrB,UACA,IACA,OAC8B;CAC9B,IAAI;EACH,MAAM,SAAS,MAAM,OAAO,IAAI,QAAQ,KAAM,MAAM,WAAW,UAAU,EAAE;EAC3E,IAAI,UAAU,MAAM,OAAO,KAAA;EAE3B,QAAQ,OAAO,aAAa,OAAO,IAAA,CAAK;CACzC,QAAQ;EACP;CACD;AACD;;;;;AAMA,eAAe,WACd,UACA,IACkF;CAClF,MAAM,SAAS,YAAY,QAAQ,QAAQ,GAAG,MAAM;CACpD,MAAM,OAAO,MAAM,GAAG,KAAK,MAAM;CACjC,IAAI,QAAQ,MAAM,OAAO,KAAA;CACzB,IAAI,KAAK,OAAO,SAAS,WAAW,OAAO;EAAE,KAAK;EAAQ,WAAW,KAAA;CAAU;CAE/E,MAAM,MAAM,aAAa,KAAK,IAAI,YAAY,CAAC,CAAC,OAAO,MAAM,GAAG,SAAS,MAAM,CAAC,CAAC,CAAC,GAAG;CACrF,IAAI,OAAO,MAAM,OAAO,KAAA;CAExB,MAAM,SAASA,QAAY,UAAU,GAAG;CACxC,IAAI;CACJ,IAAI;EACH,YAAY,IAAI,YAAY,CAAC,CAAC,OAAO,MAAM,GAAG,SAAS,YAAY,QAAQ,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,KAAK;CACzG,QAAQ,CAAC;CACT,OAAO;EAAE,KAAK,QAAQ,MAAM;EAAG,WAAW,YAAY,QAAQA,QAAY,QAAQ,SAAS,CAAC,IAAI,KAAA;CAAU;AAC3G;;;;;;AAOA,SAAgB,aAAa,QAAmB,OAA6B;CAC5E,IAAI,UAAU,QAAQ,SAAS,MAAM,OAAO,KAAA;CAC5C,IAAI,WAAW,YAAY,OAAO,UAAU,aAAa,aAAa,KAAA;CACtE,IAAI,UAAU,YAAY,OAAO,KAAA;CAEjC,OAAO,OAAO,WAAW,MAAM,SAAS,SAAS,KAAA;AAClD"}
1
+ {"version":3,"file":"headState.js","names":["resolvePath"],"sources":["../../../../git-cli/src/exec/headState.ts"],"sourcesContent":["import { resolve as resolvePath } from 'node:path';\nimport type { Cache } from '@gitlens/git/cache.js';\nimport type { FileSystemProvider } from '@gitlens/git/context.js';\nimport { FileType } from '@gitlens/git/context.js';\nimport type { Uri } from '@gitlens/utils/uri.js';\nimport { fileUri, joinUriPath } from '@gitlens/utils/uri.js';\nimport { parseCommonDir, parseGitFile } from './gitDirResolver.js';\n\n/**\n * What a worktree's HEAD is on: the full name of its branch (`refs/heads/main`), `'detached'`, or `undefined` when\n * unknown — unreadable, not a branch name git allows, or a HEAD kept as a symlink to its branch\n * (`core.preferSymlinkRefs`). Reading such a HEAD follows the link to the branch's object id, as a detached HEAD\n * reads, so it is unknown when the git dir's listing flags it as a link, and `'detached'` on a host whose listing\n * doesn't.\n */\nexport type HeadState = { readonly branch: string } | 'detached' | undefined;\n\nconst headBranchRegex = /^ref:\\s*(refs\\/heads\\/\\S+)\\s*$/;\nconst headShaRegex = /^(?:[0-9a-f]{40}|[0-9a-f]{64})\\s*$/;\n\n/**\n * Reads `repoPath`'s HEAD from the worktree's own `HEAD` file, without running git: read around a write, it says which\n * branch that write moved, whatever a switch outside core left in the caches. The git dir is the cached one, else the\n * one `<repoPath>/.git` is or points to, so a worktree core never registered is read without spawning git either.\n * A reftable repository's `HEAD` file holds the placeholder `refs/heads/.invalid` (its real HEAD is in the tables),\n * which is no branch name git allows, so it reads as unknown, as does a branch that is a symbolic ref to another (a\n * `master` kept pointing at `main`), since git follows it and a write moves the target. An object id is detached only\n * when the git dir's listing doesn't flag `HEAD` as a symlink (a host's `stat` may follow the link): read as detached,\n * a symlinked HEAD would say a write on its branch moved no branch. Never rejects.\n */\nexport async function readHeadState(repoPath: string, fs: FileSystemProvider, cache: Cache): Promise<HeadState> {\n\ttry {\n\t\tconst cached = cache.gitDir.get(repoPath);\n\t\tconst gitDir =\n\t\t\tcached != null ? { uri: cached.uri, commonUri: cached.commonUri } : await findGitDir(repoPath, fs);\n\t\tif (gitDir == null) return undefined;\n\n\t\tconst decoder = new TextDecoder();\n\t\tconst head = decoder.decode(await fs.readFile(joinUriPath(gitDir.uri, 'HEAD')));\n\t\tif (headShaRegex.test(head)) {\n\t\t\tconst entries = await fs.readDirectory(gitDir.uri);\n\t\t\tconst linked = entries.some(([name, type]) => name === 'HEAD' && (type & FileType.SymbolicLink) !== 0);\n\t\t\treturn linked ? undefined : 'detached';\n\t\t}\n\n\t\tconst ref = headBranchRegex.exec(head)?.[1];\n\t\tif (ref == null || ref.split('/').some(part => part.startsWith('.'))) return undefined;\n\n\t\t// Packed refs can't hold a symbolic ref, so a missing loose file is a plain branch\n\t\tlet target: string | undefined;\n\t\ttry {\n\t\t\ttarget = decoder.decode(await fs.readFile(joinUriPath(gitDir.commonUri ?? gitDir.uri, ref)));\n\t\t} catch {}\n\t\treturn target?.startsWith('ref:') ? undefined : { branch: ref };\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * The path of `repoPath`'s common git dir, without running git: the cached one, else the folder `<repoPath>/.git` is,\n * or the shared one a linked worktree's `gitdir:` file leads to. `undefined` when it can't be read. Never rejects.\n */\nexport async function readCommonGitDir(\n\trepoPath: string,\n\tfs: FileSystemProvider,\n\tcache: Cache,\n): Promise<string | undefined> {\n\ttry {\n\t\tconst gitDir = cache.gitDir.get(repoPath) ?? (await findGitDir(repoPath, fs));\n\t\tif (gitDir == null) return undefined;\n\n\t\treturn (gitDir.commonUri ?? gitDir.uri).fsPath;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * The git dir `<repoPath>/.git` is: the folder itself, or the one a linked worktree's `gitdir:` file names, whose\n * `commondir` file names the repository's shared one\n */\nasync function findGitDir(\n\trepoPath: string,\n\tfs: FileSystemProvider,\n): Promise<{ readonly uri: Uri; readonly commonUri: Uri | undefined } | undefined> {\n\tconst dotGit = joinUriPath(fileUri(repoPath), '.git');\n\tconst stat = await fs.stat(dotGit);\n\tif (stat == null) return undefined;\n\tif (stat.type & FileType.Directory) return { uri: dotGit, commonUri: undefined };\n\n\tconst dir = parseGitFile(new TextDecoder().decode(await fs.readFile(dotGit)));\n\tif (dir == null) return undefined;\n\n\tconst gitDir = resolvePath(repoPath, dir);\n\tlet commonDir: string | undefined;\n\ttry {\n\t\tcommonDir = parseCommonDir(\n\t\t\tnew TextDecoder().decode(await fs.readFile(joinUriPath(fileUri(gitDir), 'commondir'))),\n\t\t);\n\t} catch {}\n\treturn { uri: fileUri(gitDir), commonUri: commonDir ? fileUri(resolvePath(gitDir, commonDir)) : undefined };\n}\n\n/**\n * Which branch a write that moves only HEAD's branch moved, from HEAD read before and after it: that branch when HEAD\n * was on the same one both times, `'detached'` (no branch) when it was detached both times, otherwise `undefined` — a\n * hook or an outside switch moved HEAD meanwhile, or a read failed, so the moved branch is unknown.\n */\nexport function getMovedHead(before: HeadState, after: HeadState): HeadState {\n\tif (before == null || after == null) return undefined;\n\tif (before === 'detached') return after === 'detached' ? 'detached' : undefined;\n\tif (after === 'detached') return undefined;\n\n\treturn before.branch === after.branch ? before : undefined;\n}\n"],"mappings":";;;;;AAiBA,MAAM,kBAAkB;AACxB,MAAM,eAAe;;;;;;;;;;;AAYrB,eAAsB,cAAc,UAAkB,IAAwB,OAAkC;CAC/G,IAAI;EACH,MAAM,SAAS,MAAM,OAAO,IAAI,QAAQ;EACxC,MAAM,SACL,UAAU,OAAO;GAAE,KAAK,OAAO;GAAK,WAAW,OAAO;EAAU,IAAI,MAAM,WAAW,UAAU,EAAE;EAClG,IAAI,UAAU,MAAM,OAAO,KAAA;EAE3B,MAAM,UAAU,IAAI,YAAY;EAChC,MAAM,OAAO,QAAQ,OAAO,MAAM,GAAG,SAAS,YAAY,OAAO,KAAK,MAAM,CAAC,CAAC;EAC9E,IAAI,aAAa,KAAK,IAAI,GAGzB,QADe,MADO,GAAG,cAAc,OAAO,GAAG,EAAA,CAC1B,MAAM,CAAC,MAAM,UAAU,SAAS,WAAW,OAAO,SAAS,kBAAkB,CACxF,IAAI,KAAA,IAAY;EAG7B,MAAM,MAAM,gBAAgB,KAAK,IAAI,CAAC,GAAG;EACzC,IAAI,OAAO,QAAQ,IAAI,MAAM,GAAG,CAAC,CAAC,MAAK,SAAQ,KAAK,WAAW,GAAG,CAAC,GAAG,OAAO,KAAA;EAG7E,IAAI;EACJ,IAAI;GACH,SAAS,QAAQ,OAAO,MAAM,GAAG,SAAS,YAAY,OAAO,aAAa,OAAO,KAAK,GAAG,CAAC,CAAC;EAC5F,QAAQ,CAAC;EACT,OAAO,QAAQ,WAAW,MAAM,IAAI,KAAA,IAAY,EAAE,QAAQ,IAAI;CAC/D,QAAQ;EACP;CACD;AACD;;;;;AAMA,eAAsB,iBACrB,UACA,IACA,OAC8B;CAC9B,IAAI;EACH,MAAM,SAAS,MAAM,OAAO,IAAI,QAAQ,KAAM,MAAM,WAAW,UAAU,EAAE;EAC3E,IAAI,UAAU,MAAM,OAAO,KAAA;EAE3B,QAAQ,OAAO,aAAa,OAAO,IAAA,CAAK;CACzC,QAAQ;EACP;CACD;AACD;;;;;AAMA,eAAe,WACd,UACA,IACkF;CAClF,MAAM,SAAS,YAAY,QAAQ,QAAQ,GAAG,MAAM;CACpD,MAAM,OAAO,MAAM,GAAG,KAAK,MAAM;CACjC,IAAI,QAAQ,MAAM,OAAO,KAAA;CACzB,IAAI,KAAK,OAAO,SAAS,WAAW,OAAO;EAAE,KAAK;EAAQ,WAAW,KAAA;CAAU;CAE/E,MAAM,MAAM,aAAa,IAAI,YAAY,CAAC,CAAC,OAAO,MAAM,GAAG,SAAS,MAAM,CAAC,CAAC;CAC5E,IAAI,OAAO,MAAM,OAAO,KAAA;CAExB,MAAM,SAASA,QAAY,UAAU,GAAG;CACxC,IAAI;CACJ,IAAI;EACH,YAAY,eACX,IAAI,YAAY,CAAC,CAAC,OAAO,MAAM,GAAG,SAAS,YAAY,QAAQ,MAAM,GAAG,WAAW,CAAC,CAAC,CACtF;CACD,QAAQ,CAAC;CACT,OAAO;EAAE,KAAK,QAAQ,MAAM;EAAG,WAAW,YAAY,QAAQA,QAAY,QAAQ,SAAS,CAAC,IAAI,KAAA;CAAU;AAC3G;;;;;;AAOA,SAAgB,aAAa,QAAmB,OAA6B;CAC5E,IAAI,UAAU,QAAQ,SAAS,MAAM,OAAO,KAAA;CAC5C,IAAI,WAAW,YAAY,OAAO,UAAU,aAAa,aAAa,KAAA;CACtE,IAAI,UAAU,YAAY,OAAO,KAAA;CAEjC,OAAO,OAAO,WAAW,MAAM,SAAS,SAAS,KAAA;AAClD"}
@@ -0,0 +1,78 @@
1
+ import { Git } from "./git.js";
2
+ //#region ../git-cli/src/exec/indexRefresh.d.ts
3
+ export interface IndexRefresherOptions {
4
+ /** Runs the refreshing `git status` */
5
+ git: Pick<Git, 'run'>;
6
+ /** The index file's modified time; `undefined` when it can't be read (no checkout, no index) */
7
+ statIndexMtimeMs: (checkoutPath: string) => Promise<number | undefined>;
8
+ /**
9
+ * Whether {@link IndexRefresher.refreshAfter} refreshes after core's own writes; off by default, since each refresh is
10
+ * a status of the whole checkout, worth it only where that checkout will be read again. {@link IndexRefresher.schedule}
11
+ * runs either way.
12
+ */
13
+ afterWrites?: boolean;
14
+ now?: () => number;
15
+ /** Calls `fn` after `ms` and returns a function that cancels it; the default timer doesn't keep the process alive */
16
+ setTimer?: (fn: () => void, ms: number) => () => void;
17
+ }
18
+ /**
19
+ * The refresh's own cap, far past git's default: a status that re-hashes a large checkout can run for minutes and must
20
+ * be allowed to finish, while one stuck on a stalled file system would otherwise hold a git slot, and keep that
21
+ * checkout from ever being refreshed again, for as long as the process lives
22
+ */
23
+ export declare const indexRefreshTimeoutMs: number;
24
+ /**
25
+ * Rewrites a checkout's index once, in the background, after git has written a batch of its files.
26
+ *
27
+ * A write that touches many files (a clone, a worktree add, a branch switch, a fast-forward) stamps them in the same
28
+ * second as the index file, and git compares timestamps at whole-second precision: an entry whose second is not
29
+ * earlier than the index file's is "racily clean", so git can't trust it, and every later status or diff re-hashes the
30
+ * file for as long as nothing rewrites the index. Core's own status reads run with `GIT_OPTIONAL_LOCKS=0`, so they
31
+ * never write the refreshed index back.
32
+ *
33
+ * The refresh waits until the clock is in a later second than the index file's: one that rewrote the index in the same
34
+ * second as the last file written would leave the entries racy all the same. It is a `status`, not `update-index
35
+ * --refresh`: the latter holds the index lock for the whole re-hash, so a writer in the checkout meanwhile (a reset, a
36
+ * first `git add`) would fail with "index.lock exists", whereas a status locks only for the short write at the end and
37
+ * skips that write, still exiting 0, when someone else holds the lock. Optional locks are forced on for it because that
38
+ * write is the whole point, applied over the inherited environment so a user's own `GIT_OPTIONAL_LOCKS=0` can't defeat it.
39
+ *
40
+ * One queue per checkout: a schedule while the refresh is waiting joins it, since the status hasn't read the tree yet,
41
+ * and the wait is taken again from the index as that write left it; one while the status runs causes exactly one more
42
+ * refresh after it, however many arrive, since the write may have landed after the status read the tree. Nothing here
43
+ * throws, nor does a checkout removed meanwhile.
44
+ */
45
+ export declare class IndexRefresher {
46
+ private readonly options;
47
+ private readonly _entries;
48
+ private _disposed;
49
+ private readonly _now;
50
+ private readonly _setTimer;
51
+ constructor(options: IndexRefresherOptions);
52
+ /** Cancels the waits and stops anything more from starting; a status already running is left to finish */
53
+ dispose(): void;
54
+ /** The refresh waiting or running for a checkout, if any */
55
+ pending(checkoutPath: string): Promise<void> | undefined;
56
+ /** Starts a refresh of the checkout's index in the background, or joins the one in progress. Never throws. */
57
+ schedule(checkoutPath: string): void;
58
+ /**
59
+ * Runs a write, then schedules a refresh when it rewrote the checkout's index, which is the sign that it rewrote
60
+ * files; a write that touched none (already up to date, a switch to the commit already checked out) leaves the index
61
+ * alone and schedules none. A write that fails is checked the same way before its error is rethrown untouched: a
62
+ * merge or rebase that stops on conflicts has rewritten the clean files and the index first, and those are read the
63
+ * most while the conflicts are resolved, whereas a checkout or merge refused outright leaves the index alone and
64
+ * schedules none (a stash apply or push rewrites it first, so schedules one even when refused or with nothing to
65
+ * save). Only the check is awaited, not the refresh it may schedule.
66
+ *
67
+ * `checkoutPath` is the checkout the write rewrote, which isn't always the one it ran in. Without
68
+ * {@link IndexRefresherOptions.afterWrites}, it only runs the write.
69
+ */
70
+ refreshAfter<T>(checkoutPath: string, write: () => Promise<T>): Promise<T>;
71
+ private scheduleIfIndexRewritten;
72
+ private run;
73
+ private refreshOnce;
74
+ }
75
+ /** {@link IndexRefresher.refreshAfter}, or only the write when there is no refresher */
76
+ export declare function refreshIndexAfter<T>(refresher: IndexRefresher | undefined, checkoutPath: string, write: () => Promise<T>): Promise<T>;
77
+ //#endregion
78
+ //# sourceMappingURL=indexRefresh.d.ts.map
@@ -0,0 +1,159 @@
1
+ import { normalizePath } from "../../utils/path.js";
2
+ import { gitConfigsStatus } from "./git.js";
3
+ //#region ../git-cli/src/exec/indexRefresh.ts
4
+ /**
5
+ * The refresh's own cap, far past git's default: a status that re-hashes a large checkout can run for minutes and must
6
+ * be allowed to finish, while one stuck on a stalled file system would otherwise hold a git slot, and keep that
7
+ * checkout from ever being refreshed again, for as long as the process lives
8
+ */
9
+ const indexRefreshTimeoutMs = 6e5;
10
+ function setUnrefTimer(fn, ms) {
11
+ const timer = setTimeout(fn, ms);
12
+ timer.unref();
13
+ return () => clearTimeout(timer);
14
+ }
15
+ /**
16
+ * Rewrites a checkout's index once, in the background, after git has written a batch of its files.
17
+ *
18
+ * A write that touches many files (a clone, a worktree add, a branch switch, a fast-forward) stamps them in the same
19
+ * second as the index file, and git compares timestamps at whole-second precision: an entry whose second is not
20
+ * earlier than the index file's is "racily clean", so git can't trust it, and every later status or diff re-hashes the
21
+ * file for as long as nothing rewrites the index. Core's own status reads run with `GIT_OPTIONAL_LOCKS=0`, so they
22
+ * never write the refreshed index back.
23
+ *
24
+ * The refresh waits until the clock is in a later second than the index file's: one that rewrote the index in the same
25
+ * second as the last file written would leave the entries racy all the same. It is a `status`, not `update-index
26
+ * --refresh`: the latter holds the index lock for the whole re-hash, so a writer in the checkout meanwhile (a reset, a
27
+ * first `git add`) would fail with "index.lock exists", whereas a status locks only for the short write at the end and
28
+ * skips that write, still exiting 0, when someone else holds the lock. Optional locks are forced on for it because that
29
+ * write is the whole point, applied over the inherited environment so a user's own `GIT_OPTIONAL_LOCKS=0` can't defeat it.
30
+ *
31
+ * One queue per checkout: a schedule while the refresh is waiting joins it, since the status hasn't read the tree yet,
32
+ * and the wait is taken again from the index as that write left it; one while the status runs causes exactly one more
33
+ * refresh after it, however many arrive, since the write may have landed after the status read the tree. Nothing here
34
+ * throws, nor does a checkout removed meanwhile.
35
+ */
36
+ var IndexRefresher = class {
37
+ options;
38
+ _entries = /* @__PURE__ */ new Map();
39
+ _disposed = false;
40
+ _now;
41
+ _setTimer;
42
+ constructor(options) {
43
+ this.options = options;
44
+ this._now = options.now ?? Date.now;
45
+ this._setTimer = options.setTimer ?? setUnrefTimer;
46
+ }
47
+ /** Cancels the waits and stops anything more from starting; a status already running is left to finish */
48
+ dispose() {
49
+ this._disposed = true;
50
+ for (const entry of this._entries.values()) entry.cancelWait?.();
51
+ }
52
+ /** The refresh waiting or running for a checkout, if any */
53
+ pending(checkoutPath) {
54
+ return this._entries.get(normalizePath(checkoutPath))?.task;
55
+ }
56
+ /** Starts a refresh of the checkout's index in the background, or joins the one in progress. Never throws. */
57
+ schedule(checkoutPath) {
58
+ if (this._disposed) return;
59
+ const key = normalizePath(checkoutPath);
60
+ const entry = this._entries.get(key);
61
+ if (entry == null) {
62
+ const created = {
63
+ path: checkoutPath,
64
+ task: Promise.resolve(),
65
+ running: false,
66
+ again: false,
67
+ joined: false,
68
+ cancelWait: void 0
69
+ };
70
+ this._entries.set(key, created);
71
+ created.task = this.run(key, created);
72
+ return;
73
+ }
74
+ if (entry.running) entry.again = true;
75
+ else entry.joined = true;
76
+ }
77
+ /**
78
+ * Runs a write, then schedules a refresh when it rewrote the checkout's index, which is the sign that it rewrote
79
+ * files; a write that touched none (already up to date, a switch to the commit already checked out) leaves the index
80
+ * alone and schedules none. A write that fails is checked the same way before its error is rethrown untouched: a
81
+ * merge or rebase that stops on conflicts has rewritten the clean files and the index first, and those are read the
82
+ * most while the conflicts are resolved, whereas a checkout or merge refused outright leaves the index alone and
83
+ * schedules none (a stash apply or push rewrites it first, so schedules one even when refused or with nothing to
84
+ * save). Only the check is awaited, not the refresh it may schedule.
85
+ *
86
+ * `checkoutPath` is the checkout the write rewrote, which isn't always the one it ran in. Without
87
+ * {@link IndexRefresherOptions.afterWrites}, it only runs the write.
88
+ */
89
+ async refreshAfter(checkoutPath, write) {
90
+ if (!this.options.afterWrites) return write();
91
+ const startedAt = this._now();
92
+ try {
93
+ return await write();
94
+ } finally {
95
+ await this.scheduleIfIndexRewritten(checkoutPath, startedAt);
96
+ }
97
+ }
98
+ async scheduleIfIndexRewritten(checkoutPath, writeStartedAt) {
99
+ try {
100
+ const mtimeMs = await this.options.statIndexMtimeMs(checkoutPath);
101
+ if (mtimeMs != null && mtimeMs >= Math.floor(writeStartedAt / 1e3) * 1e3) this.schedule(checkoutPath);
102
+ } catch {}
103
+ }
104
+ async run(key, entry) {
105
+ try {
106
+ do {
107
+ entry.again = false;
108
+ entry.running = false;
109
+ await this.refreshOnce(entry);
110
+ } while (entry.again && !this._disposed);
111
+ } catch {} finally {
112
+ this._entries.delete(key);
113
+ }
114
+ }
115
+ async refreshOnce(entry) {
116
+ const checkoutPath = entry.path;
117
+ do {
118
+ entry.joined = false;
119
+ const mtimeMs = await this.options.statIndexMtimeMs(checkoutPath);
120
+ if (mtimeMs == null) return;
121
+ const laterSecondMs = (Math.floor(mtimeMs / 1e3) + 1) * 1e3;
122
+ const deadlineMs = Math.min(laterSecondMs, this._now() + laterSecondMs - mtimeMs);
123
+ for (let wait = deadlineMs - this._now(); wait > 0; wait = deadlineMs - this._now()) {
124
+ if (this._disposed) return;
125
+ await new Promise((resolve) => {
126
+ const cancel = this._setTimer(resolve, wait);
127
+ entry.cancelWait = () => {
128
+ cancel();
129
+ resolve();
130
+ };
131
+ });
132
+ entry.cancelWait = void 0;
133
+ }
134
+ if (this._disposed) return;
135
+ } while (entry.joined);
136
+ entry.running = true;
137
+ await this.options.git.run({
138
+ cwd: checkoutPath,
139
+ configs: [
140
+ ...gitConfigsStatus,
141
+ "-c",
142
+ "status.renames=false"
143
+ ],
144
+ env: { GIT_OPTIONAL_LOCKS: "1" },
145
+ errors: "ignore",
146
+ priority: "background",
147
+ selfMaintenance: true,
148
+ timeout: indexRefreshTimeoutMs
149
+ }, "status", "--porcelain", "-uno", "--ignore-submodules=all");
150
+ }
151
+ };
152
+ /** {@link IndexRefresher.refreshAfter}, or only the write when there is no refresher */
153
+ function refreshIndexAfter(refresher, checkoutPath, write) {
154
+ return refresher != null ? refresher.refreshAfter(checkoutPath, write) : write();
155
+ }
156
+ //#endregion
157
+ export { IndexRefresher, indexRefreshTimeoutMs, refreshIndexAfter };
158
+
159
+ //# sourceMappingURL=indexRefresh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"indexRefresh.js","names":[],"sources":["../../../../git-cli/src/exec/indexRefresh.ts"],"sourcesContent":["import { normalizePath } from '@gitlens/utils/path.js';\nimport type { Git } from './git.js';\nimport { gitConfigsStatus } from './git.js';\n\nexport interface IndexRefresherOptions {\n\t/** Runs the refreshing `git status` */\n\tgit: Pick<Git, 'run'>;\n\t/** The index file's modified time; `undefined` when it can't be read (no checkout, no index) */\n\tstatIndexMtimeMs: (checkoutPath: string) => Promise<number | undefined>;\n\t/**\n\t * Whether {@link IndexRefresher.refreshAfter} refreshes after core's own writes; off by default, since each refresh is\n\t * a status of the whole checkout, worth it only where that checkout will be read again. {@link IndexRefresher.schedule}\n\t * runs either way.\n\t */\n\tafterWrites?: boolean;\n\tnow?: () => number;\n\t/** Calls `fn` after `ms` and returns a function that cancels it; the default timer doesn't keep the process alive */\n\tsetTimer?: (fn: () => void, ms: number) => () => void;\n}\n\ninterface Entry {\n\t/** The checkout's path as the first schedule gave it, which the provider's caches are keyed by */\n\treadonly path: string;\n\ttask: Promise<void>;\n\t/** Whether the status has started: a write that lands after that may have missed it */\n\trunning: boolean;\n\t/** Whether another write was scheduled while the status was running */\n\tagain: boolean;\n\t/** Whether another write was scheduled while the refresh waited: it may have stamped the index in a later second */\n\tjoined: boolean;\n\t/** Ends the wait for the clock early, while there is one */\n\tcancelWait: (() => void) | undefined;\n}\n\n/**\n * The refresh's own cap, far past git's default: a status that re-hashes a large checkout can run for minutes and must\n * be allowed to finish, while one stuck on a stalled file system would otherwise hold a git slot, and keep that\n * checkout from ever being refreshed again, for as long as the process lives\n */\nexport const indexRefreshTimeoutMs = 10 * 60 * 1000;\n\nfunction setUnrefTimer(fn: () => void, ms: number): () => void {\n\tconst timer = setTimeout(fn, ms);\n\ttimer.unref();\n\treturn () => clearTimeout(timer);\n}\n\n/**\n * Rewrites a checkout's index once, in the background, after git has written a batch of its files.\n *\n * A write that touches many files (a clone, a worktree add, a branch switch, a fast-forward) stamps them in the same\n * second as the index file, and git compares timestamps at whole-second precision: an entry whose second is not\n * earlier than the index file's is \"racily clean\", so git can't trust it, and every later status or diff re-hashes the\n * file for as long as nothing rewrites the index. Core's own status reads run with `GIT_OPTIONAL_LOCKS=0`, so they\n * never write the refreshed index back.\n *\n * The refresh waits until the clock is in a later second than the index file's: one that rewrote the index in the same\n * second as the last file written would leave the entries racy all the same. It is a `status`, not `update-index\n * --refresh`: the latter holds the index lock for the whole re-hash, so a writer in the checkout meanwhile (a reset, a\n * first `git add`) would fail with \"index.lock exists\", whereas a status locks only for the short write at the end and\n * skips that write, still exiting 0, when someone else holds the lock. Optional locks are forced on for it because that\n * write is the whole point, applied over the inherited environment so a user's own `GIT_OPTIONAL_LOCKS=0` can't defeat it.\n *\n * One queue per checkout: a schedule while the refresh is waiting joins it, since the status hasn't read the tree yet,\n * and the wait is taken again from the index as that write left it; one while the status runs causes exactly one more\n * refresh after it, however many arrive, since the write may have landed after the status read the tree. Nothing here\n * throws, nor does a checkout removed meanwhile.\n */\nexport class IndexRefresher {\n\tprivate readonly _entries = new Map<string, Entry>();\n\tprivate _disposed = false;\n\tprivate readonly _now: () => number;\n\tprivate readonly _setTimer: (fn: () => void, ms: number) => () => void;\n\n\tconstructor(private readonly options: IndexRefresherOptions) {\n\t\tthis._now = options.now ?? Date.now;\n\t\tthis._setTimer = options.setTimer ?? setUnrefTimer;\n\t}\n\n\t/** Cancels the waits and stops anything more from starting; a status already running is left to finish */\n\tdispose(): void {\n\t\tthis._disposed = true;\n\t\tfor (const entry of this._entries.values()) {\n\t\t\tentry.cancelWait?.();\n\t\t}\n\t}\n\n\t/** The refresh waiting or running for a checkout, if any */\n\tpending(checkoutPath: string): Promise<void> | undefined {\n\t\treturn this._entries.get(normalizePath(checkoutPath))?.task;\n\t}\n\n\t/** Starts a refresh of the checkout's index in the background, or joins the one in progress. Never throws. */\n\tschedule(checkoutPath: string): void {\n\t\tif (this._disposed) return;\n\n\t\tconst key = normalizePath(checkoutPath);\n\t\tconst entry = this._entries.get(key);\n\t\tif (entry == null) {\n\t\t\tconst created: Entry = {\n\t\t\t\tpath: checkoutPath,\n\t\t\t\ttask: Promise.resolve(),\n\t\t\t\trunning: false,\n\t\t\t\tagain: false,\n\t\t\t\tjoined: false,\n\t\t\t\tcancelWait: undefined,\n\t\t\t};\n\t\t\tthis._entries.set(key, created);\n\t\t\tcreated.task = this.run(key, created);\n\t\t\treturn;\n\t\t}\n\n\t\tif (entry.running) {\n\t\t\tentry.again = true;\n\t\t} else {\n\t\t\tentry.joined = true;\n\t\t}\n\t}\n\n\t/**\n\t * Runs a write, then schedules a refresh when it rewrote the checkout's index, which is the sign that it rewrote\n\t * files; a write that touched none (already up to date, a switch to the commit already checked out) leaves the index\n\t * alone and schedules none. A write that fails is checked the same way before its error is rethrown untouched: a\n\t * merge or rebase that stops on conflicts has rewritten the clean files and the index first, and those are read the\n\t * most while the conflicts are resolved, whereas a checkout or merge refused outright leaves the index alone and\n\t * schedules none (a stash apply or push rewrites it first, so schedules one even when refused or with nothing to\n\t * save). Only the check is awaited, not the refresh it may schedule.\n\t *\n\t * `checkoutPath` is the checkout the write rewrote, which isn't always the one it ran in. Without\n\t * {@link IndexRefresherOptions.afterWrites}, it only runs the write.\n\t */\n\tasync refreshAfter<T>(checkoutPath: string, write: () => Promise<T>): Promise<T> {\n\t\tif (!this.options.afterWrites) return write();\n\n\t\tconst startedAt = this._now();\n\t\ttry {\n\t\t\treturn await write();\n\t\t} finally {\n\t\t\tawait this.scheduleIfIndexRewritten(checkoutPath, startedAt);\n\t\t}\n\t}\n\n\tprivate async scheduleIfIndexRewritten(checkoutPath: string, writeStartedAt: number): Promise<void> {\n\t\ttry {\n\t\t\tconst mtimeMs = await this.options.statIndexMtimeMs(checkoutPath);\n\t\t\tif (mtimeMs != null && mtimeMs >= Math.floor(writeStartedAt / 1000) * 1000) {\n\t\t\t\tthis.schedule(checkoutPath);\n\t\t\t}\n\t\t} catch {}\n\t}\n\n\tprivate async run(key: string, entry: Entry): Promise<void> {\n\t\ttry {\n\t\t\tdo {\n\t\t\t\tentry.again = false;\n\t\t\t\tentry.running = false;\n\t\t\t\tawait this.refreshOnce(entry);\n\t\t\t} while (entry.again && !this._disposed);\n\t\t} catch {\n\t\t\t// Best-effort: a failed refresh only leaves the entries as untrusted as they were\n\t\t} finally {\n\t\t\tthis._entries.delete(key);\n\t\t}\n\t}\n\n\tprivate async refreshOnce(entry: Entry): Promise<void> {\n\t\tconst checkoutPath = entry.path;\n\t\t// A write that joined while this waited may have stamped the index in a later second than the one waited out\n\t\tdo {\n\t\t\tentry.joined = false;\n\t\t\tconst mtimeMs = await this.options.statIndexMtimeMs(checkoutPath);\n\t\t\tif (mtimeMs == null) return;\n\n\t\t\t// The file system can stamp the index ahead of this clock (a network or VM-shared folder, a clock set back),\n\t\t\t// and waiting for this clock to reach that second could take any time. The file system's own clock was at\n\t\t\t// least at the stamp when the stat read it, so it has passed that second once the rest of the second has gone\n\t\t\t// by since\n\t\t\tconst laterSecondMs = (Math.floor(mtimeMs / 1000) + 1) * 1000;\n\t\t\tconst deadlineMs = Math.min(laterSecondMs, this._now() + laterSecondMs - mtimeMs);\n\t\t\tfor (let wait = deadlineMs - this._now(); wait > 0; wait = deadlineMs - this._now()) {\n\t\t\t\tif (this._disposed) return;\n\n\t\t\t\tawait new Promise<void>(resolve => {\n\t\t\t\t\tconst cancel = this._setTimer(resolve, wait);\n\t\t\t\t\tentry.cancelWait = () => {\n\t\t\t\t\t\tcancel();\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t};\n\t\t\t\t});\n\t\t\t\tentry.cancelWait = undefined;\n\t\t\t}\n\t\t\tif (this._disposed) return;\n\t\t} while (entry.joined);\n\n\t\tentry.running = true;\n\t\tawait this.options.git.run(\n\t\t\t{\n\t\t\t\tcwd: checkoutPath,\n\t\t\t\t// Skips the rename detection between HEAD and the index, which the refresh doesn't need; older git ignores it\n\t\t\t\tconfigs: [...gitConfigsStatus, '-c', 'status.renames=false'],\n\t\t\t\tenv: { GIT_OPTIONAL_LOCKS: '1' },\n\t\t\t\terrors: 'ignore',\n\t\t\t\tpriority: 'background',\n\t\t\t\tselfMaintenance: true,\n\t\t\t\ttimeout: indexRefreshTimeoutMs,\n\t\t\t},\n\t\t\t'status',\n\t\t\t'--porcelain',\n\t\t\t'-uno',\n\t\t\t// The index refreshes all the same; this only skips the status git runs inside each submodule\n\t\t\t'--ignore-submodules=all',\n\t\t);\n\t}\n}\n\n/** {@link IndexRefresher.refreshAfter}, or only the write when there is no refresher */\nexport function refreshIndexAfter<T>(\n\trefresher: IndexRefresher | undefined,\n\tcheckoutPath: string,\n\twrite: () => Promise<T>,\n): Promise<T> {\n\treturn refresher != null ? refresher.refreshAfter(checkoutPath, write) : write();\n}\n"],"mappings":";;;;;;;;AAuCA,MAAa,wBAAwB;AAErC,SAAS,cAAc,IAAgB,IAAwB;CAC9D,MAAM,QAAQ,WAAW,IAAI,EAAE;CAC/B,MAAM,MAAM;CACZ,aAAa,aAAa,KAAK;AAChC;;;;;;;;;;;;;;;;;;;;;;AAuBA,IAAa,iBAAb,MAA4B;CAME;CAL7B,2BAA4B,IAAI,IAAmB;CACnD,YAAoB;CACpB;CACA;CAEA,YAAY,SAAiD;EAAhC,KAAA,UAAA;EAC5B,KAAK,OAAO,QAAQ,OAAO,KAAK;EAChC,KAAK,YAAY,QAAQ,YAAY;CACtC;;CAGA,UAAgB;EACf,KAAK,YAAY;EACjB,KAAK,MAAM,SAAS,KAAK,SAAS,OAAO,GACxC,MAAM,aAAa;CAErB;;CAGA,QAAQ,cAAiD;EACxD,OAAO,KAAK,SAAS,IAAI,cAAc,YAAY,CAAC,CAAC,EAAE;CACxD;;CAGA,SAAS,cAA4B;EACpC,IAAI,KAAK,WAAW;EAEpB,MAAM,MAAM,cAAc,YAAY;EACtC,MAAM,QAAQ,KAAK,SAAS,IAAI,GAAG;EACnC,IAAI,SAAS,MAAM;GAClB,MAAM,UAAiB;IACtB,MAAM;IACN,MAAM,QAAQ,QAAQ;IACtB,SAAS;IACT,OAAO;IACP,QAAQ;IACR,YAAY,KAAA;GACb;GACA,KAAK,SAAS,IAAI,KAAK,OAAO;GAC9B,QAAQ,OAAO,KAAK,IAAI,KAAK,OAAO;GACpC;EACD;EAEA,IAAI,MAAM,SACT,MAAM,QAAQ;OAEd,MAAM,SAAS;CAEjB;;;;;;;;;;;;;CAcA,MAAM,aAAgB,cAAsB,OAAqC;EAChF,IAAI,CAAC,KAAK,QAAQ,aAAa,OAAO,MAAM;EAE5C,MAAM,YAAY,KAAK,KAAK;EAC5B,IAAI;GACH,OAAO,MAAM,MAAM;EACpB,UAAU;GACT,MAAM,KAAK,yBAAyB,cAAc,SAAS;EAC5D;CACD;CAEA,MAAc,yBAAyB,cAAsB,gBAAuC;EACnG,IAAI;GACH,MAAM,UAAU,MAAM,KAAK,QAAQ,iBAAiB,YAAY;GAChE,IAAI,WAAW,QAAQ,WAAW,KAAK,MAAM,iBAAiB,GAAI,IAAI,KACrE,KAAK,SAAS,YAAY;EAE5B,QAAQ,CAAC;CACV;CAEA,MAAc,IAAI,KAAa,OAA6B;EAC3D,IAAI;GACH,GAAG;IACF,MAAM,QAAQ;IACd,MAAM,UAAU;IAChB,MAAM,KAAK,YAAY,KAAK;GAC7B,SAAS,MAAM,SAAS,CAAC,KAAK;EAC/B,QAAQ,CAER,UAAU;GACT,KAAK,SAAS,OAAO,GAAG;EACzB;CACD;CAEA,MAAc,YAAY,OAA6B;EACtD,MAAM,eAAe,MAAM;EAE3B,GAAG;GACF,MAAM,SAAS;GACf,MAAM,UAAU,MAAM,KAAK,QAAQ,iBAAiB,YAAY;GAChE,IAAI,WAAW,MAAM;GAMrB,MAAM,iBAAiB,KAAK,MAAM,UAAU,GAAI,IAAI,KAAK;GACzD,MAAM,aAAa,KAAK,IAAI,eAAe,KAAK,KAAK,IAAI,gBAAgB,OAAO;GAChF,KAAK,IAAI,OAAO,aAAa,KAAK,KAAK,GAAG,OAAO,GAAG,OAAO,aAAa,KAAK,KAAK,GAAG;IACpF,IAAI,KAAK,WAAW;IAEpB,MAAM,IAAI,SAAc,YAAW;KAClC,MAAM,SAAS,KAAK,UAAU,SAAS,IAAI;KAC3C,MAAM,mBAAmB;MACxB,OAAO;MACP,QAAQ;KACT;IACD,CAAC;IACD,MAAM,aAAa,KAAA;GACpB;GACA,IAAI,KAAK,WAAW;EACrB,SAAS,MAAM;EAEf,MAAM,UAAU;EAChB,MAAM,KAAK,QAAQ,IAAI,IACtB;GACC,KAAK;GAEL,SAAS;IAAC,GAAG;IAAkB;IAAM;GAAsB;GAC3D,KAAK,EAAE,oBAAoB,IAAI;GAC/B,QAAQ;GACR,UAAU;GACV,iBAAiB;GACjB,SAAS;EACV,GACA,UACA,eACA,QAEA,yBACD;CACD;AACD;;AAGA,SAAgB,kBACf,WACA,cACA,OACa;CACb,OAAO,aAAa,OAAO,UAAU,aAAa,cAAc,KAAK,IAAI,MAAM;AAChF"}
@@ -43,12 +43,9 @@ async function resolveBranchRemote(provider, repoPath, ref, cancellation) {
43
43
  const split = splitRemoteBranchName(ref.name, remoteNames);
44
44
  if (split != null) [remote, name] = split;
45
45
  }
46
- if (ref.upstream != null) {
46
+ if (!ref.remote && ref.upstream != null) {
47
47
  const split = splitRemoteBranchName(ref.upstream.name, remoteNames);
48
- if (split != null) {
49
- if (!ref.remote) remote = split[0];
50
- upstream = split[1];
51
- }
48
+ if (split != null) [remote, upstream] = split;
52
49
  }
53
50
  }
54
51
  if (!ref.remote && remote != null && !remoteNames.includes(remote)) remote = void 0;
@@ -1 +1 @@
1
- {"version":3,"file":"branchRemote.js","names":[],"sources":["../../../../git-cli/src/providers/branchRemote.ts"],"sourcesContent":["import type { GitBranchReference } from '@gitlens/git/models/reference.js';\nimport {\n\tgetBranchNameAndRemote,\n\tgetBranchTrackingWithoutRemote,\n\tsplitRemoteBranchName,\n} from '@gitlens/git/utils/branch.utils.js';\nimport type { CliGitProviderInternal } from '../cliGitProvider.js';\n\n/**\n * Splits a branch reference into its name, remote and upstream branch. A remote's own name can contain\n * a `/`, so the cheap first-`/` split only stands when it leaves no further `/` to be ambiguous.\n *\n * Git reports whether an upstream is a local branch on every version, and a local upstream never has a\n * remote, so it is never split (`release/2.0` is not remote `release`).\n *\n * A reference that carries neither `local` nor `remote` on its upstream (one a host built itself) is looked\n * up fresh in the branch list, since guessing from the name can't tell a local `release/2.0` from remote\n * `release`'s `2.0`.\n *\n * A remote a local branch's reference carries may have been removed since the reference was read, and\n * git would then take the stale name as a path, so it must be one of the repository's remotes. A failed\n * remote read (`getRemotes` answers `[]`) refuses too: a retry beats a push to a path.\n */\nexport async function resolveBranchRemote(\n\tprovider: Pick<CliGitProviderInternal, 'remotes' | 'branches'>,\n\trepoPath: string,\n\tref: GitBranchReference,\n\tcancellation?: AbortSignal,\n): Promise<{ name: string; remote: string | undefined; upstream: string | undefined; local: boolean }> {\n\tif (!ref.remote && ref.upstream != null && !ref.upstream.local && ref.upstream.remote == null) {\n\t\tconst fresh = await provider.branches.getBranch(repoPath, ref.name, undefined, cancellation);\n\t\t// `getBranch` matches by short name, which a remote-tracking ref can share. A missing upstream is one the\n\t\t// caller is about to set, which the branch doesn't track yet\n\t\tif (fresh != null && !fresh.remote && (fresh.upstream != null || !ref.upstream.missing)) {\n\t\t\tref = fresh;\n\t\t}\n\t}\n\n\tlet [name, remote] = getBranchNameAndRemote(ref);\n\tlet upstream = getBranchTrackingWithoutRemote(ref);\n\n\tconst local = !ref.remote && ref.upstream?.local === true;\n\tif (local) return { name: name, remote: undefined, upstream: upstream, local: true };\n\n\tconst needsSplit = !ref.upstream?.remote && (ref.remote ? name : upstream)?.includes('/');\n\tconst needsVerify = !ref.remote && remote != null;\n\tif (!needsSplit && !needsVerify) return { name: name, remote: remote, upstream: upstream, local: local };\n\n\tconst remoteNames = (await provider.remotes.getRemotes(repoPath, undefined, cancellation)).map(r => r.name);\n\n\tif (needsSplit) {\n\t\tif (ref.remote) {\n\t\t\tconst split = splitRemoteBranchName(ref.name, remoteNames);\n\t\t\tif (split != null) {\n\t\t\t\t[remote, name] = split;\n\t\t\t}\n\t\t}\n\n\t\tif (ref.upstream != null) {\n\t\t\tconst split = splitRemoteBranchName(ref.upstream.name, remoteNames);\n\t\t\tif (split != null) {\n\t\t\t\tif (!ref.remote) {\n\t\t\t\t\tremote = split[0];\n\t\t\t\t}\n\t\t\t\tupstream = split[1];\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!ref.remote && remote != null && !remoteNames.includes(remote)) {\n\t\tremote = undefined;\n\t}\n\n\treturn { name: name, remote: remote, upstream: upstream, local: local };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAuBA,eAAsB,oBACrB,UACA,UACA,KACA,cACsG;CACtG,IAAI,CAAC,IAAI,UAAU,IAAI,YAAY,QAAQ,CAAC,IAAI,SAAS,SAAS,IAAI,SAAS,UAAU,MAAM;EAC9F,MAAM,QAAQ,MAAM,SAAS,SAAS,UAAU,UAAU,IAAI,MAAM,KAAA,GAAW,YAAY;EAG3F,IAAI,SAAS,QAAQ,CAAC,MAAM,WAAW,MAAM,YAAY,QAAQ,CAAC,IAAI,SAAS,UAC9E,MAAM;CAER;CAEA,IAAI,CAAC,MAAM,UAAU,uBAAuB,GAAG;CAC/C,IAAI,WAAW,+BAA+B,GAAG;CAEjD,MAAM,QAAQ,CAAC,IAAI,UAAU,IAAI,UAAU,UAAU;CACrD,IAAI,OAAO,OAAO;EAAQ;EAAM,QAAQ,KAAA;EAAqB;EAAU,OAAO;CAAK;CAEnF,MAAM,aAAa,CAAC,IAAI,UAAU,WAAW,IAAI,SAAS,OAAO,SAAA,EAAW,SAAS,GAAG;CACxF,MAAM,cAAc,CAAC,IAAI,UAAU,UAAU;CAC7C,IAAI,CAAC,cAAc,CAAC,aAAa,OAAO;EAAQ;EAAc;EAAkB;EAAiB;CAAM;CAEvG,MAAM,eAAe,MAAM,SAAS,QAAQ,WAAW,UAAU,KAAA,GAAW,YAAY,EAAA,CAAG,KAAI,MAAK,EAAE,IAAI;CAE1G,IAAI,YAAY;EACf,IAAI,IAAI,QAAQ;GACf,MAAM,QAAQ,sBAAsB,IAAI,MAAM,WAAW;GACzD,IAAI,SAAS,MACZ,CAAC,QAAQ,QAAQ;EAEnB;EAEA,IAAI,IAAI,YAAY,MAAM;GACzB,MAAM,QAAQ,sBAAsB,IAAI,SAAS,MAAM,WAAW;GAClE,IAAI,SAAS,MAAM;IAClB,IAAI,CAAC,IAAI,QACR,SAAS,MAAM;IAEhB,WAAW,MAAM;GAClB;EACD;CACD;CAEA,IAAI,CAAC,IAAI,UAAU,UAAU,QAAQ,CAAC,YAAY,SAAS,MAAM,GAChE,SAAS,KAAA;CAGV,OAAO;EAAQ;EAAc;EAAkB;EAAiB;CAAM;AACvE"}
1
+ {"version":3,"file":"branchRemote.js","names":[],"sources":["../../../../git-cli/src/providers/branchRemote.ts"],"sourcesContent":["import type { GitBranchReference } from '@gitlens/git/models/reference.js';\nimport {\n\tgetBranchNameAndRemote,\n\tgetBranchTrackingWithoutRemote,\n\tsplitRemoteBranchName,\n} from '@gitlens/git/utils/branch.utils.js';\nimport type { CliGitProviderInternal } from '../cliGitProvider.js';\n\n/**\n * Splits a branch reference into its name, remote and upstream branch. A remote's own name can contain\n * a `/`, so the cheap first-`/` split only stands when it leaves no further `/` to be ambiguous.\n *\n * Git reports whether an upstream is a local branch on every version, and a local upstream never has a\n * remote, so it is never split (`release/2.0` is not remote `release`).\n *\n * A reference that carries neither `local` nor `remote` on its upstream (one a host built itself) is looked\n * up fresh in the branch list, since guessing from the name can't tell a local `release/2.0` from remote\n * `release`'s `2.0`.\n *\n * A remote a local branch's reference carries may have been removed since the reference was read, and\n * git would then take the stale name as a path, so it must be one of the repository's remotes. A failed\n * remote read (`getRemotes` answers `[]`) refuses too: a retry beats a push to a path.\n */\nexport async function resolveBranchRemote(\n\tprovider: Pick<CliGitProviderInternal, 'remotes' | 'branches'>,\n\trepoPath: string,\n\tref: GitBranchReference,\n\tcancellation?: AbortSignal,\n): Promise<{ name: string; remote: string | undefined; upstream: string | undefined; local: boolean }> {\n\tif (!ref.remote && ref.upstream != null && !ref.upstream.local && ref.upstream.remote == null) {\n\t\tconst fresh = await provider.branches.getBranch(repoPath, ref.name, undefined, cancellation);\n\t\t// `getBranch` matches by short name, which a remote-tracking ref can share. A missing upstream is one the\n\t\t// caller is about to set, which the branch doesn't track yet\n\t\tif (fresh != null && !fresh.remote && (fresh.upstream != null || !ref.upstream.missing)) {\n\t\t\tref = fresh;\n\t\t}\n\t}\n\n\tlet [name, remote] = getBranchNameAndRemote(ref);\n\tlet upstream = getBranchTrackingWithoutRemote(ref);\n\n\tconst local = !ref.remote && ref.upstream?.local === true;\n\tif (local) return { name: name, remote: undefined, upstream: upstream, local: true };\n\n\tconst needsSplit = !ref.upstream?.remote && (ref.remote ? name : upstream)?.includes('/');\n\tconst needsVerify = !ref.remote && remote != null;\n\tif (!needsSplit && !needsVerify) return { name: name, remote: remote, upstream: upstream, local: local };\n\n\tconst remoteNames = (await provider.remotes.getRemotes(repoPath, undefined, cancellation)).map(r => r.name);\n\n\tif (needsSplit) {\n\t\tif (ref.remote) {\n\t\t\tconst split = splitRemoteBranchName(ref.name, remoteNames);\n\t\t\tif (split != null) {\n\t\t\t\t[remote, name] = split;\n\t\t\t}\n\t\t}\n\n\t\t// A remote-tracking branch has no upstream (see `getBranchTrackingWithoutRemote`)\n\t\tif (!ref.remote && ref.upstream != null) {\n\t\t\tconst split = splitRemoteBranchName(ref.upstream.name, remoteNames);\n\t\t\tif (split != null) {\n\t\t\t\t[remote, upstream] = split;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!ref.remote && remote != null && !remoteNames.includes(remote)) {\n\t\tremote = undefined;\n\t}\n\n\treturn { name: name, remote: remote, upstream: upstream, local: local };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAuBA,eAAsB,oBACrB,UACA,UACA,KACA,cACsG;CACtG,IAAI,CAAC,IAAI,UAAU,IAAI,YAAY,QAAQ,CAAC,IAAI,SAAS,SAAS,IAAI,SAAS,UAAU,MAAM;EAC9F,MAAM,QAAQ,MAAM,SAAS,SAAS,UAAU,UAAU,IAAI,MAAM,KAAA,GAAW,YAAY;EAG3F,IAAI,SAAS,QAAQ,CAAC,MAAM,WAAW,MAAM,YAAY,QAAQ,CAAC,IAAI,SAAS,UAC9E,MAAM;CAER;CAEA,IAAI,CAAC,MAAM,UAAU,uBAAuB,GAAG;CAC/C,IAAI,WAAW,+BAA+B,GAAG;CAEjD,MAAM,QAAQ,CAAC,IAAI,UAAU,IAAI,UAAU,UAAU;CACrD,IAAI,OAAO,OAAO;EAAQ;EAAM,QAAQ,KAAA;EAAqB;EAAU,OAAO;CAAK;CAEnF,MAAM,aAAa,CAAC,IAAI,UAAU,WAAW,IAAI,SAAS,OAAO,SAAA,EAAW,SAAS,GAAG;CACxF,MAAM,cAAc,CAAC,IAAI,UAAU,UAAU;CAC7C,IAAI,CAAC,cAAc,CAAC,aAAa,OAAO;EAAQ;EAAc;EAAkB;EAAiB;CAAM;CAEvG,MAAM,eAAe,MAAM,SAAS,QAAQ,WAAW,UAAU,KAAA,GAAW,YAAY,EAAA,CAAG,KAAI,MAAK,EAAE,IAAI;CAE1G,IAAI,YAAY;EACf,IAAI,IAAI,QAAQ;GACf,MAAM,QAAQ,sBAAsB,IAAI,MAAM,WAAW;GACzD,IAAI,SAAS,MACZ,CAAC,QAAQ,QAAQ;EAEnB;EAGA,IAAI,CAAC,IAAI,UAAU,IAAI,YAAY,MAAM;GACxC,MAAM,QAAQ,sBAAsB,IAAI,SAAS,MAAM,WAAW;GAClE,IAAI,SAAS,MACZ,CAAC,QAAQ,YAAY;EAEvB;CACD;CAEA,IAAI,CAAC,IAAI,UAAU,UAAU,QAAQ,CAAC,YAAY,SAAS,MAAM,GAChE,SAAS,KAAA;CAGV,OAAO;EAAQ;EAAc;EAAkB;EAAiB;CAAM;AACvE"}