1
>
/*---------------------------------------------------------------------------------------------
agentHostGitService.ts
2
>
* Copyright (c) Microsoft Corporation. All rights reserved.
3
>
* Licensed under the MIT License. See License.txt in the project root for license information.
4
>
*--------------------------------------------------------------------------------------------*/
5
>
6
>
import { VSBuffer } from '../../../base/common/buffer.js';
7
>
import { URI } from '../../../base/common/uri.js';
8
>
import { createDecorator } from '../../instantiation/common/instantiation.js';
9
>
import { ISessionFileDiff, ISessionGitState } from './state/sessionState.js';
10
>
11
>
/**
12
>
* Provider-agnostic session-database metadata key under which agents
13
>
* persist the branch they want git-driven diffs anchored to. Read by
14
>
* {@link IAgentHostChangesetService} when computing per-session file diffs; absent
15
>
* value means the diff falls back to anchoring at HEAD.
16
>
*/
17
>
export const META_DIFF_BASE_BRANCH = 'agentHost.diffBaseBranch';
18
>
19
>
/**
20
>
* Resolves the Branch Changes base-branch **name** from its two sources, in
21
>
* precedence order: the agent-persisted {@link META_DIFF_BASE_BRANCH} metadata
22
>
* value, then the session git state's detected base branch. Returns `undefined`
23
>
* when neither is available (callers then anchor the diff at `HEAD`).
24
>
*
25
>
* Shared by {@link IAgentHostChangesetService} and the review service so both
26
>
* pick the same base branch.
27
>
*/
28
>
export function resolveDiffBaseBranchName(persistedBaseBranch: string | undefined, sessionGitStateBaseBranch: string | undefined): string | undefined {
29
return persistedBaseBranch ?? sessionGitStateBaseBranch;
30
}
32
>
/**
33
>
* The well-known SHA-1 of git's empty tree, used as a fallback when a
34
>
* repository has no commits (no `HEAD` to read into the temp index).
35
>
*/
36
>
export const EMPTY_TREE_OBJECT = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
37
>
38
>
/** Options for {@link IAgentHostGitService.computeSessionFileDiffs}. */
39
>
export interface IComputeSessionFileDiffsOptions {
40
>
/**
41
>
* The session URI, used as the authority of the produced
42
>
* `git-blob:` URIs so the resolver can find the session's working
43
>
* directory.
44
>
*/
45
>
readonly sessionUri: string;
46
>
/**
47
>
* The branch to diff against. Typically the worktree's start-point
48
>
* branch (for worktree sessions) or the repository's default branch.
49
>
* When undefined or unresolvable, the diff is taken against `HEAD`,
50
>
* which surfaces uncommitted work but no committed-on-branch work.
51
>
*/
52
>
readonly baseBranch?: string;
53
>
}
54
>
55
>
/** Cheap repository facts used to decide whether a branch diff is safe to compute. */
56
>
export interface IBranchDiffSafetyInfo {
57
>
readonly hasVirtualFileSystem: boolean;
58
>
readonly baselineCommitTimestamp: number | undefined;
59
>
readonly commitCount: number | undefined;
60
>
readonly workspaceFileCount: number;
61
>
}
62
>
63
>
/** A bounded unified-diff result. */
64
>
export interface IDiffPatchResult {
65
>
readonly patch: string | undefined;
66
>
readonly tooLarge: boolean;
67
>
}
68
>
69
>
/** Options for {@link IAgentHostGitService.push}. */
70
>
export interface IPushOptions {
71
>
/** The branch or refspec to push. Defaults to the current branch. */
72
>
readonly ref?: string;
73
>
/** The remote to push to. Defaults to `origin`. */
74
>
readonly remote?: string;
75
>
/**
76
>
* When true, the push uses `-u` so the pushed branch tracks the remote
77
>
* branch for subsequent fetch/push commands.
78
>
*/
79
>
readonly setUpstream?: boolean;
80
>
}
81
>
82
>
/** Options for {@link IAgentHostGitService.pull}. */
83
>
export interface IPullOptions {
84
>
/** The branch or ref to pull. Defaults to the configured upstream. */
85
>
readonly ref?: string;
86
>
/** The remote to pull from. Defaults to `origin`. */
87
>
readonly remote?: string;
88
>
/** When true, local commits are rebased onto the fetched ref (`-r`) instead of merged. */
89
>
readonly rebase?: boolean;
90
>
}
91
>
92
>
export const IAgentHostGitService = createDecorator<IAgentHostGitService>('agentHostGitService');
93
>
94
>
export interface IRefQuery {
95
>
readonly count?: number;
96
>
readonly pattern?: string | string[];
97
>
readonly sort?: 'alphabetically' | 'committerdate' | 'creatordate';
98
>
}
99
>
100
>
export type Branch = IBranch | IRemoteBranch;
101
>
export type GitRef = IBranch | IRemoteBranch | ITag;
102
>
103
>
export const enum GitRefType {
104
>
Head,
105
>
RemoteHead,
106
>
DetachedHead,
107
>
Tag
108
>
}
109
>
110
>
export interface IBranch {
111
>
readonly ref: string;
112
>
readonly name: string;
113
>
readonly upstream?: {
114
>
readonly ref: string;
115
>
readonly name: string;
116
>
readonly remote: string;
117
>
};
118
>
readonly kind: GitRefType.Head;
119
>
}
120
>
121
>
export interface IRemoteBranch {
122
>
readonly ref: string;
123
>
readonly name: string;
124
>
readonly remote: string;
125
>
readonly kind: GitRefType.RemoteHead;
126
>
}
127
>
128
>
export interface ITag {
129
>
readonly ref: string;
130
>
readonly name: string;
131
>
readonly kind: GitRefType.Tag;
132
>
}
133
>
134
>
export interface IDetachedHead {
135
>
readonly name: string;
136
>
readonly kind: GitRefType.DetachedHead;
137
>
}
138
>
139
>
export interface IDefaultBranch {
140
>
readonly name: string;
141
>
readonly startPoint: string;
142
>
}
143
>
144
>
export interface IAgentHostGitService {
145
>
readonly _serviceBrand: undefined;
146
>
getCurrentBranch(workingDirectory: URI): Promise<string | undefined>;
147
>
getDefaultBranch(workingDirectory: URI): Promise<IDefaultBranch | undefined>;
148
>
getRefs(workingDirectory: URI, query?: IRefQuery): Promise<GitRef[]>;
149
>
getBranches(workingDirectory: URI, query?: IRefQuery): Promise<Branch[]>;
150
>
getBranch(workingDirectory: URI, name: string): Promise<Branch | undefined>;
151
>
getRepositoryRoot(workingDirectory: URI): Promise<URI | undefined>;
152
>
getWorktreeRoots(workingDirectory: URI): Promise<URI[]>;
153
>
addWorktree(repositoryRoot: URI, worktree: URI, branchName: string, startPoint: string): Promise<void>;
154
>
copyWorktreeIncludeFiles(repositoryRoot: URI, worktree: URI, globs: readonly string[]): Promise<void>;
155
>
/**
156
>
* Adds a worktree for an existing branch (no `-b`). Used when restoring
157
>
* a worktree whose branch was preserved (e.g. unarchiving a session
158
>
* whose worktree was previously cleaned up on archive).
159
>
*/
160
>
addExistingWorktree(repositoryRoot: URI, worktree: URI, branchName: string): Promise<void>;
161
>
removeWorktree(repositoryRoot: URI, worktree: URI): Promise<void>;
162
>
/**
163
>
* Returns true when the named branch exists in the repository
164
>
* (`refs/heads/<branchName>` resolves). Used by archive cleanup to
165
>
* confirm the branch is preserved before deleting the worktree, and by
166
>
* the unarchive path to confirm the branch is still around before
167
>
* recreating the worktree.
168
>
*/
169
>
branchExists(repositoryRoot: URI, branchName: string): Promise<boolean>;
170
>
/**
171
>
* Returns true when the working tree has any tracked, staged, or
172
>
* untracked changes. Used by archive cleanup to skip removing a
173
>
* worktree that still contains uncommitted work.
174
>
*/
175
>
hasUncommittedChanges(workingDirectory: URI): Promise<boolean>;
176
>
177
>
/**
178
>
* Stages and commits all tracked, staged, and untracked changes in the
179
>
* working tree. Mirrors the Copilot CLI session PR path, which commits
180
>
* uncommitted work before creating a pull request.
181
>
*/
182
>
commitAll(workingDirectory: URI, message: string): Promise<void>;
183
>
184
>
/**
185
>
* Restores files in the working tree via `git restore`. When
186
>
* {@link options.staged} is true, restores the index instead of the
187
>
* working tree. When {@link options.ref} is provided, the contents are
188
>
* taken from that ref (`--source`). An empty {@link paths} array
189
>
* restores everything (`.`).
190
>
*/
191
>
restore(workingDirectory: URI, paths: readonly string[], options?: { readonly staged?: boolean; readonly ref?: string }): Promise<void>;
192
>
193
>
/**
194
>
* Returns true when the named branch has an upstream tracking ref
195
>
* (i.e. `<branch>@{upstream}` resolves). Used before {@link push}
196
>
* to decide whether `--set-upstream` is needed.
197
>
*/
198
>
hasUpstream(workingDirectory: URI, branchName: string): Promise<boolean>;
199
>
200
>
/**
201
>
* Fetches the latest changes from the remote (`origin` unless
202
>
* {@link IPullOptions.remote} overrides it) and integrates them into the
203
>
* current branch. When {@link IPullOptions.rebase} is true, local commits
204
>
* are rebased onto the fetched ref instead of merged. When
205
>
* {@link IPullOptions.ref} is provided, that ref is pulled instead of the
206
>
* branch's configured upstream.
207
>
*/
208
>
pull(workingDirectory: URI, options?: IPullOptions): Promise<void>;
209
>
210
>
/**
211
>
* Pushes the current branch (or {@link IPushOptions.ref}) to the remote
212
>
* (`origin` unless {@link IPushOptions.remote} overrides it). When
213
>
* {@link IPushOptions.setUpstream} is true, the push uses `-u` so
214
>
* subsequent fetch/push commands track the remote branch.
215
>
*/
216
>
push(workingDirectory: URI, options?: IPushOptions): Promise<void>;
217
>
218
>
/**
219
>
* Computes the {@link ISessionGitState} for the working directory by
220
>
* shelling out to `git`. Returns undefined if the directory is not a
221
>
* git work tree. Called on session open and after each turn completes
222
>
* so the UI always reflects current branch/remote/change state.
223
>
*/
224
>
getSessionGitState(workingDirectory: URI): Promise<ISessionGitState | undefined>;
225
>
/** Returns fetch remote URLs with the preferred remote, then `origin`, first. */
226
>
getFetchRemoteUrls(workingDirectory: URI, preferredRemote?: string): Promise<readonly string[] | undefined>;
227
>
/** Returns repo-relative untracked file paths. */
228
>
getUntrackedPaths(workingDirectory: URI): Promise<readonly string[] | undefined>;
229
>
230
>
/**
231
>
* Computes per-file diffs for the session by shelling out to `git
232
>
* diff --raw --numstat --diff-filter=ADMR -z` against the merge base of
233
>
* the current branch and {@link IComputeSessionFileDiffsOptions.baseBranch}
234
>
* (or `HEAD` if no base branch is available). When the working tree has
235
>
* untracked files, the diff is computed via a temp index so the
236
>
* untracked content is included.
237
>
*
238
>
* Returns `undefined` when {@link workingDirectory} is not a git work
239
>
* tree, so callers can fall back to other diff sources.
240
>
*
241
>
* Each returned {@link ISessionFileDiff} has its `before.content` set to
242
>
* a `git-blob:` URI ({@link buildGitBlobUri}); `after.content` is a
243
>
* `file:` URI on the working-tree path. Adds and deletes drop the
244
>
* missing side.
245
>
*/
246
>
computeSessionFileDiffs(workingDirectory: URI, options: IComputeSessionFileDiffsOptions): Promise<readonly ISessionFileDiff[] | undefined>;
247
>
248
>
/**
249
>
* Resolves the commit-ish the **Branch Changes** baseline is measured from:
250
>
* the merge-base of `HEAD` and `baseBranch` (preferring the
251
>
* `origin/<baseBranch>` remote-tracking ref when it exists), falling back to
252
>
* `HEAD`, then to the empty-tree object for a repo with no commits. Returns
253
>
* `undefined` only when {@link workingDirectory} is not a git work tree.
254
>
*
255
>
* Shared by {@link computeSessionFileDiffs} (which anchors the Branch Changes
256
>
* diff here) and the review service, so both agree on the exact baseline.
257
>
*/
258
>
resolveBranchBaselineCommit(workingDirectory: URI, baseBranch?: string): Promise<string | undefined>;
259
>
260
>
/**
261
>
* Reads a single git blob via `git show <ref>:<repoRelativePath>` from
262
>
* the given working directory. Returns `undefined` when the blob does
263
>
* not exist or the directory is not a git work tree.
264
>
*/
265
>
showBlob(workingDirectory: URI, ref: string, repoRelativePath: string): Promise<VSBuffer | undefined>;
266
>
267
>
// ---- Checkpoint plumbing (used by IAgentHostCheckpointService) -------
268
>
269
>
/**
270
>
* Captures the current working tree (including untracked files) as a
271
>
* tree object, returning the tree OID. Uses a throwaway `GIT_INDEX_FILE`
272
>
* so the user's real index is untouched. Returns `undefined` when the
273
>
* directory is not a git work tree.
274
>
*/
275
>
captureWorkingTreeAsTree(workingDirectory: URI): Promise<string | undefined>;
276
>
277
>
/**
278
>
* Creates a commit object from a tree (optionally chained to a parent)
279
>
* and returns its OID. Does NOT update any ref.
280
>
*/
281
>
commitTree(repositoryRoot: URI, treeOid: string, parentOid: string | undefined, message: string): Promise<string | undefined>;
282
>
283
>
/**
284
>
* Updates a ref to point at `newOid`. Creates the ref if missing.
285
>
*/
286
>
updateRef(repositoryRoot: URI, ref: string, newOid: string): Promise<void>;
287
>
288
>
/**
289
>
* Batch-deletes the given refs via `git update-ref --stdin -z`.
290
>
* Missing refs are tolerated.
291
>
*/
292
>
deleteRefs(repositoryRoot: URI, refs: readonly string[]): Promise<void>;
293
>
294
>
/**
295
>
* Resolves a ref/object expression to its OID, e.g. `revParse(repo, 'refs/agents/abc/...')`
296
>
* or `revParse(repo, '<commit>^{tree}')`. Returns `undefined` when the
297
>
* ref does not exist.
298
>
*/
299
>
revParse(repositoryRoot: URI, expression: string): Promise<string | undefined>;
300
>
301
>
/**
302
>
* Builds a new tree from `baseTreeOid` in which the single repo-relative
303
>
* `path` is replaced by its content (blob + mode) from `sourceTreeOid`, or
304
>
* removed when the path is absent in `sourceTreeOid`. All other paths are
305
>
* copied verbatim from `baseTreeOid`. Uses a throwaway `GIT_INDEX_FILE` so
306
>
* the user's real index is untouched. Returns the new tree OID, or
307
>
* `undefined` on git failure.
308
>
*
309
>
* File-level building block for review (see `IAgentHostReviewService`): to
310
>
* mark a file reviewed, overlay it from the working-tree snapshot tree; to
311
>
* unmark, overlay it from the baseline tree.
312
>
*/
313
>
overlayPathIntoTree(repositoryRoot: URI, baseTreeOid: string, path: string, sourceTreeOid: string): Promise<string | undefined>;
314
>
315
>
/**
316
>
* Returns the repo-relative paths that differ between two tree-ish (commit
317
>
* or tree) objects via `git diff --name-only --no-renames -z`. Rename
318
>
* detection is off so a rename shows as delete(old) + add(new). Returns
319
>
* `undefined` on git failure (e.g. not a git work tree).
320
>
*/
321
>
diffTreePaths(repositoryRoot: URI, fromTreeish: string, toTreeish: string): Promise<string[] | undefined>;
322
>
323
>
/**
324
>
* Computes per-file diffs between two refs (typically two consecutive
325
>
* checkpoint refs) by shelling out to
326
>
* `git diff --raw --numstat --diff-filter=ADMR -z <fromRef> <toRef>`.
327
>
* Returns the same {@link ISessionFileDiff} shape as
328
>
* {@link computeSessionFileDiffs}: `before.content` is a `git-blob:`
329
>
* URI anchored on `fromRef`, `after.content` is a `git-blob:` URI
330
>
* anchored on `toRef`. Returns `undefined` on git failure.
331
>
*
332
>
* Used by the changeset service to materialise per-turn diffs from
333
>
* checkpoint refs when they are available — that path captures
334
>
* terminal-tool edits the FileEditTracker pipeline misses.
335
>
*/
336
>
computeFileDiffsBetweenRefs(workingDirectory: URI, options: { readonly sessionUri: string; readonly fromRef: string; readonly toRef: string }): Promise<readonly ISessionFileDiff[] | undefined>;
337
>
/** Reads bounded facts needed before computing an expensive branch diff. */
338
>
getBranchDiffSafetyInfo(workingDirectory: URI, baselineCommit: string): Promise<IBranchDiffSafetyInfo | undefined>;
339
>
/** Computes a unified patch for paths between immutable tree-ish values. */
340
>
getDiffPatchBetweenRefs(workingDirectory: URI, options: { readonly fromRef: string; readonly toRef: string; readonly paths: readonly string[]; readonly maxBuffer: number }): Promise<IDiffPatchResult | undefined>;
341
>
}
342
>
343
function getCommonBranchPriority(branch: string): number {
344
if (branch === 'main') {