Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions extensions/git/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,6 @@
{
"command": "git.branch",
"title": "%command.branch%",
"icon": "$(plus)",
"category": "Git",
"enablement": "!operationInProgress"
},
Expand Down Expand Up @@ -1040,6 +1039,20 @@
"title": "%command.graphCompareRef%",
"category": "Git",
"enablement": "!operationInProgress"
},
{
"command": "git.repositories.createBranch",
"title": "%command.branch%",
"icon": "$(plus)",
"category": "Git",
"enablement": "!operationInProgress"
},
{
"command": "git.repositories.createTag",
"title": "%command.createTag%",
"icon": "$(plus)",
"category": "Git",
"enablement": "!operationInProgress"
}
],
"continueEditSession": [
Expand Down Expand Up @@ -1675,6 +1688,14 @@
{
"command": "git.repositories.compareRef",
"when": "false"
},
{
"command": "git.repositories.createBranch",
"when": "false"
},
{
"command": "git.repositories.createTag",
"when": "false"
}
],
"scm/title": [
Expand Down Expand Up @@ -1867,12 +1888,12 @@
],
"scm/artifactGroup/context": [
{
"command": "git.branch",
"command": "git.repositories.createBranch",
"group": "inline@1",
"when": "scmProvider == git && scmArtifactGroup == branches"
},
{
"command": "git.createTag",
"command": "git.repositories.createTag",
"group": "inline@1",
"when": "scmProvider == git && scmArtifactGroup == tags"
}
Expand Down
58 changes: 40 additions & 18 deletions extensions/git/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3363,24 +3363,7 @@ export class CommandCenter {

@command('git.createTag', { repository: true })
async createTag(repository: Repository, historyItem?: SourceControlHistoryItem): Promise<void> {
const inputTagName = await window.showInputBox({
placeHolder: l10n.t('Tag name'),
prompt: l10n.t('Please provide a tag name'),
ignoreFocusOut: true
});

if (!inputTagName) {
return;
}

const inputMessage = await window.showInputBox({
placeHolder: l10n.t('Message'),
prompt: l10n.t('Please provide a message to annotate the tag'),
ignoreFocusOut: true
});

const name = inputTagName.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$/g, '-');
await repository.tag({ name, message: inputMessage, ref: historyItem?.id });
await this._createTag(repository, historyItem?.id);
}

@command('git.deleteTag', { repository: true })
Expand Down Expand Up @@ -5180,6 +5163,24 @@ export class CommandCenter {
config.update(setting, !enabled, true);
}

@command('git.repositories.createBranch', { repository: true })
async artifactGroupCreateBranch(repository: Repository): Promise<void> {
if (!repository) {
return;
}

await this._branch(repository, undefined, false);
}

@command('git.repositories.createTag', { repository: true })
async artifactGroupCreateTag(repository: Repository): Promise<void> {
if (!repository) {
return;
}

await this._createTag(repository);
}

@command('git.repositories.checkout', { repository: true })
async artifactCheckout(repository: Repository, artifact: SourceControlArtifact): Promise<void> {
if (!repository || !artifact) {
Expand Down Expand Up @@ -5233,6 +5234,27 @@ export class CommandCenter {
`${sourceRef.ref.name} ↔ ${artifact.name}`);
}

private async _createTag(repository: Repository, ref?: string): Promise<void> {
const inputTagName = await window.showInputBox({
placeHolder: l10n.t('Tag name'),
prompt: l10n.t('Please provide a tag name'),
ignoreFocusOut: true
});

if (!inputTagName) {
return;
}

const inputMessage = await window.showInputBox({
placeHolder: l10n.t('Message'),
prompt: l10n.t('Please provide a message to annotate the tag'),
ignoreFocusOut: true
});

const name = inputTagName.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$/g, '-');
await repository.tag({ name, message: inputMessage, ref });
}

private createCommand(id: string, key: string, method: Function, options: ScmCommandOptions): (...args: any[]) => any {
const result = (...args: any[]) => {
let result: Promise<any>;
Expand Down
9 changes: 5 additions & 4 deletions src/vs/workbench/contrib/scm/browser/media/scm.css
Original file line number Diff line number Diff line change
Expand Up @@ -538,17 +538,18 @@
.scm-repositories-view .scm-artifact-group,
.scm-repositories-view .scm-artifact {
display: flex;
align-items: center;

.icon {
margin-right: 2px;
}
}

.scm-repositories-view .scm-artifact-group .monaco-icon-label,
.scm-repositories-view .scm-artifact .monaco-icon-label {
flex-grow: 1;
}

.scm-repositories-view .scm-artifact .monaco-icon-label-container {
display: flex;
}

.scm-repositories-view .scm-artifact-group .monaco-highlighted-label,
.scm-repositories-view .scm-artifact .monaco-highlighted-label {
display: flex;
Expand Down
83 changes: 56 additions & 27 deletions src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { basename } from '../../../../base/common/resources.js';
import { ICompressibleTreeRenderer } from '../../../../base/browser/ui/tree/objectTree.js';
import { ICompressedTreeNode } from '../../../../base/browser/ui/tree/compressedObjectTreeModel.js';
import { ITreeCompressionDelegate } from '../../../../base/browser/ui/tree/asyncDataTree.js';
import { Codicon } from '../../../../base/common/codicons.js';
Copy link

Copilot AI Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The Codicon import is only used for Codicon.folder. Consider using a more specific import or verify if ThemeIcon already provides folder icon constants that would be more consistent with the existing icon handling pattern in this file.

Copilot uses AI. Check for mistakes.

type TreeElement = ISCMRepository | SCMArtifactGroupTreeElement | SCMArtifactTreeElement | IResourceNode<SCMArtifactTreeElement, SCMArtifactGroupTreeElement>;

Expand All @@ -66,6 +67,7 @@ class ListDelegate implements IListVirtualDelegate<ISCMRepository> {
}

interface ArtifactGroupTemplate {
readonly icon: HTMLElement;
readonly label: IconLabel;
readonly actionBar: WorkbenchToolBar;
readonly elementDisposables: DisposableStore;
Expand All @@ -89,21 +91,23 @@ class ArtifactGroupRenderer implements ICompressibleTreeRenderer<SCMArtifactGrou

renderTemplate(container: HTMLElement): ArtifactGroupTemplate {
const element = append(container, $('.scm-artifact-group'));
const label = new IconLabel(element, { supportIcons: true });
const icon = append(element, $('.icon'));
const label = new IconLabel(element, { supportIcons: false });

const actionsContainer = append(element, $('.actions'));
const actionBar = new WorkbenchToolBar(actionsContainer, undefined, this._menuService, this._contextKeyService, this._contextMenuService, this._keybindingService, this._commandService, this._telemetryService);

return { label, actionBar, elementDisposables: new DisposableStore(), templateDisposable: combinedDisposable(label, actionBar) };
return { icon, label, actionBar, elementDisposables: new DisposableStore(), templateDisposable: combinedDisposable(label, actionBar) };
}

renderElement(node: ITreeNode<SCMArtifactGroupTreeElement, FuzzyScore>, index: number, templateData: ArtifactGroupTemplate): void {
const provider = node.element.repository.provider;
const artifactGroup = node.element.artifactGroup;
const artifactGroupIcon = ThemeIcon.isThemeIcon(artifactGroup.icon)
? `$(${artifactGroup.icon.id}) ` : '';

templateData.label.setLabel(`${artifactGroupIcon}${artifactGroup.name}`);
templateData.icon.className = ThemeIcon.isThemeIcon(artifactGroup.icon)
? `icon ${ThemeIcon.asClassName(artifactGroup.icon)}`
: '';
templateData.label.setLabel(artifactGroup.name);

const repositoryMenus = this._scmViewService.menus.getRepositoryMenus(provider);
templateData.elementDisposables.add(connectPrimaryMenu(repositoryMenus.getArtifactGroupMenu(artifactGroup), primary => {
Expand All @@ -127,6 +131,7 @@ class ArtifactGroupRenderer implements ICompressibleTreeRenderer<SCMArtifactGrou
}

interface ArtifactTemplate {
readonly icon: HTMLElement;
readonly label: IconLabel;
readonly actionBar: WorkbenchToolBar;
readonly elementDisposables: DisposableStore;
Expand All @@ -150,32 +155,35 @@ class ArtifactRenderer implements ICompressibleTreeRenderer<SCMArtifactTreeEleme

renderTemplate(container: HTMLElement): ArtifactTemplate {
const element = append(container, $('.scm-artifact'));
const label = new IconLabel(element, { supportIcons: true });
const icon = append(element, $('.icon'));
const label = new IconLabel(element, { supportIcons: false });

const actionsContainer = append(element, $('.actions'));
const actionBar = new WorkbenchToolBar(actionsContainer, undefined, this._menuService, this._contextKeyService, this._contextMenuService, this._keybindingService, this._commandService, this._telemetryService);

return { label, actionBar, elementDisposables: new DisposableStore(), templateDisposable: combinedDisposable(label, actionBar) };
return { icon, label, actionBar, elementDisposables: new DisposableStore(), templateDisposable: combinedDisposable(label, actionBar) };
}

renderElement(nodeOrElement: ITreeNode<SCMArtifactTreeElement | IResourceNode<SCMArtifactTreeElement, SCMArtifactGroupTreeElement>, FuzzyScore>, index: number, templateData: ArtifactTemplate): void {
const artifactOrFolder = nodeOrElement.element;

if (isSCMArtifactNode(artifactOrFolder)) {
// Folder
templateData.label.setLabel(`$(folder) ${basename(artifactOrFolder.uri)}`);
templateData.icon.className = `icon ${ThemeIcon.asClassName(Codicon.folder)}`;
templateData.label.setLabel(basename(artifactOrFolder.uri));

templateData.actionBar.setActions([]);
templateData.actionBar.context = undefined;
} else {
// Artifact
const artifact = artifactOrFolder.artifact;
const artifactIcon = ThemeIcon.isThemeIcon(artifactOrFolder.group.icon)
? `$(${artifactOrFolder.group.icon.id}) `

templateData.icon.className = ThemeIcon.isThemeIcon(artifactOrFolder.group.icon)
? `icon ${ThemeIcon.asClassName(artifactOrFolder.group.icon)}`
: '';

const artifactLabel = artifact.name.split('/').pop() ?? artifact.name;
templateData.label.setLabel(`${artifactIcon}${artifactLabel}`, artifact.description);
templateData.label.setLabel(artifactLabel, artifact.description);

const provider = artifactOrFolder.repository.provider;
const repositoryMenus = this._scmViewService.menus.getRepositoryMenus(provider);
Expand All @@ -187,12 +195,30 @@ class ArtifactRenderer implements ICompressibleTreeRenderer<SCMArtifactTreeEleme
}

renderCompressedElements(node: ITreeNode<ICompressedTreeNode<SCMArtifactTreeElement | IResourceNode<SCMArtifactTreeElement, SCMArtifactGroupTreeElement>>, FuzzyScore>, index: number, templateData: ArtifactTemplate, details?: ITreeElementRenderDetails): void {
const compressed = node.element as ICompressedTreeNode<IResourceNode<SCMArtifactTreeElement, SCMArtifactGroupTreeElement>>;
const folder = compressed.elements[compressed.elements.length - 1];
templateData.label.setLabel(`$(folder) ${folder.uri.fsPath.substring(1)}`);
const compressed = node.element;
const artifactOrFolder = compressed.elements[compressed.elements.length - 1];

if (isSCMArtifactTreeElement(artifactOrFolder)) {
const artifact = artifactOrFolder.artifact;

templateData.icon.className = ThemeIcon.isThemeIcon(artifactOrFolder.group.icon)
? `icon ${ThemeIcon.asClassName(artifactOrFolder.group.icon)}`
: '';
templateData.label.setLabel(artifact.name, artifact.description);

const provider = artifactOrFolder.repository.provider;
const repositoryMenus = this._scmViewService.menus.getRepositoryMenus(provider);
templateData.elementDisposables.add(connectPrimaryMenu(repositoryMenus.getArtifactMenu(artifactOrFolder.group), primary => {
templateData.actionBar.setActions(primary);
}, 'inline', provider));
templateData.actionBar.context = artifact;
} else if (ResourceTree.isResourceNode(artifactOrFolder)) {
templateData.icon.className = `icon ${ThemeIcon.asClassName(Codicon.folder)}`;
templateData.label.setLabel(artifactOrFolder.uri.fsPath.substring(1));

templateData.actionBar.setActions([]);
templateData.actionBar.context = undefined;
templateData.actionBar.setActions([]);
templateData.actionBar.context = undefined;
}
}

disposeElement(element: ITreeNode<SCMArtifactTreeElement | IResourceNode<SCMArtifactTreeElement, SCMArtifactGroupTreeElement>, FuzzyScore>, index: number, templateData: ArtifactTemplate, details?: ITreeElementRenderDetails): void {
Expand Down Expand Up @@ -307,6 +333,10 @@ class RepositoriesTreeCompressionDelegate implements ITreeCompressionDelegate<Tr
isIncompressible(element: TreeElement): boolean {
if (ResourceTree.isResourceNode(element)) {
return element.childrenCount === 0 || !element.parent || !element.parent.parent;
} else if (isSCMArtifactTreeElement(element)) {
// Artifacts are never incompressible as this allows us to
// compress an artifact that is on its own in the folder
return false;
}

return true;
Expand Down Expand Up @@ -440,18 +470,17 @@ export class SCMRepositoriesViewPane extends ViewPane {
}

// Explorer mode
// Expand artifact folders with one child only
if (isSCMArtifactNode(e)) {
if (e.childrenCount !== 1) {
return true;
}

// Check if the only child is a leaf node
const firstChild = Iterable.first(e.children);
return firstChild?.element !== undefined;
if (isSCMRepository(e)) {
return true;
} else if (isSCMArtifactGroupTreeElement(e)) {
return true;
} else if (isSCMArtifactTreeElement(e)) {
return false;
} else if (isSCMArtifactNode(e)) {
return e.childrenCount !== 1;
} else {
return true;
}

return true;
},
compressionEnabled: true,
overrideStyles: this.getLocationBasedColors().listOverrideStyles,
Expand Down
Loading