mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-02 08:15:56 +01:00
Sessions - initial implementation for git changes (#299855)
* Sessions - initial implementation of repository changes * Deduplicate resources and fix badge
This commit is contained in:
@@ -13,9 +13,9 @@ import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import { MarkdownString } from '../../../../base/common/htmlContent.js';
|
||||
import { Iterable } from '../../../../base/common/iterator.js';
|
||||
import { DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { autorun, derived, derivedOpts, IObservable, IObservableWithChange, observableFromEvent, observableValue } from '../../../../base/common/observable.js';
|
||||
import { autorun, derived, derivedOpts, IObservable, IObservableWithChange, observableFromEvent, observableFromPromise, observableValue } from '../../../../base/common/observable.js';
|
||||
import { basename, dirname } from '../../../../base/common/path.js';
|
||||
import { isEqual } from '../../../../base/common/resources.js';
|
||||
import { extUriBiasedIgnorePathCase, isEqual } from '../../../../base/common/resources.js';
|
||||
import { ThemeIcon } from '../../../../base/common/themables.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { localize } from '../../../../nls.js';
|
||||
@@ -58,6 +58,7 @@ import { IWorkbenchLayoutService } from '../../../../workbench/services/layout/b
|
||||
import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js';
|
||||
import { GITHUB_REMOTE_FILE_SCHEME } from '../../fileTreeView/browser/githubFileSystemProvider.js';
|
||||
import { CodeReviewStateKind, getCodeReviewFilesFromSessionChanges, getCodeReviewVersion, ICodeReviewService } from '../../codeReview/browser/codeReviewService.js';
|
||||
import { IGitService } from '../../../../workbench/contrib/git/common/gitService.js';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
@@ -101,6 +102,8 @@ interface IChangesFolderItem {
|
||||
interface IActiveSession {
|
||||
readonly resource: URI;
|
||||
readonly sessionType: string;
|
||||
readonly repository: URI | undefined;
|
||||
readonly worktree: URI | undefined;
|
||||
}
|
||||
|
||||
type ChangesTreeElement = IChangesFileItem | IChangesFolderItem;
|
||||
@@ -230,6 +233,7 @@ export class ChangesViewPane extends ViewPane {
|
||||
private readonly activeSession: IObservableWithChange<IActiveSession | undefined>;
|
||||
private readonly activeSessionFileCountObs: IObservableWithChange<number>;
|
||||
private readonly activeSessionHasChangesObs: IObservableWithChange<boolean>;
|
||||
private readonly activeSessionRepositoryChangesObs: IObservableWithChange<IChangesFileItem[] | undefined>;
|
||||
|
||||
get activeSessionHasChanges(): IObservable<boolean> {
|
||||
return this.activeSessionHasChangesObs;
|
||||
@@ -257,6 +261,7 @@ export class ChangesViewPane extends ViewPane {
|
||||
@ILabelService private readonly labelService: ILabelService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@ICodeReviewService private readonly codeReviewService: ICodeReviewService,
|
||||
@IGitService private readonly gitService: IGitService,
|
||||
) {
|
||||
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
|
||||
|
||||
@@ -278,16 +283,49 @@ export class ChangesViewPane extends ViewPane {
|
||||
|
||||
return {
|
||||
resource: activeSession.resource,
|
||||
repository: activeSession.repository,
|
||||
worktree: activeSession.worktree,
|
||||
sessionType: getChatSessionType(activeSession.resource),
|
||||
};
|
||||
}).recomputeInitiallyAndOnChange(this._store);
|
||||
|
||||
// Track active session repository changes
|
||||
const repositoryObs = derived(reader => {
|
||||
const activeSessionWorktree = this.activeSession.read(reader)?.worktree;
|
||||
if (!activeSessionWorktree) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return observableFromPromise(this.gitService.openRepository(activeSessionWorktree));
|
||||
});
|
||||
|
||||
this.activeSessionRepositoryChangesObs = derived(reader => {
|
||||
const repository = repositoryObs.read(reader)?.read(reader);
|
||||
if (!repository) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const state = repository.value?.state.read(reader);
|
||||
return (state?.workingTreeChanges ?? []).map(change => {
|
||||
const isDeletion = change.modifiedUri === undefined;
|
||||
const isAddition = change.originalUri === undefined;
|
||||
return {
|
||||
type: 'file',
|
||||
uri: change.modifiedUri ?? change.uri,
|
||||
originalUri: change.originalUri,
|
||||
state: ModifiedFileEntryState.Accepted,
|
||||
isDeletion,
|
||||
changeType: isDeletion ? 'deleted' : isAddition ? 'added' : 'modified',
|
||||
reviewCommentCount: 0,
|
||||
linesAdded: 0,
|
||||
linesRemoved: 0,
|
||||
} satisfies IChangesFileItem;
|
||||
});
|
||||
});
|
||||
|
||||
this.activeSessionFileCountObs = this.createActiveSessionFileCountObservable();
|
||||
this.activeSessionHasChangesObs = this.activeSessionFileCountObs.map(fileCount => fileCount > 0).recomputeInitiallyAndOnChange(this._store);
|
||||
|
||||
// Setup badge tracking
|
||||
this.registerBadgeTracking();
|
||||
|
||||
// Set chatSessionType on the view's context key service so ViewTitle
|
||||
// menu items can use it in their `when` clauses. Update reactively
|
||||
// when the active session changes.
|
||||
@@ -298,14 +336,6 @@ export class ChangesViewPane extends ViewPane {
|
||||
}));
|
||||
}
|
||||
|
||||
private registerBadgeTracking(): void {
|
||||
// Update badge when file count changes
|
||||
this._register(autorun(reader => {
|
||||
const fileCount = this.activeSessionFileCountObs.read(reader);
|
||||
this.updateBadge(fileCount);
|
||||
}));
|
||||
}
|
||||
|
||||
private createActiveSessionFileCountObservable(): IObservableWithChange<number> {
|
||||
const activeSessionResource = this.activeSession.map(a => a?.resource);
|
||||
|
||||
@@ -532,13 +562,24 @@ export class ChangesViewPane extends ViewPane {
|
||||
const combinedEntriesObs = derived(reader => {
|
||||
const editEntries = editSessionEntriesObs.read(reader);
|
||||
const sessionFiles = sessionFilesObs.read(reader);
|
||||
return [...editEntries, ...sessionFiles];
|
||||
const repositoryFiles = this.activeSessionRepositoryChangesObs.read(reader) ?? [];
|
||||
|
||||
const resources = new Set();
|
||||
const entries: IChangesFileItem[] = [];
|
||||
for (const item of [...editEntries, ...sessionFiles, ...repositoryFiles]) {
|
||||
if (!resources.has(item.uri.fsPath)) {
|
||||
resources.add(item.uri.fsPath);
|
||||
entries.push(item);
|
||||
}
|
||||
}
|
||||
return entries.sort((a, b) => extUriBiasedIgnorePathCase.compare(a.uri, b.uri));
|
||||
});
|
||||
|
||||
// Calculate stats from combined entries
|
||||
const topLevelStats = derived(reader => {
|
||||
const editEntries = editSessionEntriesObs.read(reader);
|
||||
const sessionFiles = sessionFilesObs.read(reader);
|
||||
const repositoryFiles = this.activeSessionRepositoryChangesObs.read(reader) ?? [];
|
||||
const entries = combinedEntriesObs.read(reader);
|
||||
|
||||
let added = 0, removed = 0;
|
||||
@@ -549,7 +590,7 @@ export class ChangesViewPane extends ViewPane {
|
||||
}
|
||||
|
||||
const files = entries.length;
|
||||
const isSessionMenu = editEntries.length === 0 && sessionFiles.length > 0;
|
||||
const isSessionMenu = editEntries.length === 0 && (sessionFiles.length > 0 || repositoryFiles.length > 0);
|
||||
|
||||
return { files, added, removed, isSessionMenu };
|
||||
});
|
||||
@@ -653,6 +694,11 @@ export class ChangesViewPane extends ViewPane {
|
||||
dom.setVisibility(!hasEntries, this.welcomeContainer!);
|
||||
}));
|
||||
|
||||
// Update badge when file count changes
|
||||
this.renderDisposables.add(autorun(reader => {
|
||||
this.updateBadge(topLevelStats.read(reader).files);
|
||||
}));
|
||||
|
||||
// Update summary text (line counts only, file count is shown in badge)
|
||||
if (this.summaryContainer) {
|
||||
dom.clearNode(this.summaryContainer);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { ResourceMap } from '../../../base/common/map.js';
|
||||
import { URI } from '../../../base/common/uri.js';
|
||||
import { GitRepository } from '../../contrib/git/browser/gitService.js';
|
||||
import { IGitExtensionDelegate, IGitService, GitRef, GitRefQuery, GitRefType, GitRepositoryState, GitBranch, IGitRepository } from '../../contrib/git/common/gitService.js';
|
||||
import { IGitExtensionDelegate, IGitService, GitRef, GitRefQuery, GitRefType, GitRepositoryState, GitBranch, GitChange, IGitRepository } from '../../contrib/git/common/gitService.js';
|
||||
import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';
|
||||
import { ExtHostContext, ExtHostGitExtensionShape, GitRefTypeDto, GitRepositoryStateDto, MainContext, MainThreadGitExtensionShape } from '../common/extHost.protocol.js';
|
||||
|
||||
@@ -32,6 +32,26 @@ function toGitRepositoryState(dto: GitRepositoryStateDto | undefined): GitReposi
|
||||
ahead: dto.HEAD.ahead,
|
||||
behind: dto.HEAD.behind,
|
||||
} satisfies GitBranch : undefined,
|
||||
mergeChanges: dto?.mergeChanges?.map(c => ({
|
||||
uri: URI.revive(c.uri),
|
||||
originalUri: c.originalUri ? URI.revive(c.originalUri) : undefined,
|
||||
modifiedUri: c.modifiedUri ? URI.revive(c.modifiedUri) : undefined,
|
||||
} satisfies GitChange)) ?? [],
|
||||
indexChanges: dto?.indexChanges?.map(c => ({
|
||||
uri: URI.revive(c.uri),
|
||||
originalUri: c.originalUri ? URI.revive(c.originalUri) : undefined,
|
||||
modifiedUri: c.modifiedUri ? URI.revive(c.modifiedUri) : undefined,
|
||||
} satisfies GitChange)) ?? [],
|
||||
workingTreeChanges: dto?.workingTreeChanges?.map(c => ({
|
||||
uri: URI.revive(c.uri),
|
||||
originalUri: c.originalUri ? URI.revive(c.originalUri) : undefined,
|
||||
modifiedUri: c.modifiedUri ? URI.revive(c.modifiedUri) : undefined,
|
||||
} satisfies GitChange)) ?? [],
|
||||
untrackedChanges: dto?.untrackedChanges?.map(c => ({
|
||||
uri: URI.revive(c.uri),
|
||||
originalUri: c.originalUri ? URI.revive(c.originalUri) : undefined,
|
||||
modifiedUri: c.modifiedUri ? URI.revive(c.modifiedUri) : undefined,
|
||||
} satisfies GitChange)) ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3627,8 +3627,18 @@ export interface GitRefDto {
|
||||
readonly revision: string;
|
||||
}
|
||||
|
||||
export interface GitChangeDto {
|
||||
readonly uri: UriComponents;
|
||||
readonly originalUri: UriComponents | undefined;
|
||||
readonly modifiedUri: UriComponents | undefined;
|
||||
}
|
||||
|
||||
export interface GitRepositoryStateDto {
|
||||
readonly HEAD?: GitBranchDto;
|
||||
readonly mergeChanges: readonly GitChangeDto[];
|
||||
readonly indexChanges: readonly GitChangeDto[];
|
||||
readonly workingTreeChanges: readonly GitChangeDto[];
|
||||
readonly untrackedChanges: readonly GitChangeDto[];
|
||||
}
|
||||
|
||||
export interface GitBranchDto {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ExtensionIdentifier } from '../../../platform/extensions/common/extensi
|
||||
import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';
|
||||
import { IExtHostExtensionService } from './extHostExtensionService.js';
|
||||
import { IExtHostRpcService } from './extHostRpcService.js';
|
||||
import { ExtHostGitExtensionShape, GitBranchDto, GitRefDto, GitRefQueryDto, GitRefTypeDto, GitRepositoryStateDto, GitUpstreamRefDto, MainContext, MainThreadGitExtensionShape } from './extHost.protocol.js';
|
||||
import { ExtHostGitExtensionShape, GitBranchDto, GitChangeDto, GitRefDto, GitRefQueryDto, GitRefTypeDto, GitRepositoryStateDto, GitUpstreamRefDto, MainContext, MainThreadGitExtensionShape } from './extHost.protocol.js';
|
||||
import { ResourceMap } from '../../../base/common/map.js';
|
||||
|
||||
const GIT_EXTENSION_ID = 'vscode.git';
|
||||
@@ -45,6 +45,52 @@ function toGitUpstreamRefDto(upstream: UpstreamRef): GitUpstreamRefDto {
|
||||
};
|
||||
}
|
||||
|
||||
// Status values from the git extension's const enum Status
|
||||
const enum GitStatus {
|
||||
INDEX_ADDED = 1,
|
||||
INDEX_DELETED = 2,
|
||||
INDEX_RENAMED = 3,
|
||||
MODIFIED = 5,
|
||||
DELETED = 6,
|
||||
UNTRACKED = 7,
|
||||
INTENT_TO_ADD = 9,
|
||||
INTENT_TO_RENAME = 10,
|
||||
}
|
||||
|
||||
function toGitChangeDto(change: Change): GitChangeDto {
|
||||
switch (change.status) {
|
||||
// Added: no original
|
||||
case GitStatus.INDEX_ADDED:
|
||||
case GitStatus.UNTRACKED:
|
||||
case GitStatus.INTENT_TO_ADD:
|
||||
return { uri: change.uri, originalUri: undefined, modifiedUri: change.uri };
|
||||
|
||||
// Deleted: no modified
|
||||
case GitStatus.INDEX_DELETED:
|
||||
case GitStatus.DELETED:
|
||||
return { uri: change.uri, originalUri: change.uri, modifiedUri: undefined };
|
||||
|
||||
// Renamed: original is old name, modified is new name
|
||||
case GitStatus.INDEX_RENAMED:
|
||||
case GitStatus.INTENT_TO_RENAME:
|
||||
return { uri: change.uri, originalUri: change.originalUri, modifiedUri: change.renameUri };
|
||||
|
||||
// Modified and everything else: both original and modified
|
||||
default:
|
||||
return { uri: change.uri, originalUri: change.originalUri, modifiedUri: change.uri };
|
||||
}
|
||||
}
|
||||
|
||||
function toGitRepositoryStateDto(state: RepositoryState): GitRepositoryStateDto {
|
||||
return {
|
||||
HEAD: state.HEAD ? toGitBranchDto(state.HEAD) : undefined,
|
||||
mergeChanges: state.mergeChanges.map(toGitChangeDto),
|
||||
indexChanges: state.indexChanges.map(toGitChangeDto),
|
||||
workingTreeChanges: state.workingTreeChanges.map(toGitChangeDto),
|
||||
untrackedChanges: state.untrackedChanges.map(toGitChangeDto),
|
||||
};
|
||||
}
|
||||
|
||||
interface Repository {
|
||||
readonly rootUri: vscode.Uri;
|
||||
readonly state: RepositoryState;
|
||||
@@ -53,8 +99,19 @@ interface Repository {
|
||||
getRefs(query: GitRefQuery, token?: vscode.CancellationToken): Promise<GitRef[]>;
|
||||
}
|
||||
|
||||
interface Change {
|
||||
readonly uri: vscode.Uri;
|
||||
readonly originalUri: vscode.Uri;
|
||||
readonly renameUri: vscode.Uri | undefined;
|
||||
readonly status: number;
|
||||
}
|
||||
|
||||
interface RepositoryState {
|
||||
readonly HEAD: Branch | undefined;
|
||||
readonly mergeChanges: Change[];
|
||||
readonly indexChanges: Change[];
|
||||
readonly workingTreeChanges: Change[];
|
||||
readonly untrackedChanges: Change[];
|
||||
readonly onDidChange: Event<void>;
|
||||
}
|
||||
|
||||
@@ -148,9 +205,7 @@ export class ExtHostGitExtensionService extends Disposable implements IExtHostGi
|
||||
return {
|
||||
handle: existingHandle,
|
||||
rootUri: repository.rootUri,
|
||||
state: {
|
||||
HEAD: repository.state.HEAD ? toGitBranchDto(repository.state.HEAD) : undefined
|
||||
}
|
||||
state: toGitRepositoryStateDto(repository.state),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -178,11 +233,7 @@ export class ExtHostGitExtensionService extends Disposable implements IExtHostGi
|
||||
return {
|
||||
handle,
|
||||
rootUri: repository.rootUri,
|
||||
state: {
|
||||
HEAD: repository.state.HEAD
|
||||
? toGitBranchDto(repository.state.HEAD)
|
||||
: undefined
|
||||
}
|
||||
state: toGitRepositoryStateDto(repository.state),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -225,8 +276,7 @@ export class ExtHostGitExtensionService extends Disposable implements IExtHostGi
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const state = repository.state;
|
||||
return { HEAD: state.HEAD ? toGitBranchDto(state.HEAD) : undefined };
|
||||
return toGitRepositoryStateDto(repository.state);
|
||||
}
|
||||
|
||||
private async _ensureGitApi(): Promise<GitExtensionAPI | undefined> {
|
||||
|
||||
@@ -29,8 +29,18 @@ export interface GitRefQuery {
|
||||
readonly sort?: 'alphabetically' | 'committerdate' | 'creatordate';
|
||||
}
|
||||
|
||||
export interface GitChange {
|
||||
readonly uri: URI;
|
||||
readonly originalUri: URI | undefined;
|
||||
readonly modifiedUri: URI | undefined;
|
||||
}
|
||||
|
||||
export interface GitRepositoryState {
|
||||
readonly HEAD?: GitBranch;
|
||||
readonly mergeChanges: readonly GitChange[];
|
||||
readonly indexChanges: readonly GitChange[];
|
||||
readonly workingTreeChanges: readonly GitChange[];
|
||||
readonly untrackedChanges: readonly GitChange[];
|
||||
}
|
||||
|
||||
export interface GitBranch extends GitRef {
|
||||
|
||||
Reference in New Issue
Block a user