diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index 79eaf1ca4ca43..0a44f6810d29b 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -545,6 +545,7 @@ function getStatus(status: Status): string { case Status.INTENT_TO_ADD: return 'INTENT_TO_ADD'; case Status.INTENT_TO_RENAME: return 'INTENT_TO_RENAME'; case Status.TYPE_CHANGED: return 'TYPE_CHANGED'; + case Status.INDEX_TYPE_CHANGED: return 'INDEX_TYPE_CHANGED'; case Status.ADDED_BY_US: return 'ADDED_BY_US'; case Status.ADDED_BY_THEM: return 'ADDED_BY_THEM'; case Status.DELETED_BY_US: return 'DELETED_BY_US'; diff --git a/extensions/git/src/api/git.constants.ts b/extensions/git/src/api/git.constants.ts index 5847e21d5d0da..209718e81bd4a 100644 --- a/extensions/git/src/api/git.constants.ts +++ b/extensions/git/src/api/git.constants.ts @@ -44,6 +44,8 @@ export const Status = Object.freeze({ BOTH_ADDED: 16, BOTH_DELETED: 17, BOTH_MODIFIED: 18, + + INDEX_TYPE_CHANGED: 19, }) satisfies typeof git.Status; export const GitErrorCodes = Object.freeze({ diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 03a06a7794bb7..7b5f905cccc9a 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -105,7 +105,9 @@ export const enum Status { DELETED_BY_THEM, BOTH_ADDED, BOTH_DELETED, - BOTH_MODIFIED + BOTH_MODIFIED, + + INDEX_TYPE_CHANGED } export interface Change { diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index ec27cbd57fee4..3c23b6b38d5c6 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -70,6 +70,7 @@ export class Resource implements SourceControlResourceState { case Status.INTENT_TO_RENAME: return 'R'; case Status.TYPE_CHANGED: + case Status.INDEX_TYPE_CHANGED: return 'T'; case Status.UNTRACKED: return 'U'; @@ -104,6 +105,7 @@ export class Resource implements SourceControlResourceState { case Status.INTENT_TO_ADD: return l10n.t('Intent to Add'); case Status.INTENT_TO_RENAME: return l10n.t('Intent to Rename'); case Status.TYPE_CHANGED: return l10n.t('Type Changed'); + case Status.INDEX_TYPE_CHANGED: return l10n.t('Index Type Changed'); case Status.BOTH_DELETED: return l10n.t('Conflict: Both Deleted'); case Status.ADDED_BY_US: return l10n.t('Conflict: Added By Us'); case Status.DELETED_BY_THEM: return l10n.t('Conflict: Deleted By Them'); @@ -118,6 +120,7 @@ export class Resource implements SourceControlResourceState { static getStatusColor(type: Status): ThemeColor { switch (type) { case Status.INDEX_MODIFIED: + case Status.INDEX_TYPE_CHANGED: return new ThemeColor('gitDecoration.stageModifiedResourceForeground'); case Status.MODIFIED: case Status.TYPE_CHANGED: @@ -230,6 +233,7 @@ export class Resource implements SourceControlResourceState { case Status.INTENT_TO_ADD: return Resource.Icons[theme].Added; case Status.INTENT_TO_RENAME: return Resource.Icons[theme].Renamed; case Status.TYPE_CHANGED: return Resource.Icons[theme].TypeChanged; + case Status.INDEX_TYPE_CHANGED: return Resource.Icons[theme].TypeChanged; case Status.BOTH_DELETED: return Resource.Icons[theme].Conflict; case Status.ADDED_BY_US: return Resource.Icons[theme].Conflict; case Status.DELETED_BY_THEM: return Resource.Icons[theme].Conflict; @@ -289,6 +293,7 @@ export class Resource implements SourceControlResourceState { case Status.MODIFIED: case Status.INDEX_COPIED: case Status.TYPE_CHANGED: + case Status.INDEX_TYPE_CHANGED: return 2; case Status.IGNORED: return 3; @@ -593,6 +598,7 @@ class ResourceCommandResolver { case Status.INDEX_RENAMED: case Status.INTENT_TO_RENAME: case Status.TYPE_CHANGED: + case Status.INDEX_TYPE_CHANGED: return { original: toGitUri(resource.original, 'HEAD') }; case Status.MODIFIED: @@ -611,6 +617,7 @@ class ResourceCommandResolver { case Status.INDEX_ADDED: case Status.INDEX_COPIED: case Status.INDEX_RENAMED: + case Status.INDEX_TYPE_CHANGED: return { modified: toGitUri(resource.resourceUri, '') }; case Status.INDEX_DELETED: @@ -653,6 +660,7 @@ class ResourceCommandResolver { case Status.INDEX_MODIFIED: case Status.INDEX_RENAMED: case Status.INDEX_ADDED: + case Status.INDEX_TYPE_CHANGED: return l10n.t('{0} (Index)', basename); case Status.MODIFIED: @@ -3074,6 +3082,7 @@ export class Repository implements Disposable { case 'D': indexGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Index, uri, Status.INDEX_DELETED, useIcons, undefined, this.kind)); break; case 'R': indexGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Index, uri, Status.INDEX_RENAMED, useIcons, renameUri, this.kind)); break; case 'C': indexGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Index, uri, Status.INDEX_COPIED, useIcons, renameUri, this.kind)); break; + case 'T': indexGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Index, uri, Status.INDEX_TYPE_CHANGED, useIcons, undefined, this.kind)); break; } switch (raw.y) { diff --git a/extensions/git/src/test/smoke.test.ts b/extensions/git/src/test/smoke.test.ts index c2870a2631ee3..946307ed99e0f 100644 --- a/extensions/git/src/test/smoke.test.ts +++ b/extensions/git/src/test/smoke.test.ts @@ -147,6 +147,36 @@ suite('git smoke test', function () { assert.strictEqual(repository.state.indexChanges.length, 0); }); + test('reflects type changes', async function () { + if (process.platform === 'win32') { + this.skip(); // symlinks require elevated privileges on Windows + } + + const link = file('link'); + fs.symlinkSync('newfile.txt', link); + cp.execSync('git add link', { cwd }); + cp.execSync('git commit -m "add symlink"', { cwd }); + + // Replace the symlink with a regular file + fs.unlinkSync(link); + fs.writeFileSync(link, 'no longer a symlink', 'utf8'); + await repository.status(); + + assert.strictEqual(repository.state.workingTreeChanges.length, 1); + assert.strictEqual(repository.state.workingTreeChanges[0].uri.path, uri('link').path); + assert.strictEqual(repository.state.workingTreeChanges[0].status, Status.TYPE_CHANGED); + + await repository.add([link]); + + assert.strictEqual(repository.state.indexChanges.length, 1); + assert.strictEqual(repository.state.indexChanges[0].uri.path, uri('link').path); + assert.strictEqual(repository.state.indexChanges[0].status, Status.INDEX_TYPE_CHANGED); + + await repository.commit('replace symlink with file'); + assert.strictEqual(repository.state.workingTreeChanges.length, 0); + assert.strictEqual(repository.state.indexChanges.length, 0); + }); + test('rename/delete conflict', async function () { await commands.executeCommand('workbench.view.scm'); diff --git a/extensions/github/src/typings/git.constants.ts b/extensions/github/src/typings/git.constants.ts index e39a3fb03b327..562b93248a7a4 100644 --- a/extensions/github/src/typings/git.constants.ts +++ b/extensions/github/src/typings/git.constants.ts @@ -44,6 +44,8 @@ export const Status = Object.freeze({ BOTH_ADDED: 16, BOTH_DELETED: 17, BOTH_MODIFIED: 18, + + INDEX_TYPE_CHANGED: 19, }) satisfies typeof git.Status; export const GitErrorCodes = Object.freeze({ diff --git a/extensions/github/src/typings/git.d.ts b/extensions/github/src/typings/git.d.ts index e600b767c7cd1..e82d12951e8fe 100644 --- a/extensions/github/src/typings/git.d.ts +++ b/extensions/github/src/typings/git.d.ts @@ -87,7 +87,9 @@ export const enum Status { DELETED_BY_THEM, BOTH_ADDED, BOTH_DELETED, - BOTH_MODIFIED + BOTH_MODIFIED, + + INDEX_TYPE_CHANGED } export interface Change {