mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-24 18:49:00 +01:00
Git - add "Stashes" node to the repositories view (#279400)
* WIP - Initial implementation * Get author and committer date for a stash * Add drop stash command * More cleanup
This commit is contained in:
@@ -4,9 +4,10 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { LogOutputChannel, SourceControlArtifactProvider, SourceControlArtifactGroup, SourceControlArtifact, Event, EventEmitter, ThemeIcon, l10n, workspace, Uri, Disposable } from 'vscode';
|
||||
import { dispose, fromNow, IDisposable } from './util';
|
||||
import { dispose, filterEvent, fromNow, getStashDescription, IDisposable } from './util';
|
||||
import { Repository } from './repository';
|
||||
import { Ref, RefType } from './api/git';
|
||||
import { OperationKind } from './operation';
|
||||
|
||||
function getArtifactDescription(ref: Ref, shortCommitLength: number): string {
|
||||
const segments: string[] = [];
|
||||
@@ -82,6 +83,7 @@ export class GitArtifactProvider implements SourceControlArtifactProvider, IDisp
|
||||
) {
|
||||
this._groups = [
|
||||
{ id: 'branches', name: l10n.t('Branches'), icon: new ThemeIcon('git-branch') },
|
||||
{ id: 'stashes', name: l10n.t('Stashes'), icon: new ThemeIcon('git-stash') },
|
||||
{ id: 'tags', name: l10n.t('Tags'), icon: new ThemeIcon('tag') }
|
||||
];
|
||||
|
||||
@@ -98,6 +100,15 @@ export class GitArtifactProvider implements SourceControlArtifactProvider, IDisp
|
||||
|
||||
this._onDidChangeArtifacts.fire(Array.from(groups));
|
||||
}));
|
||||
|
||||
const onDidRunWriteOperation = filterEvent(
|
||||
repository.onDidRunOperation, e => !e.operation.readOnly);
|
||||
|
||||
this._disposables.push(onDidRunWriteOperation(result => {
|
||||
if (result.operation.kind === OperationKind.Stash) {
|
||||
this._onDidChangeArtifacts.fire(['stashes']);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
provideArtifactGroups(): SourceControlArtifactGroup[] {
|
||||
@@ -133,6 +144,15 @@ export class GitArtifactProvider implements SourceControlArtifactProvider, IDisp
|
||||
? new ThemeIcon('target')
|
||||
: new ThemeIcon('tag')
|
||||
}));
|
||||
} else if (group === 'stashes') {
|
||||
const stashes = await this.repository.getStashes();
|
||||
|
||||
return stashes.map(s => ({
|
||||
id: `stash@{${s.index}}`,
|
||||
name: s.description,
|
||||
description: getStashDescription(s),
|
||||
icon: new ThemeIcon('git-stash')
|
||||
}));
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(`[GitArtifactProvider][provideArtifacts] Error while providing artifacts for group '${group}': `, err);
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Model } from './model';
|
||||
import { GitResourceGroup, Repository, Resource, ResourceGroupType } from './repository';
|
||||
import { DiffEditorSelectionHunkToolbarContext, LineChange, applyLineChanges, getIndexDiffInformation, getModifiedRange, getWorkingTreeDiffInformation, intersectDiffWithRange, invertLineChange, toLineChanges, toLineRanges, compareLineChanges } from './staging';
|
||||
import { fromGitUri, toGitUri, isGitUri, toMergeUris, toMultiFileDiffEditorUris } from './uri';
|
||||
import { DiagnosticSeverityConfig, dispose, fromNow, getHistoryItemDisplayName, grep, isDefined, isDescendant, isLinuxSnap, isRemote, isWindows, pathEquals, relativePath, subject, toDiagnosticSeverity, truncate } from './util';
|
||||
import { DiagnosticSeverityConfig, dispose, fromNow, getHistoryItemDisplayName, getStashDescription, grep, isDefined, isDescendant, isLinuxSnap, isRemote, isWindows, pathEquals, relativePath, subject, toDiagnosticSeverity, truncate } from './util';
|
||||
import { GitTimelineItem } from './timelineProvider';
|
||||
import { ApiRepository } from './api/api1';
|
||||
import { getRemoteSourceActions, pickRemoteSource } from './remoteSource';
|
||||
@@ -333,7 +333,7 @@ class RepositoryItem implements QuickPickItem {
|
||||
class StashItem implements QuickPickItem {
|
||||
get label(): string { return `#${this.stash.index}: ${this.stash.description}`; }
|
||||
|
||||
get description(): string | undefined { return this.stash.branchName; }
|
||||
get description(): string | undefined { return getStashDescription(this.stash); }
|
||||
|
||||
constructor(readonly stash: Stash) { }
|
||||
}
|
||||
@@ -4544,7 +4544,7 @@ export class CommandCenter {
|
||||
return;
|
||||
}
|
||||
|
||||
await this._stashDrop(repository, stash);
|
||||
await this._stashDrop(repository, stash.index, stash.description);
|
||||
}
|
||||
|
||||
@command('git.stashDropAll', { repository: true })
|
||||
@@ -4577,15 +4577,15 @@ export class CommandCenter {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await this._stashDrop(result.repository, result.stash)) {
|
||||
if (await this._stashDrop(result.repository, result.stash.index, result.stash.description)) {
|
||||
await commands.executeCommand('workbench.action.closeActiveEditor');
|
||||
}
|
||||
}
|
||||
|
||||
async _stashDrop(repository: Repository, stash: Stash): Promise<boolean> {
|
||||
async _stashDrop(repository: Repository, index: number, description: string): Promise<boolean> {
|
||||
const yes = l10n.t('Yes');
|
||||
const result = await window.showWarningMessage(
|
||||
l10n.t('Are you sure you want to drop the stash: {0}?', stash.description),
|
||||
l10n.t('Are you sure you want to drop the stash: {0}?', description),
|
||||
{ modal: true },
|
||||
yes
|
||||
);
|
||||
@@ -4593,7 +4593,7 @@ export class CommandCenter {
|
||||
return false;
|
||||
}
|
||||
|
||||
await repository.dropStash(stash.index);
|
||||
await repository.dropStash(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -4606,36 +4606,7 @@ export class CommandCenter {
|
||||
return;
|
||||
}
|
||||
|
||||
const stashChanges = await repository.showStash(stash.index);
|
||||
if (!stashChanges || stashChanges.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A stash commit can have up to 3 parents:
|
||||
// 1. The first parent is the commit that was HEAD when the stash was created.
|
||||
// 2. The second parent is the commit that represents the index when the stash was created.
|
||||
// 3. The third parent (when present) represents the untracked files when the stash was created.
|
||||
const stashFirstParentCommit = stash.parents.length > 0 ? stash.parents[0] : `${stash.hash}^`;
|
||||
const stashUntrackedFilesParentCommit = stash.parents.length === 3 ? stash.parents[2] : undefined;
|
||||
const stashUntrackedFiles: string[] = [];
|
||||
|
||||
if (stashUntrackedFilesParentCommit) {
|
||||
const untrackedFiles = await repository.getObjectFiles(stashUntrackedFilesParentCommit);
|
||||
stashUntrackedFiles.push(...untrackedFiles.map(f => path.join(repository.root, f.file)));
|
||||
}
|
||||
|
||||
const title = `Git Stash #${stash.index}: ${stash.description}`;
|
||||
const multiDiffSourceUri = toGitUri(Uri.file(repository.root), `stash@{${stash.index}}`, { scheme: 'git-stash' });
|
||||
|
||||
const resources: { originalUri: Uri | undefined; modifiedUri: Uri | undefined }[] = [];
|
||||
for (const change of stashChanges) {
|
||||
const isChangeUntracked = !!stashUntrackedFiles.find(f => pathEquals(f, change.uri.fsPath));
|
||||
const modifiedUriRef = !isChangeUntracked ? stash.hash : stashUntrackedFilesParentCommit ?? stash.hash;
|
||||
|
||||
resources.push(toMultiFileDiffEditorUris(change, stashFirstParentCommit, modifiedUriRef));
|
||||
}
|
||||
|
||||
commands.executeCommand('_workbench.openMultiDiffEditor', { multiDiffSourceUri, title, resources });
|
||||
await this._viewStash(repository, stash);
|
||||
}
|
||||
|
||||
private async pickStash(repository: Repository, placeHolder: string): Promise<Stash | undefined> {
|
||||
@@ -4680,6 +4651,39 @@ export class CommandCenter {
|
||||
return { repository, stash };
|
||||
}
|
||||
|
||||
private async _viewStash(repository: Repository, stash: Stash): Promise<void> {
|
||||
const stashChanges = await repository.showStash(stash.index);
|
||||
if (!stashChanges || stashChanges.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A stash commit can have up to 3 parents:
|
||||
// 1. The first parent is the commit that was HEAD when the stash was created.
|
||||
// 2. The second parent is the commit that represents the index when the stash was created.
|
||||
// 3. The third parent (when present) represents the untracked files when the stash was created.
|
||||
const stashFirstParentCommit = stash.parents.length > 0 ? stash.parents[0] : `${stash.hash}^`;
|
||||
const stashUntrackedFilesParentCommit = stash.parents.length === 3 ? stash.parents[2] : undefined;
|
||||
const stashUntrackedFiles: string[] = [];
|
||||
|
||||
if (stashUntrackedFilesParentCommit) {
|
||||
const untrackedFiles = await repository.getObjectFiles(stashUntrackedFilesParentCommit);
|
||||
stashUntrackedFiles.push(...untrackedFiles.map(f => path.join(repository.root, f.file)));
|
||||
}
|
||||
|
||||
const title = `Git Stash #${stash.index}: ${stash.description}`;
|
||||
const multiDiffSourceUri = toGitUri(Uri.file(repository.root), `stash@{${stash.index}}`, { scheme: 'git-stash' });
|
||||
|
||||
const resources: { originalUri: Uri | undefined; modifiedUri: Uri | undefined }[] = [];
|
||||
for (const change of stashChanges) {
|
||||
const isChangeUntracked = !!stashUntrackedFiles.find(f => pathEquals(f, change.uri.fsPath));
|
||||
const modifiedUriRef = !isChangeUntracked ? stash.hash : stashUntrackedFilesParentCommit ?? stash.hash;
|
||||
|
||||
resources.push(toMultiFileDiffEditorUris(change, stashFirstParentCommit, modifiedUriRef));
|
||||
}
|
||||
|
||||
commands.executeCommand('_workbench.openMultiDiffEditor', { multiDiffSourceUri, title, resources });
|
||||
}
|
||||
|
||||
@command('git.timeline.openDiff', { repository: false })
|
||||
async timelineOpenDiff(item: TimelineItem, uri: Uri | undefined, _source: string) {
|
||||
const cmd = this.resolveTimelineOpenDiffCommand(
|
||||
@@ -5217,6 +5221,78 @@ export class CommandCenter {
|
||||
await repository.deleteTag(artifact.name);
|
||||
}
|
||||
|
||||
@command('git.repositories.stashView', { repository: true })
|
||||
async artifactStashView(repository: Repository, artifact: SourceControlArtifact): Promise<void> {
|
||||
if (!repository || !artifact) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract stash index from artifact id
|
||||
const regex = /^stash@\{(\d+)\}$/;
|
||||
const match = regex.exec(artifact.id);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stashes = await repository.getStashes();
|
||||
const stash = stashes.find(s => s.index === parseInt(match[1]));
|
||||
if (!stash) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this._viewStash(repository, stash);
|
||||
}
|
||||
|
||||
@command('git.repositories.stashApply', { repository: true })
|
||||
async artifactStashApply(repository: Repository, artifact: SourceControlArtifact): Promise<void> {
|
||||
if (!repository || !artifact) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract stash index from artifact id (format: "stash@{index}")
|
||||
const regex = /^stash@\{(\d+)\}$/;
|
||||
const match = regex.exec(artifact.id);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stashIndex = parseInt(match[1]);
|
||||
await repository.applyStash(stashIndex);
|
||||
}
|
||||
|
||||
@command('git.repositories.stashPop', { repository: true })
|
||||
async artifactStashPop(repository: Repository, artifact: SourceControlArtifact): Promise<void> {
|
||||
if (!repository || !artifact) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract stash index from artifact id (format: "stash@{index}")
|
||||
const regex = /^stash@\{(\d+)\}$/;
|
||||
const match = regex.exec(artifact.id);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stashIndex = parseInt(match[1]);
|
||||
await repository.popStash(stashIndex);
|
||||
}
|
||||
|
||||
@command('git.repositories.stashDrop', { repository: true })
|
||||
async artifactStashDrop(repository: Repository, artifact: SourceControlArtifact): Promise<void> {
|
||||
if (!repository || !artifact) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract stash index from artifact id
|
||||
const regex = /^stash@\{(\d+)\}$/;
|
||||
const match = regex.exec(artifact.id);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this._stashDrop(repository, parseInt(match[1]), artifact.name);
|
||||
}
|
||||
|
||||
private createCommand(id: string, key: string, method: Function, options: ScmCommandOptions): (...args: any[]) => any {
|
||||
const result = (...args: any[]) => {
|
||||
let result: Promise<any>;
|
||||
|
||||
@@ -44,6 +44,8 @@ export interface Stash {
|
||||
readonly index: number;
|
||||
readonly description: string;
|
||||
readonly branchName?: string;
|
||||
readonly authorDate?: Date;
|
||||
readonly commitDate?: Date;
|
||||
}
|
||||
|
||||
interface MutableRemote extends Remote {
|
||||
@@ -370,7 +372,7 @@ function sanitizeRelativePath(path: string): string {
|
||||
}
|
||||
|
||||
const COMMIT_FORMAT = '%H%n%aN%n%aE%n%at%n%ct%n%P%n%D%n%B';
|
||||
const STASH_FORMAT = '%H%n%P%n%gd%n%gs';
|
||||
const STASH_FORMAT = '%H%n%P%n%gd%n%gs%n%at%n%ct';
|
||||
|
||||
export interface ICloneOptions {
|
||||
readonly parentPath: string;
|
||||
@@ -999,12 +1001,12 @@ export function parseLsFiles(raw: string): LsFilesElement[] {
|
||||
.map(([, mode, object, stage, file]) => ({ mode, object, stage, file }));
|
||||
}
|
||||
|
||||
const stashRegex = /([0-9a-f]{40})\n(.*)\nstash@{(\d+)}\n(WIP\s)*on([^:]+):(.*)(?:\x00)/gmi;
|
||||
const stashRegex = /([0-9a-f]{40})\n(.*)\nstash@{(\d+)}\n(WIP\s)?on\s([^:]+):\s(.*)\n(\d+)\n(\d+)(?:\x00)/gmi;
|
||||
|
||||
function parseGitStashes(raw: string): Stash[] {
|
||||
const result: Stash[] = [];
|
||||
|
||||
let match, hash, parents, index, wip, branchName, description;
|
||||
let match, hash, parents, index, wip, branchName, description, authorDate, commitDate;
|
||||
|
||||
do {
|
||||
match = stashRegex.exec(raw);
|
||||
@@ -1012,13 +1014,15 @@ function parseGitStashes(raw: string): Stash[] {
|
||||
break;
|
||||
}
|
||||
|
||||
[, hash, parents, index, wip, branchName, description] = match;
|
||||
[, hash, parents, index, wip, branchName, description, authorDate, commitDate] = match;
|
||||
result.push({
|
||||
hash,
|
||||
parents: parents.split(' '),
|
||||
index: parseInt(index),
|
||||
branchName: branchName.trim(),
|
||||
description: wip ? `WIP (${description.trim()})` : description.trim()
|
||||
description: wip ? `WIP (${description.trim()})` : description.trim(),
|
||||
authorDate: authorDate ? new Date(Number(authorDate) * 1000) : undefined,
|
||||
commitDate: commitDate ? new Date(Number(commitDate) * 1000) : undefined,
|
||||
});
|
||||
} while (true);
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@ export const Operation = {
|
||||
Show: { kind: OperationKind.Show, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as ShowOperation,
|
||||
Stage: { kind: OperationKind.Stage, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as StageOperation,
|
||||
Status: { kind: OperationKind.Status, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as StatusOperation,
|
||||
Stash: { kind: OperationKind.Stash, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as StashOperation,
|
||||
Stash: (readOnly: boolean) => ({ kind: OperationKind.Stash, blocking: false, readOnly, remote: false, retry: false, showProgress: true } as StashOperation),
|
||||
SubmoduleUpdate: { kind: OperationKind.SubmoduleUpdate, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as SubmoduleUpdateOperation,
|
||||
Sync: { kind: OperationKind.Sync, blocking: true, readOnly: false, remote: true, retry: true, showProgress: true } as SyncOperation,
|
||||
Tag: { kind: OperationKind.Tag, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as TagOperation,
|
||||
|
||||
@@ -2253,7 +2253,7 @@ export class Repository implements Disposable {
|
||||
}
|
||||
|
||||
async getStashes(): Promise<Stash[]> {
|
||||
return this.run(Operation.Stash, () => this.repository.getStashes());
|
||||
return this.run(Operation.Stash(true), () => this.repository.getStashes());
|
||||
}
|
||||
|
||||
async createStash(message?: string, includeUntracked?: boolean, staged?: boolean): Promise<void> {
|
||||
@@ -2262,26 +2262,26 @@ export class Repository implements Disposable {
|
||||
...!staged ? this.workingTreeGroup.resourceStates.map(r => r.resourceUri.fsPath) : [],
|
||||
...includeUntracked ? this.untrackedGroup.resourceStates.map(r => r.resourceUri.fsPath) : []];
|
||||
|
||||
return await this.run(Operation.Stash, async () => {
|
||||
return await this.run(Operation.Stash(false), async () => {
|
||||
await this.repository.createStash(message, includeUntracked, staged);
|
||||
this.closeDiffEditors(indexResources, workingGroupResources);
|
||||
});
|
||||
}
|
||||
|
||||
async popStash(index?: number, options?: { reinstateStagedChanges?: boolean }): Promise<void> {
|
||||
return await this.run(Operation.Stash, () => this.repository.popStash(index, options));
|
||||
return await this.run(Operation.Stash(false), () => this.repository.popStash(index, options));
|
||||
}
|
||||
|
||||
async dropStash(index?: number): Promise<void> {
|
||||
return await this.run(Operation.Stash, () => this.repository.dropStash(index));
|
||||
return await this.run(Operation.Stash(false), () => this.repository.dropStash(index));
|
||||
}
|
||||
|
||||
async applyStash(index?: number, options?: { reinstateStagedChanges?: boolean }): Promise<void> {
|
||||
return await this.run(Operation.Stash, () => this.repository.applyStash(index, options));
|
||||
return await this.run(Operation.Stash(false), () => this.repository.applyStash(index, options));
|
||||
}
|
||||
|
||||
async showStash(index: number): Promise<Change[] | undefined> {
|
||||
return await this.run(Operation.Stash, () => this.repository.showStash(index));
|
||||
return await this.run(Operation.Stash(true), () => this.repository.showStash(index));
|
||||
}
|
||||
|
||||
async getCommitTemplate(): Promise<string> {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { dirname, normalize, sep, relative } from 'path';
|
||||
import { Readable } from 'stream';
|
||||
import { promises as fs, createReadStream } from 'fs';
|
||||
import byline from 'byline';
|
||||
import { Stash } from './git';
|
||||
|
||||
export const isMacintosh = process.platform === 'darwin';
|
||||
export const isWindows = process.platform === 'win32';
|
||||
@@ -846,3 +847,19 @@ export function extractFilePathFromArgs(argv: string[], startIndex: number): str
|
||||
// leading quote and return the path as-is
|
||||
return path.slice(1);
|
||||
}
|
||||
|
||||
export function getStashDescription(stash: Stash): string | undefined {
|
||||
if (!stash.commitDate && !stash.branchName) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const descriptionSegments: string[] = [];
|
||||
if (stash.commitDate) {
|
||||
descriptionSegments.push(fromNow(stash.commitDate));
|
||||
}
|
||||
if (stash.branchName) {
|
||||
descriptionSegments.push(stash.branchName);
|
||||
}
|
||||
|
||||
return descriptionSegments.join(' \u2022 ');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user