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