arcane-os 0.91.0 → 0.92.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.
- package/CHANGELOG.md +16 -0
- package/README.md +1 -1
- package/docs/reference/core-repositories.md +87 -4
- package/docs/reference/git-text-snapshot.md +47 -3
- package/docs/reference/inventory/package-api.json +5 -5
- package/docs/reference/sdk-api.md +52 -7
- package/package.json +1 -1
- package/src/core/repositories.mjs +60 -3
- package/src/git-text-snapshot.mjs +25 -5
- package/src/process.mjs +25 -1
- package/src/repository.mjs +25 -1
- package/src/windows-git-executable.mjs +103 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.92.0
|
|
4
|
+
|
|
5
|
+
- Add opt-in `allowUnborn` Git text snapshots and `initialBranch` repository
|
|
6
|
+
preparation for first publication from a remote with no advertised revision.
|
|
7
|
+
Repository configuration now reports the actual revision and unborn state.
|
|
8
|
+
Preserve existing checkouts, advertised refs, complete diagnostics and real
|
|
9
|
+
Git failures; first publication uses the existing explicit non-force writer.
|
|
10
|
+
- Let the default Windows `git` process runner discover an installed Git for
|
|
11
|
+
Windows registration and append its command directory only to that child's
|
|
12
|
+
PATH. Existing lookup priority, other platforms, explicit executable paths
|
|
13
|
+
and custom adapters retain their behavior; no installation, global setting
|
|
14
|
+
change or command retry is introduced.
|
|
15
|
+
- Reuse the retained Windows host, Whisper and ONNX runtime archives. This
|
|
16
|
+
release changes shared JavaScript repository/process behavior, not those
|
|
17
|
+
native binaries; application adoption remains independently owned.
|
|
18
|
+
|
|
3
19
|
## 0.91.0
|
|
4
20
|
|
|
5
21
|
- Persist complete native `modelAssets.prepare()` acquisitions across release,
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ selected SDK projection or an explicit live SDK source mount. Both profiles
|
|
|
25
25
|
share the theme, packaging, event, cancellation,
|
|
26
26
|
and browser run contracts while retaining their selected application layout.
|
|
27
27
|
|
|
28
|
-
This checkout defines the `0.
|
|
28
|
+
This checkout defines the `0.92.0` SDK contract. Development applications declare
|
|
29
29
|
`arcane-os` as `latest` and retain their lockfile. Exact SDK pins begin when Roshi
|
|
30
30
|
designates a production build. Follow [Consumer updates](docs/consumer-updates.md)
|
|
31
31
|
to refresh and compare the installed SDK and lockfile with the published channel;
|
|
@@ -47,7 +47,7 @@ const status = await repository.status({signal});
|
|
|
47
47
|
await repository.close();
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
`createRepositoryWorkspace({name,directory,dataRoot,remote,remoteBase,branch,longPaths,cloneIdentity,gitIdentity,onEvent,run}={})`
|
|
50
|
+
`createRepositoryWorkspace({name,directory,dataRoot,remote,remoteBase,branch,initialBranch,longPaths,cloneIdentity,gitIdentity,onEvent,run}={})`
|
|
51
51
|
returns `{directory,open,status,configuration,pull,push,write,close,drain,dispose}`. The `directory`
|
|
52
52
|
property is the absolute selected working path.
|
|
53
53
|
|
|
@@ -63,6 +63,10 @@ property is the absolute selected working path.
|
|
|
63
63
|
`branch`, when supplied, is passed unchanged to Git's `clone --branch` option;
|
|
64
64
|
otherwise Git selects the remote's default branch. Existing checkouts keep
|
|
65
65
|
their own branch, remote, tracked files, untracked files and local changes.
|
|
66
|
+
- Optional `initialBranch` names the branch for first publication from a new
|
|
67
|
+
unborn clone. It is separate from `branch`, which selects an existing remote
|
|
68
|
+
branch or tag. Omit `branch` when the remote has no advertised revision.
|
|
69
|
+
See [first publication](#first-publication-from-an-unborn-remote).
|
|
66
70
|
- Supplying `remoteBase` explicitly declares this initial-clone `remote` to be
|
|
67
71
|
a local filesystem locator. Its native absolute command argument is captured
|
|
68
72
|
during construction. Omission leaves Git's original string interpretation
|
|
@@ -77,7 +81,11 @@ property is the absolute selected working path.
|
|
|
77
81
|
keep Git's ordinary inheritance; existing checkouts retain their configuration.
|
|
78
82
|
See [clone-local author selection](#clone-local-author-selection).
|
|
79
83
|
- `onEvent` and optional `run` use the existing SDK process owner. The default
|
|
80
|
-
adapter is `runProcess`; Git
|
|
84
|
+
adapter is `runProcess`; Git can be available on PATH or through an installed
|
|
85
|
+
Git for Windows registration. The default Windows runner appends that
|
|
86
|
+
installation's `cmd` directory only to the Git child's PATH, preserving
|
|
87
|
+
existing command lookup priority. See [Windows Git discovery](sdk-api.md#runprocess).
|
|
88
|
+
Custom `run` adapters retain their own execution behavior. Credentials and
|
|
81
89
|
authentication remain with Git and the native host. The SDK adds no download,
|
|
82
90
|
credential store, polling, network retry or process supervisor.
|
|
83
91
|
|
|
@@ -147,6 +155,8 @@ Its result is:
|
|
|
147
155
|
{
|
|
148
156
|
repositoryRoot,
|
|
149
157
|
headRef: fullSymbolicRef | null,
|
|
158
|
+
revision: currentCommit | null,
|
|
159
|
+
unborn: boolean,
|
|
150
160
|
origin: {urls: [...], pushUrls: [...]},
|
|
151
161
|
upstream: {remoteNames: [...], mergeRefs: [...], urls: [...], pushUrls: [...]} | null
|
|
152
162
|
}
|
|
@@ -160,6 +170,16 @@ arrays contain that branch's configured `remote` and `merge` values; `urls` and
|
|
|
160
170
|
means the local repository and has no remote URL arrays. Origin is observed
|
|
161
171
|
independently, even when it is not the upstream.
|
|
162
172
|
|
|
173
|
+
`revision` and `unborn` come from Git's successful
|
|
174
|
+
[porcelain-v2 branch observation](https://git-scm.com/docs/git-status#_porcelain_format_version_2):
|
|
175
|
+
`(initial)` yields `revision:null, unborn:true`; a current commit yields that
|
|
176
|
+
opaque revision and `unborn:false`, including a commit with an empty tree and
|
|
177
|
+
detached HEAD. Symbolic HEAD alone does not establish an unborn branch.
|
|
178
|
+
The command uses `--no-optional-locks` and omits untracked-file discovery, so
|
|
179
|
+
this observation does not refresh the index on disk. A missing branch-state
|
|
180
|
+
record reports `ARCANE_REPOSITORY_HEAD_UNAVAILABLE`; unsuccessful commands
|
|
181
|
+
retain their actual failures rather than becoming an initial state.
|
|
182
|
+
|
|
163
183
|
Arrays retain configured order, repeated values, complete UTF-8 strings and
|
|
164
184
|
empty strings. `[]` means unset; `['']` means an explicit empty value. The
|
|
165
185
|
reader honors configured includes and ordinary Git configuration precedence.
|
|
@@ -172,8 +192,8 @@ Metadata uses the shared raw-Git-text reader. Lifecycle/stderr events remain
|
|
|
172
192
|
with the process owner, raw stdout remains parser-owned, and undecodable text
|
|
173
193
|
reports `ARCANE_GIT_CONFIGURATION_NOT_TEXT`. Failures retain complete raw
|
|
174
194
|
stdout, process diagnostics and causal errors using the identity reader's
|
|
175
|
-
documented attachment behavior. One observation uses
|
|
176
|
-
commands for an attached local branch, or
|
|
195
|
+
documented attachment behavior. One observation uses five read-only Git
|
|
196
|
+
commands for an attached local branch, or four otherwise. Results are not a
|
|
177
197
|
transaction with later work or external Git activity. The app owns any decision
|
|
178
198
|
that the current checkout corresponds to its selected repository and branch.
|
|
179
199
|
|
|
@@ -212,6 +232,69 @@ observe, refresh, write and publish. See Git's [pull](https://git-scm.com/docs/g
|
|
|
212
232
|
[push](https://git-scm.com/docs/git-push) and [configuration](https://git-scm.com/docs/git-config)
|
|
213
233
|
contracts.
|
|
214
234
|
|
|
235
|
+
### First publication from an unborn remote
|
|
236
|
+
|
|
237
|
+
An opt-in [snapshot refresh](git-text-snapshot.md#unborn-remotes) can report
|
|
238
|
+
`{revision:null,files:[],unborn:true}` after a successful complete empty remote
|
|
239
|
+
advertisement. This means no revision was advertised to this connection; it
|
|
240
|
+
does not assert that hidden refs or unreachable objects cannot exist.
|
|
241
|
+
The application retains that explicit state instead of inventing a commit.
|
|
242
|
+
|
|
243
|
+
For an application-selected new connection with that state, prepare its first
|
|
244
|
+
branch without passing the existing-branch `clone --branch` option:
|
|
245
|
+
|
|
246
|
+
```javascript
|
|
247
|
+
const repository = createRepositoryWorkspace({
|
|
248
|
+
name: connection.name,
|
|
249
|
+
remote: connection.locator,
|
|
250
|
+
remoteBase: connection.localBaseDirectory,
|
|
251
|
+
initialBranch: 'main'
|
|
252
|
+
});
|
|
253
|
+
await repository.open({signal});
|
|
254
|
+
const observed = await repository.configuration({signal});
|
|
255
|
+
// The application matches observed.headRef, unborn and configured URLs to its connection.
|
|
256
|
+
const publication = await repository.write({
|
|
257
|
+
files: selectedFiles,
|
|
258
|
+
message: authoredCommitMessage,
|
|
259
|
+
target: {remote: connection.locator, remoteBase: connection.localBaseDirectory, ref: 'refs/heads/main'},
|
|
260
|
+
signal
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
`initialBranch` is captured during construction. After a successful new clone,
|
|
265
|
+
the SDK observes the complete remote advertisement using the clone's original
|
|
266
|
+
working-directory context and captured remote argument. Only empty successful
|
|
267
|
+
output followed by the new checkout's explicit initial state and successful
|
|
268
|
+
empty `git for-each-ref --format=%(refname)` observation selects
|
|
269
|
+
`HEAD` as `refs/heads/<initialBranch>` through
|
|
270
|
+
[Git symbolic-ref](https://git-scm.com/docs/git-symbolic-ref). Git owns branch-name
|
|
271
|
+
validity. Advertised HEAD, branches or tags keep ordinary clone selection;
|
|
272
|
+
an already committed clone and every existing destination remain unchanged.
|
|
273
|
+
Any local ref supplied by Git's template or hook behavior is preserved and
|
|
274
|
+
leaves HEAD unchanged, even when the remote advertises nothing.
|
|
275
|
+
Omission adds no process and preserves the previous open result.
|
|
276
|
+
|
|
277
|
+
For a new clone with `initialBranch`, `open` retains its clone `stdout`/`stderr`
|
|
278
|
+
and adds ordered `outputs:[{operation,stdout,stderr,code,signal}]`. Operations
|
|
279
|
+
are `clone`, `observeRemote`, `observeHead`, `observeRefs` and `selectInitialBranch`, only as
|
|
280
|
+
performed. A later preparation failure preserves completed outputs in
|
|
281
|
+
`error.details:{directory,cloned:true,outputs}` and the original complete
|
|
282
|
+
failure as `cause`; cancellation retains completed observations too. A writer
|
|
283
|
+
that prepares first retains these processes in its existing `stage:'prepare'`
|
|
284
|
+
output records. Failed preparation leaves the actual directory intact and does
|
|
285
|
+
not retry initialization or rename an existing checkout on the next call.
|
|
286
|
+
|
|
287
|
+
There is nothing to pull before the first commit of a confirmed new unborn
|
|
288
|
+
connection. Use the existing exact-file `write` and explicit non-force
|
|
289
|
+
`HEAD:refs/heads/main` publication directly. For a committed remote, retain the
|
|
290
|
+
ordinary selected-branch clone and targeted pull/write composition. An existing
|
|
291
|
+
local checkout, different local branch or intervening remote change needs the
|
|
292
|
+
application's own connection decision; an unborn observation is not permission
|
|
293
|
+
to discard or switch it. Observations and publication are not transactional:
|
|
294
|
+
concurrent remote changes, authentication failures and rejected pushes remain
|
|
295
|
+
actual errors. No pull failure is converted into success, and no retry, force,
|
|
296
|
+
rollback, global setting or fabricated revision is introduced.
|
|
297
|
+
|
|
215
298
|
### Relative local repository locators
|
|
216
299
|
|
|
217
300
|
`remoteBase` is an explicit local-path choice, not a hint for a URL parser.
|
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
from one fetched Git revision. It uses a dedicated **bare object cache**, not a
|
|
5
5
|
clone with a working checkout. It never reads uncommitted application files,
|
|
6
6
|
checks out files, modifies a user checkout/index, or creates another worktree.
|
|
7
|
-
Construction performs no I/O. The native host needs Git available on its PATH
|
|
8
|
-
Git
|
|
7
|
+
Construction performs no I/O. The native host needs Git available on its PATH
|
|
8
|
+
or an installed Git for Windows registration discoverable by the default
|
|
9
|
+
[process runner](sdk-api.md#runprocess). Discovery extends only that Git child's
|
|
10
|
+
PATH after its existing entries. Git owns remote access, authentication and
|
|
11
|
+
its actual failures; custom process adapters retain their own execution.
|
|
9
12
|
|
|
10
13
|
```js
|
|
11
14
|
import {createGitTextSnapshot} from 'arcane-os';
|
|
@@ -33,7 +36,7 @@ policy stable for the lifetime of its snapshot owner.
|
|
|
33
36
|
|
|
34
37
|
## API and lifecycle
|
|
35
38
|
|
|
36
|
-
`createGitTextSnapshot({cacheDirectory,remote,remoteBase,ref,selectPath,gitIdentity,onEvent,run})`
|
|
39
|
+
`createGitTextSnapshot({cacheDirectory,remote,remoteBase,ref,selectPath,allowUnborn=false,gitIdentity,onEvent,run})`
|
|
37
40
|
returns `{refresh,close,drain,dispose}`. `run` defaults to the SDK's existing
|
|
38
41
|
`runProcess`; applications normally omit it. It is the same process adapter
|
|
39
42
|
contract, not a second process supervisor. `onEvent` is the ordinary SDK event
|
|
@@ -91,6 +94,47 @@ application owner; this factory does not install a daemon or cross-process lock.
|
|
|
91
94
|
An empty cache is initialized once. An existing non-bare checkout is rejected
|
|
92
95
|
without repurposing it. The cache is retained when the owner closes.
|
|
93
96
|
|
|
97
|
+
## Unborn remotes
|
|
98
|
+
|
|
99
|
+
Optional `allowUnborn:true` adds an explicit no-advertised-revision state:
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
const snapshot = createGitTextSnapshot({
|
|
103
|
+
cacheDirectory: applicationCacheDirectory,
|
|
104
|
+
remote: applicationRepository,
|
|
105
|
+
ref: 'refs/heads/main',
|
|
106
|
+
selectPath: applicationSelectPath,
|
|
107
|
+
allowUnborn: true
|
|
108
|
+
});
|
|
109
|
+
const observed = await snapshot.refresh({signal});
|
|
110
|
+
// Empty advertisement: {revision: null, files: [], unborn: true}.
|
|
111
|
+
// Selected commit: {revision: actualCommit, files: selectedText, unborn: false}.
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Each opted-in acquisition first completes unfiltered
|
|
115
|
+
`git ls-remote --symref -- <remote>` with the same captured locator and identity.
|
|
116
|
+
Only successful empty stdout produces the explicit unborn result. No revision
|
|
117
|
+
is invented and no fetch, tree listing or blob process runs on that path.
|
|
118
|
+
The [Git remote advertisement](https://git-scm.com/docs/git-ls-remote) is scoped
|
|
119
|
+
to refs visible to that connection, not hidden refs or unreachable objects.
|
|
120
|
+
|
|
121
|
+
Any advertised HEAD, branch, tag or other output continues through the ordinary
|
|
122
|
+
selected-ref fetch and commit resolution. A missing selected ref on a populated
|
|
123
|
+
remote remains a Git failure. Failed observations, authentication/transport
|
|
124
|
+
errors and stderr wording never establish an unborn result. A real commit with
|
|
125
|
+
an empty tree or zero selected files retains its actual revision and
|
|
126
|
+
`unborn:false`. Omitting the boolean, or supplying `false`, preserves the
|
|
127
|
+
previous `{revision,files}` result and command sequence without a remote probe.
|
|
128
|
+
|
|
129
|
+
Bare-cache role discovery still precedes remote observation. Every later
|
|
130
|
+
refresh observes again, so prior empty or committed results cannot hide a
|
|
131
|
+
changed remote or a failure. Caller cancellation, shared acquisition, independent
|
|
132
|
+
result copies and drain remain unchanged. Opted-in completion events add
|
|
133
|
+
`unborn`; `revision:null` and `unborn:true` describe the explicit empty state.
|
|
134
|
+
This option adds one remote command per acquisition; existing revision-based
|
|
135
|
+
text reuse still applies to committed results. For writing the first commit,
|
|
136
|
+
see [first-publication composition](core-repositories.md#first-publication-from-an-unborn-remote).
|
|
137
|
+
|
|
94
138
|
## Complete text and observable failures
|
|
95
139
|
|
|
96
140
|
Content is the exact Git blob text, including whitespace, CRLF, newlines,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"sdkVersion": "0.
|
|
3
|
+
"sdkVersion": "0.91.0",
|
|
4
4
|
"environment": {
|
|
5
5
|
"runtime": "Node.js for Node entrypoints; browser for browser-only entrypoints",
|
|
6
6
|
"minimumVersion": "22.23.2 for Node entrypoints",
|
|
@@ -311,14 +311,14 @@
|
|
|
311
311
|
"name": "createRepositoryWorkspace",
|
|
312
312
|
"displayName": "createRepositoryWorkspace()",
|
|
313
313
|
"kind": "function",
|
|
314
|
-
"signature": "createRepositoryWorkspace({name,directory,dataRoot,remote,remoteBase,branch,longPaths,cloneIdentity,gitIdentity,onEvent,run=runProcess}={})",
|
|
314
|
+
"signature": "createRepositoryWorkspace({name,directory,dataRoot,remote,remoteBase,branch,initialBranch,longPaths,cloneIdentity,gitIdentity,onEvent,run=runProcess}={})",
|
|
315
315
|
"entrypoints": ["arcane-os/core/repositories"],
|
|
316
316
|
"primaryImport": "arcane-os/core/repositories",
|
|
317
317
|
"group": "Native repository workspaces",
|
|
318
318
|
"summary": "Owns one selected native working checkout with ArcaneData/Repos defaults and existing Git/process lifecycle.",
|
|
319
319
|
"availability": "Node with Git on Windows, Linux and macOS; Android requires host process adaptation",
|
|
320
320
|
"protocol": "Git working repository and SDK process adapter",
|
|
321
|
-
"normalization": "Returns directory, open, status, configuration, pull, push, write and identical close/drain/dispose hooks. Only missing or empty destinations clone; existing roots are preserved. Optional factory remoteBase explicitly selects the initial-clone remote as a local filesystem locator and captures path.resolve(remoteBase, remote) once at construction. A relative base, including an empty string, uses the construction-time working directory. Only the Git argument is derived; the caller's original locator remains unchanged. Omission preserves every Git remote name, URL, SCP-like address and remote-helper string without guessed parsing. Optional longPaths captures a boolean: explicit Windows true/false supplies clone-local core.longpaths before the first checkout, persisting only in the new repository under ordinary Git configuration precedence. Omission preserves configuration; existing repositories and non-Windows clone commands remain unchanged. No global setting or migration. configuration observes the existing root, symbolic HEAD, origin URL/pushURL arrays and branch remote/merge/upstream URL arrays without clone/init/fetch, preserving unset, empty and repeated values. Explicit paths remain selected. Same-directory operations serialize within the process; cancellation and shutdown join accepted process work. pull/push/write accept a captured target {remote,ref,remoteBase?} for current-branch fast-forward pull or non-force current-HEAD selected-ref push; omission retains ordinary configured behavior. An operation's own remoteBase declares local-path intent and captures its native absolute argument once at acceptance, before queueing; factory remote/remoteBase/branch remain independent clone inputs. Targeted pull/push results and writer results/failure outcomes retain the original remote, ref and supplied remoteBase, including relative or empty base text. Unrepresentable base text reports TypeError without transformation. write snapshots exact text and paths, stages/commits only literal owned paths and preserves unrelated staged files and Git filters. Complete outputs and causal failures retain confirmed local/committed/pushed and uncertain outcomes plus the supplied target. Optional gitIdentity captures name/email/username for child Git operations only; omitted fields remain inherited and identity selection writes no config file. Optional cloneIdentity captures exact name/email strings and persists only supplied fields through clone --config before the first checkout on every supported platform. Empty strings are explicit; omitted or undefined fields preserve inherited/template configuration. Existing checkouts are never reconfigured. Unrepresentable process arguments fail honestly without transforming them. This option changes no global configuration or credentials and does not identify an authenticated account; command-local gitIdentity remains independent."
|
|
321
|
+
"normalization": "Returns directory, open, status, configuration, pull, push, write and identical close/drain/dispose hooks. Only missing or empty destinations clone; existing roots are preserved. Optional factory remoteBase explicitly selects the initial-clone remote as a local filesystem locator and captures path.resolve(remoteBase, remote) once at construction. A relative base, including an empty string, uses the construction-time working directory. Only the Git argument is derived; the caller's original locator remains unchanged. Omission preserves every Git remote name, URL, SCP-like address and remote-helper string without guessed parsing. Optional longPaths captures a boolean: explicit Windows true/false supplies clone-local core.longpaths before the first checkout, persisting only in the new repository under ordinary Git configuration precedence. Omission preserves configuration; existing repositories and non-Windows clone commands remain unchanged. No global setting or migration. configuration adds revision and unborn from successful porcelain-v2 branch state, preserving real commits including empty trees; it observes the existing root, symbolic HEAD, origin URL/pushURL arrays and branch remote/merge/upstream URL arrays without clone/init/fetch, preserving unset, empty and repeated values. Explicit paths remain selected. Same-directory operations serialize within the process; cancellation and shutdown join accepted process work. pull/push/write accept a captured target {remote,ref,remoteBase?} for current-branch fast-forward pull or non-force current-HEAD selected-ref push; omission retains ordinary configured behavior. An operation's own remoteBase declares local-path intent and captures its native absolute argument once at acceptance, before queueing; factory remote/remoteBase/branch remain independent clone inputs. Targeted pull/push results and writer results/failure outcomes retain the original remote, ref and supplied remoteBase, including relative or empty base text. Unrepresentable base text reports TypeError without transformation. write snapshots exact text and paths, stages/commits only literal owned paths and preserves unrelated staged files and Git filters. Complete outputs and causal failures retain confirmed local/committed/pushed and uncertain outcomes plus the supplied target. Optional gitIdentity captures name/email/username for child Git operations only; omitted fields remain inherited and identity selection writes no config file. Optional cloneIdentity captures exact name/email strings and persists only supplied fields through clone --config before the first checkout on every supported platform. Empty strings are explicit; omitted or undefined fields preserve inherited/template configuration. Existing checkouts are never reconfigured. Unrepresentable process arguments fail honestly without transforming them. This option changes no global configuration or credentials and does not identify an authenticated account; command-local gitIdentity remains independent. Optional initialBranch selects refs/heads/<initialBranch> only for a newly cloned checkout with successful complete empty remote advertisement, explicit local initial state and successful empty local-ref listing; branch remains clone --branch. Any template or hook-supplied local ref preserves HEAD. Existing destinations and committed or advertised clones remain unchanged. Explicit new-clone open adds complete ordered preparation outputs, including observeRefs when performed; later preparation failures retain completed outputs and their original cause. Omission preserves commands and result shape. First publication uses existing exact-file write and explicit target without an implicit pull."
|
|
322
322
|
},
|
|
323
323
|
{
|
|
324
324
|
"id": "core-repositories:readGitIdentity",
|
|
@@ -493,14 +493,14 @@
|
|
|
493
493
|
"name": "createGitTextSnapshot",
|
|
494
494
|
"displayName": "createGitTextSnapshot()",
|
|
495
495
|
"kind": "function",
|
|
496
|
-
"signature": "createGitTextSnapshot({cacheDirectory,remote,remoteBase,ref,selectPath,gitIdentity,onEvent,run=runProcess}={})",
|
|
496
|
+
"signature": "createGitTextSnapshot({cacheDirectory,remote,remoteBase,ref,selectPath,allowUnborn=false,gitIdentity,onEvent,run=runProcess}={})",
|
|
497
497
|
"entrypoints": ["arcane-os"],
|
|
498
498
|
"primaryImport": "arcane-os",
|
|
499
499
|
"group": "Workspace, doctor, repository, and server",
|
|
500
500
|
"summary": "Reads complete selected text from one fetched revision using a dedicated bare Git object cache and batched retrieval.",
|
|
501
501
|
"availability": "Node with Git; Android requires a host supplying Git/process capability",
|
|
502
502
|
"protocol": "Git tree and batched blob transport through the shared process owner",
|
|
503
|
-
"normalization": "Returns {refresh,close,drain,dispose}; refresh yields {revision,files:[{path,content}]}, shares concurrent work, reuses unchanged revisions, preserves exact UTF-8 text, isolates caller cancellation and drains accepted work. Optional remoteBase explicitly selects remote as a local filesystem locator and derives only its Git
|
|
503
|
+
"normalization": "Returns {refresh,close,drain,dispose}; refresh yields {revision,files:[{path,content}]}, shares concurrent work, reuses unchanged revisions, preserves exact UTF-8 text, isolates caller cancellation and drains accepted work. Optional allowUnborn defaults false, preserving that shape and command sequence. Explicit true first observes the complete remote advertisement; successful empty stdout yields revision:null, files:[], unborn:true. Any advertised HEAD, branch, tag or other output retains normal selected-ref fetch and actual errors; committed results add unborn:false, including a real empty tree. Every acquisition reobserves rather than returning stale empty state; cache role, identity, cancellation and drain remain unchanged. Optional remoteBase explicitly selects remote as a local filesystem locator and derives only its Git remote argument through path.resolve(remoteBase, remote), once at construction independently of the cache location and later working-directory changes. A relative base, including an empty string, uses the construction-time working directory; original locator/options remain unchanged. Omission preserves every Git remote name, URL, SCP-like address and remote-helper string without guessed parsing. Unrepresentable base text reports TypeError without transformation; existing Git failures remain errors. Optional gitIdentity captures name/email/username for child Git commands including bare init and fetch, without changing config files, payloads or shared acquisition lifetime. See git-text-snapshot.md."
|
|
504
504
|
},
|
|
505
505
|
{
|
|
506
506
|
"id": "diarization:createDiarization",
|
|
@@ -5409,7 +5409,7 @@ a dedicated bare Git cache. It does not create or alter a working checkout.
|
|
|
5409
5409
|
### Signature, lifecycle, and result
|
|
5410
5410
|
|
|
5411
5411
|
```text
|
|
5412
|
-
createGitTextSnapshot({cacheDirectory,remote,remoteBase,ref,selectPath,gitIdentity,onEvent,run=runProcess}={})
|
|
5412
|
+
createGitTextSnapshot({cacheDirectory,remote,remoteBase,ref,selectPath,allowUnborn=false,gitIdentity,onEvent,run=runProcess}={})
|
|
5413
5413
|
```
|
|
5414
5414
|
|
|
5415
5415
|
Import from `arcane-os`. The application owns the cache location, remote, ref,
|
|
@@ -5421,6 +5421,14 @@ await accepted refresh/process cleanup. Errors remain observable instead of
|
|
|
5421
5421
|
returning stale or partial success. See [Git text snapshots](git-text-snapshot.md)
|
|
5422
5422
|
for complete inputs, text semantics, errors, events and Core service composition.
|
|
5423
5423
|
|
|
5424
|
+
Optional `allowUnborn:true` observes the complete remote advertisement on every
|
|
5425
|
+
acquisition. Only successful empty output returns
|
|
5426
|
+
`{revision:null,files:[],unborn:true}`; committed results add `unborn:false` and
|
|
5427
|
+
retain the actual commit even for an empty tree. Any advertised HEAD, branch
|
|
5428
|
+
or tag continues through ordinary selected-ref fetch; missing refs and actual
|
|
5429
|
+
Git failures remain errors. Omitted/false preserves the prior result and commands.
|
|
5430
|
+
See [unborn remotes](git-text-snapshot.md#unborn-remotes).
|
|
5431
|
+
|
|
5424
5432
|
Optional `remoteBase` explicitly selects `remote` as a local filesystem locator.
|
|
5425
5433
|
Only its Git fetch argument is derived using `path.resolve(remoteBase, remote)`,
|
|
5426
5434
|
once at construction, independently of the cache location and later changes to
|
|
@@ -5476,6 +5484,28 @@ once and stdin remains open until cancellation or a callback/input failure
|
|
|
5476
5484
|
requests shutdown. On Microsoft NT, `npm` and `npx` are normalized to their Node
|
|
5477
5485
|
CLI entrypoints.
|
|
5478
5486
|
|
|
5487
|
+
For the literal `git` command on Windows, the default runner reads Git for
|
|
5488
|
+
Windows' registered `InstallPath` in the current-user and machine registry
|
|
5489
|
+
views. When that installation's `cmd/git.exe` exists, its `cmd` directory is
|
|
5490
|
+
appended only to this child's effective PATH. Existing entries and native
|
|
5491
|
+
command lookup retain priority; duplicate `Path`/`PATH` spellings follow Node's
|
|
5492
|
+
lexical selection. Other environment fields, explicit executable paths,
|
|
5493
|
+
custom process adapters, other commands and other platforms keep their existing
|
|
5494
|
+
behavior. No global environment or registry setting is changed, and no Git
|
|
5495
|
+
download, installation or command retry occurs.
|
|
5496
|
+
|
|
5497
|
+
One successful location is retained in memory for this process while its
|
|
5498
|
+
executable remains present. A removed executable triggers fresh discovery;
|
|
5499
|
+
absent, failed and cancelled lookups are not retained. Concurrent cold callers
|
|
5500
|
+
own independent discovery and cancellation. The standard Windows registry
|
|
5501
|
+
helper uses the existing process owner; its lifecycle callbacks have
|
|
5502
|
+
`process.git.discovery.*` event names, and its captured metadata is separate
|
|
5503
|
+
from the requested command's output. Missing registration leaves ordinary
|
|
5504
|
+
lookup unchanged. Discovery failure emits `process.git.discovery.unavailable`
|
|
5505
|
+
with the complete error before ordinary lookup continues. Cancellation or an
|
|
5506
|
+
event-callback failure stops the operation before Git starts. Normal Git
|
|
5507
|
+
errors and missing-working-directory errors remain actual command failures.
|
|
5508
|
+
|
|
5479
5509
|
An `AsyncIterable` input instead feeds stdin with awaited writes and closes it
|
|
5480
5510
|
when the producer finishes. Its `return()` must cooperate with cancellation of
|
|
5481
5511
|
an outstanding `next()`; the process owner drains both input and output work.
|
|
@@ -8400,7 +8430,7 @@ explicit existing path. The application owns the remote, branch and Core API.
|
|
|
8400
8430
|
### Signature and result
|
|
8401
8431
|
|
|
8402
8432
|
```text
|
|
8403
|
-
createRepositoryWorkspace({name,directory,dataRoot,remote,remoteBase,branch,longPaths,cloneIdentity,gitIdentity,onEvent,run=runProcess}={})
|
|
8433
|
+
createRepositoryWorkspace({name,directory,dataRoot,remote,remoteBase,branch,initialBranch,longPaths,cloneIdentity,gitIdentity,onEvent,run=runProcess}={})
|
|
8404
8434
|
```
|
|
8405
8435
|
|
|
8406
8436
|
Returns `{directory,open,status,configuration,pull,push,write,close,drain,dispose}`. Construction does
|
|
@@ -8411,6 +8441,17 @@ work and preserves the checkout. No CLI cwd, launch state, preference, snapshot
|
|
|
8411
8441
|
cache or existing user-data location changes. See the [complete inputs, results,
|
|
8412
8442
|
errors and lifecycle](core-repositories.md#one-connected-working-checkout).
|
|
8413
8443
|
|
|
8444
|
+
Optional `initialBranch` selects `refs/heads/<initialBranch>` only after a new
|
|
8445
|
+
clone, successful complete empty remote advertisement, explicit local
|
|
8446
|
+
initial-state observation and a successful empty local-ref listing. Any local
|
|
8447
|
+
ref supplied by a Git template or hook remains unchanged. Existing destinations and committed/advertised
|
|
8448
|
+
clones keep their branches; `branch` still means Git's existing-branch/tag clone
|
|
8449
|
+
selection. New-clone `open` results with this option add complete ordered
|
|
8450
|
+
preparation `outputs`; subsequent failures retain completed outputs and their
|
|
8451
|
+
original cause. Omission adds no command or result field. See
|
|
8452
|
+
[first-publication composition](core-repositories.md#first-publication-from-an-unborn-remote)
|
|
8453
|
+
for the direct first commit/push without an implicit pull.
|
|
8454
|
+
|
|
8414
8455
|
Optional factory `remoteBase` explicitly selects the initial-clone `remote` as
|
|
8415
8456
|
a local filesystem locator. Only its Git argument is derived through the host's
|
|
8416
8457
|
native `path.resolve(remoteBase, remote)`, once at construction. Relative bases,
|
|
@@ -8446,13 +8487,17 @@ omitted fields remain inherited. Username remains a credential hint, with
|
|
|
8446
8487
|
authentication owned by ordinary Git and its configured helper.
|
|
8447
8488
|
|
|
8448
8489
|
`configuration({signal})` observes an existing working root without clone,
|
|
8449
|
-
initialization or fetch. It returns `{repositoryRoot,headRef,origin,upstream}`:
|
|
8490
|
+
initialization or fetch. It returns `{repositoryRoot,headRef,revision,unborn,origin,upstream}`:
|
|
8450
8491
|
full symbolic HEAD (including unborn, null when detached), origin URL/pushURL
|
|
8451
8492
|
arrays and current branch remote/merge/URL/pushURL arrays. Unset, empty and
|
|
8452
|
-
repeated configuration values remain distinct.
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
reports `
|
|
8493
|
+
repeated configuration values remain distinct. `revision:null,unborn:true`
|
|
8494
|
+
comes only from Git's explicit porcelain-v2 initial state; committed and
|
|
8495
|
+
detached states retain their current revision with `unborn:false`. Missing
|
|
8496
|
+
branch-state output reports `ARCANE_REPOSITORY_HEAD_UNAVAILABLE`. These are
|
|
8497
|
+
ordinary configured values and local branch observations, not authentication
|
|
8498
|
+
or endpoint proof, and are not atomic with later operations. Raw stdout is
|
|
8499
|
+
decoded without replacement; unrepresentable text reports
|
|
8500
|
+
`ARCANE_GIT_CONFIGURATION_NOT_TEXT` with full diagnostics.
|
|
8456
8501
|
|
|
8457
8502
|
`pull({target,signal})`, `push({target,signal})` and
|
|
8458
8503
|
`write({files:[{path,content}],message,target,signal})` accept optional
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ import Is from 'strong-type';
|
|
|
5
5
|
import {ArcaneError, throwIfAborted} from '../errors.mjs';
|
|
6
6
|
import {runProcess} from '../process.mjs';
|
|
7
7
|
import {captureRepositoryRemote, captureRepositoryTarget, repositoryConfiguration, pullRepositoryTarget, pushRepositoryTarget,
|
|
8
|
-
repositoryPushArguments, repositoryStatus} from '../repository.mjs';
|
|
8
|
+
repositoryHeadArguments, repositoryHeadState, repositoryPushArguments, repositoryStatus} from '../repository.mjs';
|
|
9
9
|
import {createGitIdentityRunner} from '../git-identity.mjs';
|
|
10
10
|
|
|
11
11
|
export {readGitIdentity} from '../git-identity.mjs';
|
|
@@ -45,7 +45,7 @@ export function resolveArcaneDataPaths({dataRoot} = {}) {
|
|
|
45
45
|
|
|
46
46
|
/** One connected checkout, composed into an application's own Core service. */
|
|
47
47
|
export function createRepositoryWorkspace(
|
|
48
|
-
{name, directory, dataRoot, remote, remoteBase, branch, longPaths, cloneIdentity = {}, gitIdentity, onEvent, run = runProcess} = {}
|
|
48
|
+
{name, directory, dataRoot, remote, remoteBase, branch, initialBranch, longPaths, cloneIdentity = {}, gitIdentity, onEvent, run = runProcess} = {}
|
|
49
49
|
) {
|
|
50
50
|
let selected = directory;
|
|
51
51
|
if (selected === undefined) {
|
|
@@ -59,6 +59,12 @@ export function createRepositoryWorkspace(
|
|
|
59
59
|
if (remote !== undefined) requireString(remote, 'remote');
|
|
60
60
|
const remoteArgument = captureRepositoryRemote(remote, remoteBase);
|
|
61
61
|
if (branch !== undefined) requireString(branch, 'branch');
|
|
62
|
+
if (initialBranch !== undefined) {
|
|
63
|
+
requireString(initialBranch, 'initialBranch');
|
|
64
|
+
if (!initialBranch.isWellFormed() || initialBranch.includes('\0')) {
|
|
65
|
+
throw new TypeError('initialBranch must be text representable as a native process argument.');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
62
68
|
if (longPaths !== undefined && !is.boolean(longPaths)) {
|
|
63
69
|
throw new TypeError('longPaths must be a boolean when supplied.');
|
|
64
70
|
}
|
|
@@ -126,9 +132,60 @@ export function createRepositoryWorkspace(
|
|
|
126
132
|
arguments_,
|
|
127
133
|
{cwd: path.dirname(repositoryDirectory), signal, onEvent}
|
|
128
134
|
);
|
|
135
|
+
let outputs;
|
|
136
|
+
if (initialBranch !== undefined) {
|
|
137
|
+
const options = {cwd: repositoryDirectory, signal, onEvent};
|
|
138
|
+
outputs = [];
|
|
139
|
+
function completed(operation, observation) {
|
|
140
|
+
outputs.push({operation, stdout: observation.stdout, stderr: observation.stderr,
|
|
141
|
+
code: observation.code, signal: observation.signal ?? null});
|
|
142
|
+
}
|
|
143
|
+
completed('clone', result);
|
|
144
|
+
try {
|
|
145
|
+
throwIfAborted(signal);
|
|
146
|
+
const advertised = await prepareRun('git', ['ls-remote', '--symref', '--', remoteArgument],
|
|
147
|
+
{cwd: path.dirname(repositoryDirectory), signal, onEvent});
|
|
148
|
+
completed('observeRemote', advertised);
|
|
149
|
+
throwIfAborted(signal);
|
|
150
|
+
if (advertised.code !== 0) {
|
|
151
|
+
throw new ArcaneError('ARCANE_OPERATION_FAILED', 'Git remote observation failed.', {details: advertised});
|
|
152
|
+
}
|
|
153
|
+
if (advertised.stdout === '') {
|
|
154
|
+
const state = await prepareRun('git', [...repositoryHeadArguments], options);
|
|
155
|
+
completed('observeHead', state);
|
|
156
|
+
throwIfAborted(signal);
|
|
157
|
+
if (state.code !== 0) {
|
|
158
|
+
throw new ArcaneError('ARCANE_OPERATION_FAILED', 'Git branch state observation failed.', {details: state});
|
|
159
|
+
}
|
|
160
|
+
if (repositoryHeadState(state.stdout, state).unborn) {
|
|
161
|
+
const references = await prepareRun('git', ['for-each-ref', '--format=%(refname)'], options);
|
|
162
|
+
completed('observeRefs', references);
|
|
163
|
+
throwIfAborted(signal);
|
|
164
|
+
if (references.code !== 0) {
|
|
165
|
+
throw new ArcaneError('ARCANE_OPERATION_FAILED', 'Git local ref observation failed.', {details: references});
|
|
166
|
+
}
|
|
167
|
+
// Git templates or hooks can supply local refs even
|
|
168
|
+
// when the remote and this checkout's HEAD are unborn.
|
|
169
|
+
if (references.stdout === '') {
|
|
170
|
+
const selection = await prepareRun('git', ['symbolic-ref', 'HEAD', `refs/heads/${initialBranch}`], options);
|
|
171
|
+
completed('selectInitialBranch', selection);
|
|
172
|
+
throwIfAborted(signal);
|
|
173
|
+
if (selection.code !== 0) {
|
|
174
|
+
throw new ArcaneError('ARCANE_OPERATION_FAILED', 'Git initial branch selection failed.', {details: selection});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} catch (cause) {
|
|
180
|
+
throw new ArcaneError(cause?.code ?? 'ARCANE_REPOSITORY_PREPARE_FAILED',
|
|
181
|
+
cause instanceof Error ? cause.message : String(cause),
|
|
182
|
+
{cause, details: {directory: repositoryDirectory, cloned: true, outputs}, exitCode: cause?.exitCode});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
129
185
|
throwIfAborted(signal);
|
|
130
186
|
opened = true;
|
|
131
|
-
return {directory: repositoryDirectory, cloned: true, stdout: result.stdout, stderr: result.stderr
|
|
187
|
+
return {directory: repositoryDirectory, cloned: true, stdout: result.stdout, stderr: result.stderr,
|
|
188
|
+
...(outputs === undefined ? {} : {outputs})};
|
|
132
189
|
}
|
|
133
190
|
|
|
134
191
|
function accept(operation, signal) {
|
|
@@ -15,11 +15,12 @@ const snapshotRef = 'refs/arcane/text-snapshot';
|
|
|
15
15
|
* One application service owns each dedicated cache directory. Construction
|
|
16
16
|
* performs no I/O; refresh coalesces callers and close drains accepted work.
|
|
17
17
|
*/
|
|
18
|
-
export function createGitTextSnapshot({cacheDirectory, remote, remoteBase, ref, selectPath, gitIdentity, onEvent, run = runProcess} = {}) {
|
|
18
|
+
export function createGitTextSnapshot({cacheDirectory, remote, remoteBase, ref, selectPath, allowUnborn = false, gitIdentity, onEvent, run = runProcess} = {}) {
|
|
19
19
|
for (const [name, value] of Object.entries({cacheDirectory, remote, ref})) {
|
|
20
20
|
if (!is.string(value) || value === '') throw new TypeError(`${name} must be a nonempty string.`);
|
|
21
21
|
}
|
|
22
22
|
if (!is.function(selectPath)) throw new TypeError('selectPath must be a function.');
|
|
23
|
+
if (!is.boolean(allowUnborn)) throw new TypeError('allowUnborn must be a boolean.');
|
|
23
24
|
const remoteArgument = captureRepositoryRemote(remote, remoteBase);
|
|
24
25
|
const execute = createGitIdentityRunner(run, gitIdentity);
|
|
25
26
|
const directory = path.resolve(cacheDirectory);
|
|
@@ -70,11 +71,28 @@ export function createGitTextSnapshot({cacheDirectory, remote, remoteBase, ref,
|
|
|
70
71
|
try {
|
|
71
72
|
await events.send({type: 'git.snapshot.refreshing', message: 'Refreshing the selected repository revision.', data: {ref}});
|
|
72
73
|
await prepare(events);
|
|
74
|
+
if (allowUnborn) {
|
|
75
|
+
// No ref pattern or --exit-code: a successful complete empty
|
|
76
|
+
// advertisement is distinct from a missing selected ref.
|
|
77
|
+
const advertised = await git(['ls-remote', '--symref', '--', remoteArgument], {onEvent: events.send});
|
|
78
|
+
if (advertised.code !== 0) {
|
|
79
|
+
throw new ArcaneError(ERROR_CODES.operationFailed, 'Git remote observation failed.', {details: advertised});
|
|
80
|
+
}
|
|
81
|
+
if (advertised.stdout === '') {
|
|
82
|
+
const result = {revision: null, files: [], unborn: true};
|
|
83
|
+
await events.send({type: 'git.snapshot.completed', message: 'The remote advertises no revision.',
|
|
84
|
+
data: {revision: null, unborn: true, reused: retained?.unborn === true}});
|
|
85
|
+
await events.drain();
|
|
86
|
+
retained = result;
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
73
90
|
await git(['fetch', '--no-tags', '--no-write-fetch-head', '--', remoteArgument, `+${ref}:${snapshotRef}`], {onEvent: events.send});
|
|
74
91
|
const selected = await git(['rev-parse', '--verify', `${snapshotRef}^{commit}`], {onEvent: events.send});
|
|
75
92
|
const revision = selected.stdout.trim();
|
|
76
93
|
if (retained?.revision === revision) {
|
|
77
|
-
await events.send({type: 'git.snapshot.completed', message: 'The selected repository revision is unchanged.',
|
|
94
|
+
await events.send({type: 'git.snapshot.completed', message: 'The selected repository revision is unchanged.',
|
|
95
|
+
data: {revision, reused: true, ...(allowUnborn ? {unborn: false} : {})}});
|
|
78
96
|
await events.drain();
|
|
79
97
|
return retained;
|
|
80
98
|
}
|
|
@@ -125,8 +143,9 @@ export function createGitTextSnapshot({cacheDirectory, remote, remoteBase, ref,
|
|
|
125
143
|
}
|
|
126
144
|
});
|
|
127
145
|
}
|
|
128
|
-
const result = {revision, files: reader.finish()};
|
|
129
|
-
await events.send({type: 'git.snapshot.completed', message: 'Repository text snapshot is ready.',
|
|
146
|
+
const result = {revision, files: reader.finish(), ...(allowUnborn ? {unborn: false} : {})};
|
|
147
|
+
await events.send({type: 'git.snapshot.completed', message: 'Repository text snapshot is ready.',
|
|
148
|
+
data: {revision, reused: false, ...(allowUnborn ? {unborn: false} : {})}});
|
|
130
149
|
await events.drain();
|
|
131
150
|
retained = result;
|
|
132
151
|
return result;
|
|
@@ -156,7 +175,8 @@ export function createGitTextSnapshot({cacheDirectory, remote, remoteBase, ref,
|
|
|
156
175
|
accepted.then(settled, settled);
|
|
157
176
|
}
|
|
158
177
|
return waitForSnapshot(active, signal).then(function copyResult(snapshot) {
|
|
159
|
-
return {revision: snapshot.revision, files: snapshot.files.map(function copyFile(file) { return {...file}; })
|
|
178
|
+
return {revision: snapshot.revision, files: snapshot.files.map(function copyFile(file) { return {...file}; }),
|
|
179
|
+
...(allowUnborn ? {unborn: snapshot.unborn} : {})};
|
|
160
180
|
});
|
|
161
181
|
}
|
|
162
182
|
|
package/src/process.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import {spawn} from 'node:child_process';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import {ArcaneError,ERROR_CODES,throwIfAborted} from './errors.mjs';
|
|
5
5
|
import {createEventQueue} from './event-queue.mjs';
|
|
6
|
+
import {windowsGitEnvironment} from './windows-git-executable.mjs';
|
|
6
7
|
|
|
7
8
|
const is = new Is(false);
|
|
8
9
|
|
|
@@ -195,6 +196,29 @@ export async function runProcess(command,args=[],{
|
|
|
195
196
|
});
|
|
196
197
|
throwIfAborted(signal);
|
|
197
198
|
|
|
199
|
+
let childEnvironment=env?{...process.env,...env}:process.env;
|
|
200
|
+
if(process.platform==='win32'&&command==='git'){
|
|
201
|
+
try{
|
|
202
|
+
childEnvironment=await windowsGitEnvironment(childEnvironment,{
|
|
203
|
+
run:runProcess,signal,
|
|
204
|
+
onEvent:function discoveryEvent(event){
|
|
205
|
+
return events.send({
|
|
206
|
+
...event,type:event.type.replace(/^process\./u,'process.git.discovery.')
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}catch(error){
|
|
211
|
+
throwIfAborted(signal);
|
|
212
|
+
if(events.error)throw events.error;
|
|
213
|
+
await events.send({
|
|
214
|
+
type:'process.git.discovery.unavailable',
|
|
215
|
+
message:'Registered Git discovery was unavailable; using the existing command environment.',
|
|
216
|
+
data:{error}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
throwIfAborted(signal);
|
|
220
|
+
}
|
|
221
|
+
|
|
198
222
|
return new Promise((resolve,reject)=>{
|
|
199
223
|
let stdout=outputEncodings.stdout!==null&&outputSelected(captureOutput,'stdout')?'':null;
|
|
200
224
|
let stderr=outputEncodings.stderr!==null&&outputSelected(captureOutput,'stderr')?'':null;
|
|
@@ -369,7 +393,7 @@ export async function runProcess(command,args=[],{
|
|
|
369
393
|
try{
|
|
370
394
|
child=spawn(executable,executableArgs,{
|
|
371
395
|
cwd,
|
|
372
|
-
env:
|
|
396
|
+
env:childEnvironment,
|
|
373
397
|
shell:false,
|
|
374
398
|
detached:process.platform!=='win32',
|
|
375
399
|
windowsHide:true,
|
package/src/repository.mjs
CHANGED
|
@@ -85,6 +85,25 @@ export function repositoryPushArguments(selection) {
|
|
|
85
85
|
: ['push', '--no-follow-tags', '--', selection.remoteArgument, `HEAD:${selection.target.ref}`];
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// Both configuration and new-clone preparation consume Git's explicit initial
|
|
89
|
+
// state. NUL framing keeps unusual working filenames outside these headers.
|
|
90
|
+
export const repositoryHeadArguments = [
|
|
91
|
+
'--no-optional-locks', 'status', '--porcelain=v2', '-z', '--branch', '--untracked-files=no'
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
export function repositoryHeadState(output, details) {
|
|
95
|
+
const prefix = '# branch.oid ';
|
|
96
|
+
const record = output.split('\0').find(function branchCommit(value) {
|
|
97
|
+
return value.startsWith(prefix);
|
|
98
|
+
});
|
|
99
|
+
const revision = record?.slice(prefix.length);
|
|
100
|
+
if (!revision) {
|
|
101
|
+
throw new ArcaneError('ARCANE_REPOSITORY_HEAD_UNAVAILABLE',
|
|
102
|
+
'Git did not report its current commit or initial branch state.', {details});
|
|
103
|
+
}
|
|
104
|
+
return revision === '(initial)' ? {revision: null, unborn: true} : {revision, unborn: false};
|
|
105
|
+
}
|
|
106
|
+
|
|
88
107
|
/** Observe an existing workspace without initializing or refreshing it. */
|
|
89
108
|
export async function repositoryConfiguration({workspaceRoot = process.cwd(), signal, onEvent, run = runProcess} = {}) {
|
|
90
109
|
const repositoryRoot = path.resolve(workspaceRoot);
|
|
@@ -147,7 +166,12 @@ export async function repositoryConfiguration({workspaceRoot = process.cwd(), si
|
|
|
147
166
|
upstream.urls = remotes.get(`remote.${remoteName}.url`) ?? [];
|
|
148
167
|
upstream.pushUrls = remotes.get(`remote.${remoteName}.pushurl`) ?? [];
|
|
149
168
|
}
|
|
150
|
-
|
|
169
|
+
const state = await readGitText([...repositoryHeadArguments], options);
|
|
170
|
+
if (state.result.code !== 0) throw processFailure(state, 'Git branch state observation failed.');
|
|
171
|
+
let headState;
|
|
172
|
+
try { headState = repositoryHeadState(state.output, state.result); }
|
|
173
|
+
catch (error) { error.rawStdout = state.rawStdout; throw error; }
|
|
174
|
+
return {repositoryRoot, headRef, ...headState, origin, upstream};
|
|
151
175
|
}
|
|
152
176
|
|
|
153
177
|
export async function repositoryStatus({
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
2
|
+
import {stat} from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {throwIfAborted} from './errors.mjs';
|
|
5
|
+
|
|
6
|
+
const is=new Is(false);
|
|
7
|
+
let installedDirectory=null;
|
|
8
|
+
|
|
9
|
+
// Git for Windows records its chosen location for third-party integrations.
|
|
10
|
+
// Read both installation scopes/views in one owned Windows process. No guessed
|
|
11
|
+
// installation paths or changes to the registry/environment are needed.
|
|
12
|
+
const READ_INSTALLATIONS=String.raw`
|
|
13
|
+
$ErrorActionPreference = 'Stop'
|
|
14
|
+
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
|
15
|
+
$installations = [System.Collections.Generic.List[string]]::new()
|
|
16
|
+
$views = @([Microsoft.Win32.RegistryView]::Registry32)
|
|
17
|
+
if ([Environment]::Is64BitOperatingSystem) {
|
|
18
|
+
$views = @([Microsoft.Win32.RegistryView]::Registry64, [Microsoft.Win32.RegistryView]::Registry32)
|
|
19
|
+
}
|
|
20
|
+
foreach ($hive in @([Microsoft.Win32.RegistryHive]::CurrentUser, [Microsoft.Win32.RegistryHive]::LocalMachine)) {
|
|
21
|
+
foreach ($view in $views) {
|
|
22
|
+
$base = [Microsoft.Win32.RegistryKey]::OpenBaseKey($hive, $view)
|
|
23
|
+
try {
|
|
24
|
+
$key = $base.OpenSubKey('Software\GitForWindows')
|
|
25
|
+
if ($null -ne $key) {
|
|
26
|
+
try {
|
|
27
|
+
$location = $key.GetValue('InstallPath')
|
|
28
|
+
if ($location -is [string] -and $location.Length -gt 0 -and -not $installations.Contains($location)) {
|
|
29
|
+
$installations.Add($location)
|
|
30
|
+
}
|
|
31
|
+
} finally { $key.Dispose() }
|
|
32
|
+
}
|
|
33
|
+
} finally { $base.Dispose() }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
ConvertTo-Json -InputObject @($installations.ToArray()) -Compress
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
function environmentKey(environment,name){
|
|
40
|
+
return Object.keys(environment).sort().find(function matchingKey(key){
|
|
41
|
+
return key.toLowerCase()===name;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function appendWindowsGitDirectory(environment,directory){
|
|
46
|
+
const keys=Object.keys(environment).filter(function pathKey(key){
|
|
47
|
+
return key.toLowerCase()==='path';
|
|
48
|
+
}).sort();
|
|
49
|
+
const selectedKey=keys[0]??'Path';
|
|
50
|
+
// Node chooses the first case-insensitive key before omitting undefined.
|
|
51
|
+
// libuv uses the parent's PATH if the child has none; null becomes 'null'.
|
|
52
|
+
const selected=environment[selectedKey];
|
|
53
|
+
const original=selected===undefined
|
|
54
|
+
?process.env[environmentKey(process.env,'path')]??'':selected;
|
|
55
|
+
const result={...environment};
|
|
56
|
+
for(const key of keys)delete result[key];
|
|
57
|
+
const originalPath=`${original}`;
|
|
58
|
+
result[selectedKey]=`${originalPath}${originalPath?';':''}"${directory}"`;
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function installedGitExists(directory){
|
|
63
|
+
try{
|
|
64
|
+
return (await stat(path.win32.join(directory,'git.exe'))).isFile();
|
|
65
|
+
}catch(error){
|
|
66
|
+
if(error.code==='ENOENT'||error.code==='ENOTDIR')return false;
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function windowsGitEnvironment(environment,{run,signal,onEvent}={}){
|
|
72
|
+
throwIfAborted(signal);
|
|
73
|
+
const retained=installedDirectory;
|
|
74
|
+
if(retained){
|
|
75
|
+
const available=await installedGitExists(retained);
|
|
76
|
+
throwIfAborted(signal);
|
|
77
|
+
if(available)return appendWindowsGitDirectory(environment,retained);
|
|
78
|
+
if(installedDirectory===retained)installedDirectory=null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const systemRoot=process.env[environmentKey(process.env,'systemroot')];
|
|
82
|
+
if(!systemRoot)return environment;
|
|
83
|
+
const powershell=path.win32.join(systemRoot,'System32','WindowsPowerShell','v1.0','powershell.exe');
|
|
84
|
+
const result=await run(powershell,['-NoProfile','-NonInteractive','-Command',READ_INSTALLATIONS],{
|
|
85
|
+
signal,onEvent,emitOutputEvents:false
|
|
86
|
+
});
|
|
87
|
+
throwIfAborted(signal);
|
|
88
|
+
const locations=JSON.parse(result.stdout);
|
|
89
|
+
if(!is.array(locations))throw new TypeError('Git installation discovery did not return a location list.');
|
|
90
|
+
for(const location of locations){
|
|
91
|
+
if(!is.string(location)||!path.win32.isAbsolute(location))continue;
|
|
92
|
+
const directory=path.win32.join(location,'cmd');
|
|
93
|
+
const available=await installedGitExists(directory);
|
|
94
|
+
throwIfAborted(signal);
|
|
95
|
+
if(available){
|
|
96
|
+
// Only successful, still-present discovery is retained. Concurrent
|
|
97
|
+
// first callers own separate lookups and cancellation independently.
|
|
98
|
+
installedDirectory=directory;
|
|
99
|
+
return appendWindowsGitDirectory(environment,directory);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return environment;
|
|
103
|
+
}
|