Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/test/node/agentHostGitService.test.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
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 assert from 'assert';
7
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
8
import { EMPTY_TREE_OBJECT, getBranchCompletions, parseDefaultBranchRef, parseGitDiffRawNumstat, parseGitStatusV2, parseHasGitHubRemote, parseUntrackedPaths } from '../../node/agentHostGitService.js';
9
import { buildGitBlobUri } from '../../node/gitDiffContent.js';
10
import { URI } from '../../../../base/common/uri.js';
11
12
suite('AgentHostGitService', () => {
13
ensureNoDisposablesAreLeakedInTestSuite();
14
15
test('sorts common branch names to the top before applying limit', () => {
16
assert.deepStrictEqual(
17
getBranchCompletions(['feature/recent', 'release', 'master', 'main', 'feature/older'], { limit: 3 }),
18
['main', 'master', 'feature/recent'],
19
);
20
});
21
22
test('preserves git order for non-common branches', () => {
23
assert.deepStrictEqual(
24
getBranchCompletions(['feature/recent', 'release', 'feature/older']),
25
['feature/recent', 'release', 'feature/older'],
26
);
27
});
28
29
test('filters before sorting common branch names', () => {
30
assert.deepStrictEqual(
31
getBranchCompletions(['feature/recent', 'master', 'main', 'maintenance'], { query: 'ma' }),
32
['main', 'master', 'maintenance'],
33
);
34
});
35
36
suite('parseGitStatusV2', () => {
37
test('parses a clean checkout with upstream', () => {
38
const out = [
39
'# branch.oid 0123456789abcdef0123456789abcdef01234567',
40
'# branch.head main',
41
'# branch.upstream origin/main',
42
'# branch.ab +0 -0',
43
].join('\n');
44
assert.deepStrictEqual(parseGitStatusV2(out), {
45
branchName: 'main',
46
upstreamBranchName: 'origin/main',
47
outgoingChanges: 0,
48
incomingChanges: 0,
49
uncommittedChanges: 0,
50
});
51
});
52
53
test('parses a dirty branch ahead and behind upstream', () => {
54
const out = [
55
'# branch.oid 0123456789abcdef0123456789abcdef01234567',
56
'# branch.head feature',
57
'# branch.upstream origin/feature',
58
'# branch.ab +3 -2',
59
'1 .M N... 100644 100644 100644 abc abc src/a.ts',
60
'2 R. N... 100644 100644 100644 abc abc R100 src/b.ts\tsrc/old-b.ts',
61
'? src/untracked.ts',
62
].join('\n');
63
assert.deepStrictEqual(parseGitStatusV2(out), {
64
branchName: 'feature',
65
upstreamBranchName: 'origin/feature',
66
outgoingChanges: 3,
67
incomingChanges: 2,
68
uncommittedChanges: 3,
69
});
70
});
71
72
test('treats (detached) HEAD as no branch and omits upstream/ab when absent', () => {
73
const out = [
74
'# branch.oid 0123456789abcdef0123456789abcdef01234567',
75
'# branch.head (detached)',
76
].join('\n');
77
assert.deepStrictEqual(parseGitStatusV2(out), {
78
branchName: undefined,
79
upstreamBranchName: undefined,
80
outgoingChanges: undefined,
81
incomingChanges: undefined,
82
uncommittedChanges: 0,
83
});
84
});
85
86
test('returns empty object for undefined input', () => {
87
assert.deepStrictEqual(parseGitStatusV2(undefined), {});
88
});
89
});
90
91
suite('parseHasGitHubRemote', () => {
92
test('detects ssh github remote', () => {
93
assert.strictEqual(parseHasGitHubRemote('origin\[email protected]:owner/repo.git (fetch)\n'), true);
94
});
95
test('detects https github remote', () => {
96
assert.strictEqual(parseHasGitHubRemote('origin\thttps://github.com/owner/repo.git (fetch)\n'), true);
97
});
98
test('returns false for non-github remotes', () => {
99
assert.strictEqual(parseHasGitHubRemote('origin\thttps://gitlab.com/owner/repo.git (fetch)\n'), false);
100
});
101
test('returns false when there are no remotes', () => {
102
assert.strictEqual(parseHasGitHubRemote(''), false);
103
});
104
test('returns undefined when probe failed (output absent)', () => {
105
assert.strictEqual(parseHasGitHubRemote(undefined), undefined);
106
});
107
});
108
109
suite('parseDefaultBranchRef', () => {
110
test('strips refs/remotes/origin/ prefix', () => {
111
assert.strictEqual(parseDefaultBranchRef('refs/remotes/origin/main\n'), 'main');
112
});
113
test('returns the ref as-is when prefix is not present', () => {
114
assert.strictEqual(parseDefaultBranchRef('main'), 'main');
115
});
116
test('returns undefined for empty/missing output', () => {
117
assert.strictEqual(parseDefaultBranchRef(undefined), undefined);
118
assert.strictEqual(parseDefaultBranchRef(' '), undefined);
119
});
120
});
121
122
suite('parseUntrackedPaths', () => {
123
test('returns empty for empty/undefined output', () => {
124
assert.deepStrictEqual(parseUntrackedPaths(undefined), []);
125
assert.deepStrictEqual(parseUntrackedPaths(''), []);
126
});
127
128
test('extracts untracked entries and skips others', () => {
129
// `git status --porcelain=v1 -z` emits NUL-separated entries; the
130
// rename entry includes a second NUL-separated "from" path that
131
// must be skipped.
132
const out = '?? new.txt\x00 M edited.txt\x00R to.txt\x00from.txt\x00?? other.txt\x00';
133
assert.deepStrictEqual(parseUntrackedPaths(out), ['new.txt', 'other.txt']);
134
});
135
});
136
137
suite('parseGitDiffRawNumstat', () => {
138
const root = URI.file('/repo');
139
const sessionUri = 'copilot:/abc';
140
const sha = 'cafe1234cafe1234cafe1234cafe1234cafe1234';
141
142
test('parses an add, modify, delete and rename in a single stream', () => {
143
// Format: alternating `--raw` and `--numstat` segments separated by
144
// NUL bytes. Renames have an extra path segment in both halves.
145
const segments: string[] = [
146
':100644 100644 0000000 1111111 M', 'modified.ts',
147
':000000 100644 0000000 2222222 A', 'added.ts',
148
':100644 000000 3333333 0000000 D', 'deleted.ts',
149
':100644 100644 4444444 5555555 R100', 'old/path.ts', 'new/path.ts',
150
'5\t2\tmodified.ts',
151
'10\t0\tadded.ts',
152
'0\t7\tdeleted.ts',
153
'3\t3\t', 'old/path.ts', 'new/path.ts',
154
'',
155
];
156
const out = segments.join('\x00');
157
const diffs = parseGitDiffRawNumstat(out, root, sessionUri, sha);
158
assert.deepStrictEqual(diffs, [
159
{
160
before: { uri: 'file:///repo/modified.ts', content: { uri: buildGitBlobUri(sessionUri, sha, 'modified.ts') } },
161
after: { uri: 'file:///repo/modified.ts', content: { uri: 'file:///repo/modified.ts' } },
162
diff: { added: 5, removed: 2 },
163
},
164
{
165
after: { uri: 'file:///repo/added.ts', content: { uri: 'file:///repo/added.ts' } },
166
diff: { added: 10, removed: 0 },
167
},
168
{
169
before: { uri: 'file:///repo/deleted.ts', content: { uri: buildGitBlobUri(sessionUri, sha, 'deleted.ts') } },
170
diff: { added: 0, removed: 7 },
171
},
172
{
173
before: { uri: 'file:///repo/old/path.ts', content: { uri: buildGitBlobUri(sessionUri, sha, 'old/path.ts') } },
174
after: { uri: 'file:///repo/new/path.ts', content: { uri: 'file:///repo/new/path.ts' } },
175
diff: { added: 3, removed: 3 },
176
},
177
]);
178
});
179
180
test('treats `-` numstat values (binary) as zero', () => {
181
const out = [':100644 100644 0 0 M', 'image.png', '-\t-\timage.png', ''].join('\x00');
182
const diffs = parseGitDiffRawNumstat(out, root, sessionUri, sha);
183
assert.strictEqual(diffs.length, 1);
184
assert.deepStrictEqual(diffs[0].diff, { added: 0, removed: 0 });
185
});
186
187
test('returns empty for empty input', () => {
188
assert.deepStrictEqual(parseGitDiffRawNumstat('', root, sessionUri, sha), []);
189
});
190
});
191
192
test('exports the well-known empty-tree object SHA', () => {
193
assert.strictEqual(EMPTY_TREE_OBJECT, '4b825dc642cb6eb9a060e54bf8d69288fbee4904');
194
});
195
});
196
197
198