Sfoglia il codice sorgente

Reworks git commands & calls them from views

Changes checkout in views to run via git commands
Changes fetch in views & palette to run via git commands
Changes pull in views & palette to run via git commands
Changes push in views & palette to run via git commands
Adds skip confirmation support for some git commands
Adds gitCommands.skipConfirmation setting
Adds force option to push git command
Adds local branch creation to remote branch checkout git command
Adds arbitrary ref support to checkout git command
main
Eric Amodio 5 anni fa
parent
commit
3a8d91c3ca
19 ha cambiato i file con 893 aggiunte e 411 eliminazioni
  1. +27
    -0
      package.json
  2. +205
    -93
      src/commands/gitCommands.ts
  3. +145
    -34
      src/commands/quick/checkout.ts
  4. +28
    -10
      src/commands/quick/cherry-pick.ts
  5. +93
    -55
      src/commands/quick/fetch.ts
  6. +0
    -88
      src/commands/quick/gitCommand.ts
  7. +14
    -8
      src/commands/quick/merge.ts
  8. +43
    -12
      src/commands/quick/pull.ts
  9. +85
    -33
      src/commands/quick/push.ts
  10. +74
    -15
      src/commands/quick/quickCommand.ts
  11. +85
    -0
      src/commands/quick/quickCommands.helpers.ts
  12. +14
    -8
      src/commands/quick/rebase.ts
  13. +17
    -6
      src/commands/repositories.ts
  14. +3
    -0
      src/config.ts
  15. +22
    -0
      src/git/git.ts
  16. +8
    -3
      src/git/gitService.ts
  17. +6
    -6
      src/git/models/repository.ts
  18. +23
    -40
      src/views/viewCommands.ts
  19. +1
    -0
      src/vsls/host.ts

+ 27
- 0
package.json Vedi File

@ -475,6 +475,33 @@
"markdownDescription": "Specifies the style of the gravatar default (fallback) images",
"scope": "window"
},
"gitlens.gitCommands.skipConfirmations": {
"type": "array",
"default": [
"checkout",
"fetch"
],
"items": {
"type": "string",
"enum": [
"checkout",
"fetch",
"pull",
"push"
],
"enumDescriptions": [
"Skips checkout command confirmation",
"Skips fetch command confirmation",
"Skips pull command confirmation",
"Skips push command confirmation"
]
},
"minItems": 0,
"maxItems": 4,
"uniqueItems": true,
"markdownDescription": "Specifies which Git commands should have their confirmations skipped when executed from a GitLens view",
"scope": "window"
},
"gitlens.heatmap.ageThreshold": {
"type": "string",
"default": "90",

+ 205
- 93
src/commands/gitCommands.ts Vedi File

@ -1,121 +1,187 @@
'use strict';
import { Disposable, QuickInputButtons, QuickPickItem, window } from 'vscode';
import { Disposable, InputBox, QuickInputButtons, QuickPick, QuickPickItem, window } from 'vscode';
import { command, Command, Commands } from './common';
import { log } from '../system';
import {
isQuickInputStep,
isQuickPickStep,
QuickCommandBase,
QuickInputStep,
QuickPickStep
} from './quick/quickCommand';
import { CommandArgs as CheckoutCommandArgs, CheckoutQuickCommand } from './quick/checkout';
import { CherryPickQuickCommand } from './quick/cherry-pick';
import { QuickCommandBase, QuickPickStep } from './quick/quickCommand';
import { FetchQuickCommand } from './quick/fetch';
import { CommandArgs as FetchCommandArgs, FetchQuickCommand } from './quick/fetch';
import { MergeQuickCommand } from './quick/merge';
import { PushQuickCommand } from './quick/push';
import { PullQuickCommand } from './quick/pull';
import { CheckoutQuickCommand } from './quick/checkout';
import { CommandArgs as PullCommandArgs, PullQuickCommand } from './quick/pull';
import { CommandArgs as PushCommandArgs, PushQuickCommand } from './quick/push';
import { RebaseQuickCommand } from './quick/rebase';
const sanitizeLabel = /\$\(.+?\)|\W/g;
@command()
export class GitCommandsCommand extends Command {
constructor() {
super(Commands.GitCommands);
}
export type GitCommandsCommandArgs = CheckoutCommandArgs | FetchCommandArgs | PullCommandArgs | PushCommandArgs;
@log({ args: false, correlate: true, singleLine: true, timed: false })
async execute() {
const commands: QuickCommandBase[] = [
new CheckoutQuickCommand(),
class PickCommandStep implements QuickPickStep {
readonly buttons = [];
readonly items: QuickCommandBase[];
readonly placeholder = 'Select command...';
readonly title = 'GitLens';
constructor(args?: GitCommandsCommandArgs) {
this.items = [
new CheckoutQuickCommand(args && args.command === 'checkout' ? args : undefined),
new CherryPickQuickCommand(),
new MergeQuickCommand(),
new FetchQuickCommand(),
new PullQuickCommand(),
new PushQuickCommand(),
new FetchQuickCommand(args && args.command === 'fetch' ? args : undefined),
new PullQuickCommand(args && args.command === 'pull' ? args : undefined),
new PushQuickCommand(args && args.command === 'push' ? args : undefined),
new RebaseQuickCommand()
];
}
const quickpick = window.createQuickPick();
quickpick.ignoreFocusOut = true;
private _active: QuickCommandBase | undefined;
get command(): QuickCommandBase | undefined {
return this._active;
}
set command(value: QuickCommandBase | undefined) {
if (this._active !== undefined) {
this._active.picked = false;
}
let inCommand: QuickCommandBase | undefined;
this._active = value;
function showCommand(command: QuickPickStep | undefined) {
if (command === undefined) {
const previousLabel = inCommand && inCommand.label;
inCommand = undefined;
if (this._active !== undefined) {
this._active.picked = true;
}
}
quickpick.buttons = [];
quickpick.title = 'GitLens';
quickpick.placeholder = 'Select command...';
quickpick.canSelectMany = false;
quickpick.items = commands;
find(commandName: string) {
const cmd = commandName.toLowerCase();
return this.items.find(c => c.label.replace(sanitizeLabel, '').toLowerCase() === cmd);
}
}
if (previousLabel) {
const active = quickpick.items.find(i => i.label === previousLabel);
if (active) {
quickpick.activeItems = [active];
}
}
}
else {
quickpick.buttons = command.buttons || [QuickInputButtons.Back];
quickpick.title = command.title;
quickpick.placeholder = command.placeholder;
quickpick.canSelectMany = Boolean(command.multiselect);
@command()
export class GitCommandsCommand extends Command {
constructor() {
super(Commands.GitCommands);
}
quickpick.items = command.items;
@log({ args: false, correlate: true, singleLine: true, timed: false })
async execute(args?: GitCommandsCommandArgs) {
const commandsStep = new PickCommandStep(args);
if (quickpick.canSelectMany) {
quickpick.selectedItems = command.selectedItems || quickpick.items.filter(i => i.picked);
quickpick.activeItems = quickpick.selectedItems;
}
else {
quickpick.activeItems = command.selectedItems || quickpick.items.filter(i => i.picked);
}
let step: QuickPickStep | QuickInputStep | undefined = commandsStep;
// // BUG: https://github.com/microsoft/vscode/issues/75046
// // If we can multiselect, then ensure the selectedItems gets reset (otherwise it could end up included the current selected items)
// if (quickpick.canSelectMany && quickpick.selectedItems.length !== 0) {
// quickpick.selectedItems = [];
// }
if (args) {
const command = commandsStep.find(args.command);
if (command !== undefined) {
commandsStep.command = command;
const next = await command.next();
if (next.done) return;
step = next.value;
}
}
async function next(command: QuickCommandBase, items: QuickPickItem[] | undefined) {
quickpick.busy = true;
// quickpick.enabled = false;
while (step !== undefined) {
if (isQuickPickStep(step)) {
step = await this.showPickStep(step, commandsStep);
continue;
}
const next = await command.next(items);
if (next.done) {
return false;
if (isQuickInputStep(step)) {
step = await this.showInputStep(step, commandsStep);
continue;
}
quickpick.value = '';
showCommand(next.value);
break;
}
}
private async showInputStep(step: QuickInputStep, commandsStep: PickCommandStep) {
const input = window.createInputBox();
input.ignoreFocusOut = true;
// quickpick.enabled = true;
quickpick.busy = false;
const disposables: Disposable[] = [];
return true;
try {
return await new Promise<QuickPickStep | QuickInputStep | undefined>(resolve => {
disposables.push(
input.onDidHide(() => resolve()),
input.onDidTriggerButton(async e => {
if (e === QuickInputButtons.Back) {
input.value = '';
if (commandsStep.command !== undefined) {
input.busy = true;
resolve((await commandsStep.command.previous()) || commandsStep);
}
return;
}
const step = commandsStep.command && commandsStep.command.value;
if (step === undefined || !isQuickInputStep(step) || step.onDidClickButton === undefined)
return;
step.onDidClickButton(input, e);
}),
input.onDidChangeValue(async e => {
if (step.validate === undefined) return;
const [, message] = await step.validate(e);
input.validationMessage = message;
}),
input.onDidAccept(async () => {
resolve(await this.nextStep(input, commandsStep.command!, input.value));
})
);
input.buttons = step.buttons || [QuickInputButtons.Back];
input.title = step.title;
input.placeholder = step.placeholder;
if (step.value !== undefined) {
input.value = step.value;
}
// If we are starting over clear the previously active command
if (commandsStep.command !== undefined && step === commandsStep) {
commandsStep.command = undefined;
}
input.show();
});
}
finally {
input.dispose();
disposables.forEach(d => d.dispose());
}
}
showCommand(undefined);
private async showPickStep(step: QuickPickStep, commandsStep: PickCommandStep) {
const quickpick = window.createQuickPick();
quickpick.ignoreFocusOut = true;
const disposables: Disposable[] = [];
try {
void (await new Promise<void>(resolve => {
return await new Promise<QuickPickStep | QuickInputStep | undefined>(resolve => {
disposables.push(
quickpick.onDidHide(() => resolve()),
quickpick.onDidTriggerButton(async e => {
if (e === QuickInputButtons.Back) {
quickpick.value = '';
if (inCommand !== undefined) {
showCommand(await inCommand.previous());
if (commandsStep.command !== undefined) {
quickpick.busy = true;
resolve((await commandsStep.command.previous()) || commandsStep);
}
return;
}
const step = inCommand && inCommand.value;
if (step === undefined || step.onDidClickButton === undefined) return;
const step = commandsStep.command && commandsStep.command.value;
if (step === undefined || !isQuickPickStep(step) || step.onDidClickButton === undefined) return;
step.onDidClickButton(quickpick, e);
}),
@ -133,21 +199,18 @@ export class GitCommandsCommand extends Command {
return;
}
const cmd = quickpick.value.toLowerCase().trim();
let items;
if (inCommand === undefined) {
const command = commands.find(
c => c.label.replace(sanitizeLabel, '').toLowerCase() === cmd
);
if (commandsStep.command === undefined) {
const command = commandsStep.find(quickpick.value.trim());
if (command === undefined) return;
inCommand = command;
commandsStep.command = command;
}
else {
const step = inCommand.value;
if (step === undefined) return;
const step = commandsStep.command.value;
if (step === undefined || !isQuickPickStep(step)) return;
const cmd = quickpick.value.trim().toLowerCase();
const item = step.items.find(
i => i.label.replace(sanitizeLabel, '').toLowerCase() === cmd
);
@ -156,40 +219,89 @@ export class GitCommandsCommand extends Command {
items = [item];
}
if (!(await next(inCommand, items))) {
resolve();
}
resolve(await this.nextStep(quickpick, commandsStep.command, items));
}
}),
quickpick.onDidAccept(async () => {
let items = quickpick.selectedItems;
if (items.length === 0) {
if (!quickpick.canSelectMany || quickpick.activeItems.length === 0) return;
if (!quickpick.canSelectMany || quickpick.activeItems.length === 0) {
const value = quickpick.value.trim();
if (value.length === 0) return;
const step = commandsStep.command && commandsStep.command.value;
if (step === undefined || !isQuickPickStep(step) || step.onDidAccept === undefined)
return;
quickpick.busy = true;
if (await step.onDidAccept(quickpick)) {
resolve(await this.nextStep(quickpick, commandsStep.command!, value));
}
quickpick.busy = false;
return;
}
items = quickpick.activeItems;
}
if (inCommand === undefined) {
if (commandsStep.command === undefined) {
const command = items[0];
if (!QuickCommandBase.is(command)) return;
inCommand = command;
commandsStep.command = command;
}
if (!(await next(inCommand, items as QuickPickItem[]))) {
resolve();
}
resolve(await this.nextStep(quickpick, commandsStep.command, items as QuickPickItem[]));
})
);
quickpick.show();
}));
quickpick.buttons = step.buttons || [QuickInputButtons.Back];
quickpick.title = step.title;
quickpick.placeholder = step.placeholder;
quickpick.canSelectMany = Boolean(step.multiselect);
quickpick.items = step.items;
if (quickpick.canSelectMany) {
quickpick.selectedItems = step.selectedItems || quickpick.items.filter(i => i.picked);
quickpick.activeItems = quickpick.selectedItems;
}
else {
quickpick.activeItems = step.selectedItems || quickpick.items.filter(i => i.picked);
}
// If we are starting over clear the previously active command
if (commandsStep.command !== undefined && step === commandsStep) {
commandsStep.command = undefined;
}
if (step.value !== undefined) {
quickpick.value = step.value;
}
quickpick.hide();
quickpick.show();
});
}
finally {
quickpick.dispose();
disposables.forEach(d => d.dispose());
}
}
private async nextStep(
quickInput: QuickPick<QuickPickItem> | InputBox,
command: QuickCommandBase,
value: QuickPickItem[] | string | undefined
) {
quickInput.busy = true;
// quickInput.enabled = false;
const next = await command.next(value);
if (next.done) return undefined;
quickInput.value = '';
return next.value;
}
}

+ 145
- 34
src/commands/quick/checkout.ts Vedi File

@ -2,21 +2,59 @@
/* eslint-disable no-loop-func */
import { ProgressLocation, QuickInputButtons, window } from 'vscode';
import { Container } from '../../container';
import { Repository } from '../../git/gitService';
import { GitBranch, GitReference, GitService, GitTag, Repository } from '../../git/gitService';
import { GlyphChars } from '../../constants';
import { GitCommandBase } from './gitCommand';
import { CommandAbortError, QuickPickStep } from './quickCommand';
import {
CommandAbortError,
getBranchesAndOrTags,
QuickCommandBase,
QuickInputStep,
QuickPickStep,
StepState
} from './quickCommand';
import { ReferencesQuickPickItem, RepositoryQuickPickItem } from '../../quickpicks';
import { Strings } from '../../system';
interface State {
repos: Repository[];
ref: string;
branchOrTagOrRef: GitBranch | GitTag | GitReference;
createBranch?: string;
}
export class CheckoutQuickCommand extends GitCommandBase {
constructor() {
export interface CommandArgs {
readonly command: 'checkout';
state?: Partial<State>;
skipConfirmation?: boolean;
}
export class CheckoutQuickCommand extends QuickCommandBase<State> {
constructor(args?: CommandArgs) {
super('checkout', 'Checkout');
if (args === undefined || args.state === undefined) return;
let counter = 0;
if (args.state.repos !== undefined && args.state.repos.length !== 0) {
counter++;
}
if (args.state.branchOrTagOrRef !== undefined) {
counter++;
}
if (
args.skipConfirmation === undefined &&
Container.config.gitCommands.skipConfirmations.includes(this.label)
) {
args.skipConfirmation = true;
}
this._initialState = {
counter: counter,
skipConfirmation: counter > 1 && args.skipConfirmation,
...args.state
};
}
async execute(state: State) {
@ -25,14 +63,19 @@ export class CheckoutQuickCommand extends GitCommandBase {
location: ProgressLocation.Notification,
title: `Checking out ${
state.repos.length === 1 ? state.repos[0].formattedName : `${state.repos.length} repositories`
} to ${state.ref}`
} to ${state.branchOrTagOrRef.ref}`
},
() => Promise.all(state.repos.map(r => r.checkout(state.ref, { progress: false })))
() =>
Promise.all(
state.repos.map(r =>
r.checkout(state.branchOrTagOrRef.ref, { createBranch: state.createBranch, progress: false })
)
)
));
}
async *steps(): AsyncIterableIterator<QuickPickStep> {
const state: Partial<State> & { counter: number } = { counter: 0 };
protected async *steps(): AsyncIterableIterator<QuickPickStep | QuickInputStep> {
const state: StepState<State> = this._initialState === undefined ? { counter: 0 } : this._initialState;
let oneRepo = false;
let showTags = false;
@ -47,7 +90,7 @@ export class CheckoutQuickCommand extends GitCommandBase {
state.repos = [repos[0]];
}
else {
const step = this.createStep<RepositoryQuickPickItem>({
const step = this.createPickStep<RepositoryQuickPickItem>({
multiselect: true,
title: this.title,
placeholder: 'Choose repositories',
@ -71,19 +114,27 @@ export class CheckoutQuickCommand extends GitCommandBase {
}
}
if (state.ref === undefined || state.counter < 2) {
if (state.branchOrTagOrRef === undefined || state.counter < 2) {
const includeTags = showTags || state.repos.length === 1;
const items = await this.getBranchesAndOrTags(state.repos, includeTags);
const step = this.createStep<ReferencesQuickPickItem>({
const items = await getBranchesAndOrTags(
state.repos,
includeTags,
state.repos.length === 1 ? undefined : { filterBranches: b => !b.remote }
);
const step = this.createPickStep<ReferencesQuickPickItem>({
title: `${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${
state.repos.length === 1
? state.repos[0].formattedName
: `${state.repos.length} repositories`
}`,
placeholder: `Choose a branch${includeTags ? ' or tag' : ''} to checkout to`,
placeholder: `Choose a branch${
includeTags ? ' or tag' : ''
} to checkout to${GlyphChars.Space.repeat(3)}(select or enter a reference)`,
items: items,
selectedItems: state.ref ? items.filter(ref => ref.label === state.ref) : undefined,
selectedItems: state.branchOrTagOrRef
? items.filter(ref => ref.label === state.branchOrTagOrRef!.ref)
: undefined,
buttons: includeTags
? [QuickInputButtons.Back]
: [
@ -104,13 +155,21 @@ export class CheckoutQuickCommand extends GitCommandBase {
showTags = true;
}
quickpick.placeholder = `Choose a branch${showTags ? ' or tag' : ''} to checkout to`;
quickpick.placeholder = `Choose a branch${
showTags ? ' or tag' : ''
} to checkout to${GlyphChars.Space.repeat(3)}(select or enter a reference)`;
quickpick.buttons = [QuickInputButtons.Back];
quickpick.items = await this.getBranchesAndOrTags(state.repos!, showTags);
quickpick.items = await getBranchesAndOrTags(state.repos!, showTags);
quickpick.busy = false;
quickpick.enabled = true;
},
onDidAccept: (quickpick): Promise<boolean> => {
const ref = quickpick.value.trim();
if (ref.length === 0 || state.repos!.length !== 1) return Promise.resolve(false);
return Container.git.validateReference(state.repos![0].path, ref);
}
});
const selection = yield step;
@ -123,29 +182,81 @@ export class CheckoutQuickCommand extends GitCommandBase {
continue;
}
state.ref = selection[0].item.ref;
state.branchOrTagOrRef =
typeof selection === 'string'
? { name: GitService.shortenSha(selection), ref: selection }
: selection[0].item;
}
const step = this.createConfirmStep(
`Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${
state.repos.length === 1 ? state.repos[0].formattedName : `${state.repos.length} repositories`
} to ${state.ref}`,
[
{
label: this.title,
description: `${state.ref}`,
detail: `Will checkout ${
if (state.branchOrTagOrRef instanceof GitBranch && state.branchOrTagOrRef.remote) {
const branches = await Container.git.getBranches(state.branchOrTagOrRef.repoPath, {
filter: b => {
return b.tracking === state.branchOrTagOrRef!.name;
}
});
if (branches.length === 0) {
const step = this.createInputStep({
title: `${this.title} new branch to ${state.branchOrTagOrRef.ref}${Strings.pad(
GlyphChars.Dot,
2,
2
)}${
state.repos.length === 1
? state.repos[0].formattedName
: `${state.repos.length} repositories`
} to ${state.ref}`
}`,
placeholder: 'Choose name for the local branch',
value: state.branchOrTagOrRef.getName(),
validate: async (value: string | undefined): Promise<[boolean, string | undefined]> => {
if (value == null) return [false, undefined];
value = value.trim();
if (value.length === 0) return [false, 'Please enter a valid branch name'];
const valid = Boolean(await Container.git.validateBranchName(value!));
return [valid, valid ? undefined : `'${value}' isn't a valid branch name`];
}
});
const value = yield step;
if (!(await this.canMoveNext(step, state, value))) {
continue;
}
]
);
const selection = yield step;
if (!this.canMoveNext(step, state, selection)) {
continue;
state.createBranch = value;
}
}
if (!state.skipConfirmation) {
const step = this.createConfirmStep(
`Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${
state.repos.length === 1
? state.repos[0].formattedName
: `${state.repos.length} repositories`
}`,
[
{
label: this.title,
description: `${state.createBranch ? `${state.createBranch} to ` : ''}${
state.branchOrTagOrRef.name
}`,
detail: `Will ${
state.createBranch ? `create ${state.createBranch} and` : ''
} checkout to ${state.branchOrTagOrRef.name} in ${
state.repos.length === 1
? state.repos[0].formattedName
: `${state.repos.length} repositories`
}`
}
]
);
const selection = yield step;
if (!this.canMoveNext(step, state, selection)) {
continue;
}
}
this.execute(state as State);

+ 28
- 10
src/commands/quick/cherry-pick.ts Vedi File

@ -4,8 +4,14 @@ import { Container } from '../../container';
import { GitBranch, GitLogCommit, Repository } from '../../git/gitService';
import { GlyphChars } from '../../constants';
import { Iterables, Strings } from '../../system';
import { GitCommandBase } from './gitCommand';
import { CommandAbortError, QuickPickStep } from './quickCommand';
import {
CommandAbortError,
getBranchesAndOrTags,
QuickCommandBase,
QuickInputStep,
QuickPickStep,
StepState
} from './quickCommand';
import { BranchQuickPickItem, CommitQuickPickItem, RepositoryQuickPickItem } from '../../quickpicks';
import { runGitCommandInTerminal } from '../../terminal';
@ -16,7 +22,7 @@ interface State {
commits: GitLogCommit[];
}
export class CherryPickQuickCommand extends GitCommandBase {
export class CherryPickQuickCommand extends QuickCommandBase<State> {
constructor() {
super('cherry-pick', 'Cherry Pick', { description: 'via Terminal' });
}
@ -27,8 +33,8 @@ export class CherryPickQuickCommand extends GitCommandBase {
runGitCommandInTerminal('cherry-pick', state.commits.map(c => c.sha).join(' '), state.repo.path, true);
}
async *steps(): AsyncIterableIterator<QuickPickStep> {
const state: Partial<State> & { counter: number } = { counter: 0 };
protected async *steps(): AsyncIterableIterator<QuickPickStep | QuickInputStep> {
const state: StepState<State> = this._initialState === undefined ? { counter: 0 } : this._initialState;
let oneRepo = false;
while (true) {
@ -44,7 +50,7 @@ export class CherryPickQuickCommand extends GitCommandBase {
else {
const active = state.repo ? state.repo : await Container.git.getActiveRepository();
const step = this.createStep<RepositoryQuickPickItem>({
const step = this.createPickStep<RepositoryQuickPickItem>({
title: this.title,
placeholder: 'Choose a repository',
items: await Promise.all(
@ -73,14 +79,20 @@ export class CherryPickQuickCommand extends GitCommandBase {
if (state.source === undefined || state.counter < 2) {
const destId = state.destination.id;
const step = this.createStep<BranchQuickPickItem>({
const step = this.createPickStep<BranchQuickPickItem>({
title: `${this.title} into ${state.destination.name}${Strings.pad(GlyphChars.Dot, 2, 2)}${
state.repo.name
}`,
placeholder: 'Choose a branch or tag to cherry-pick from',
items: await this.getBranchesAndOrTags(state.repo, true, {
items: await getBranchesAndOrTags(state.repo, true, {
filterBranches: b => b.id !== destId
})
// onDidAccept: (quickpick): Promise<boolean> => {
// const ref = quickpick.value.trim();
// if (ref.length === 0) return Promise.resolve(false);
// return Container.git.validateReference(state.repo!.path, ref);
// }
});
const selection = yield step;
@ -92,16 +104,22 @@ export class CherryPickQuickCommand extends GitCommandBase {
continue;
}
// TODO: Allow pasting in commit id
// if (typeof selection === 'string') {
// }
// else {
state.source = selection[0].item;
// }
}
if (state.commits === undefined || state.counter < 3) {
const log = await Container.git.getLog(state.source.repoPath, {
const log = await Container.git.getLog(state.repo.path, {
ref: `${state.destination.ref}..${state.source.ref}`,
merges: false
});
const step = this.createStep<CommitQuickPickItem>({
const step = this.createPickStep<CommitQuickPickItem>({
title: `${this.title} onto ${state.destination.name}${Strings.pad(GlyphChars.Dot, 2, 2)}${
state.repo.name
}`,

+ 93
- 55
src/commands/quick/fetch.ts Vedi File

@ -2,7 +2,7 @@
import { QuickPickItem } from 'vscode';
import { Container } from '../../container';
import { Repository } from '../../git/gitService';
import { CommandAbortError, QuickCommandBase, QuickPickStep } from './quickCommand';
import { CommandAbortError, QuickCommandBase, QuickInputStep, QuickPickStep, StepState } from './quickCommand';
import { RepositoryQuickPickItem } from '../../quickpicks';
import { Strings } from '../../system';
import { GlyphChars } from '../../constants';
@ -12,9 +12,36 @@ interface State {
flags: string[];
}
export class FetchQuickCommand extends QuickCommandBase {
constructor() {
export interface CommandArgs {
readonly command: 'fetch';
state?: Partial<State>;
skipConfirmation?: boolean;
}
export class FetchQuickCommand extends QuickCommandBase<State> {
constructor(args?: CommandArgs) {
super('fetch', 'Fetch');
if (args === undefined || args.state === undefined) return;
let counter = 0;
if (args.state.repos !== undefined && args.state.repos.length !== 0) {
counter++;
}
if (
args.skipConfirmation === undefined &&
Container.config.gitCommands.skipConfirmations.includes(this.label)
) {
args.skipConfirmation = true;
}
this._initialState = {
counter: counter,
skipConfirmation: counter > 0 && args.skipConfirmation,
...args.state
};
}
execute(state: State) {
@ -24,8 +51,8 @@ export class FetchQuickCommand extends QuickCommandBase {
});
}
async *steps(): AsyncIterableIterator<QuickPickStep> {
const state: Partial<State> & { counter: number } = { counter: 0 };
protected async *steps(): AsyncIterableIterator<QuickPickStep | QuickInputStep> {
const state: StepState<State> = this._initialState === undefined ? { counter: 0 } : this._initialState;
let oneRepo = false;
while (true) {
@ -39,17 +66,21 @@ export class FetchQuickCommand extends QuickCommandBase {
state.repos = [repos[0]];
}
else {
const step = this.createStep<RepositoryQuickPickItem>({
const step = this.createPickStep<RepositoryQuickPickItem>({
multiselect: true,
title: this.title,
placeholder: 'Choose repositories',
items: await Promise.all(
repos.map(r =>
RepositoryQuickPickItem.create(r, undefined, {
branch: true,
fetched: true,
status: true
})
repos.map(repo =>
RepositoryQuickPickItem.create(
repo,
state.repos ? state.repos.some(r => r.id === repo.id) : undefined,
{
branch: true,
fetched: true,
status: true
}
)
)
)
});
@ -63,55 +94,62 @@ export class FetchQuickCommand extends QuickCommandBase {
}
}
const step = this.createConfirmStep<QuickPickItem & { item: string[] }>(
`Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${
state.repos.length === 1 ? state.repos[0].formattedName : `${state.repos.length} repositories`
}`,
[
{
label: this.title,
description: '',
detail: `Will fetch ${
state.repos.length === 1
? state.repos[0].formattedName
: `${state.repos.length} repositories`
}`,
item: []
},
{
label: `${this.title} & Prune`,
description: '--prune',
detail: `Will fetch and prune ${
state.repos.length === 1
? state.repos[0].formattedName
: `${state.repos.length} repositories`
}`,
item: ['--prune']
},
{
label: `${this.title} All`,
description: '--all',
detail: `Will fetch all remotes of ${
state.repos.length === 1
? state.repos[0].formattedName
: `${state.repos.length} repositories`
}`,
item: ['--all']
if (state.skipConfirmation) {
state.flags = [];
}
else {
const step = this.createConfirmStep<QuickPickItem & { item: string[] }>(
`Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${
state.repos.length === 1
? state.repos[0].formattedName
: `${state.repos.length} repositories`
}`,
[
{
label: this.title,
description: '',
detail: `Will fetch ${
state.repos.length === 1
? state.repos[0].formattedName
: `${state.repos.length} repositories`
}`,
item: []
},
{
label: `${this.title} & Prune`,
description: '--prune',
detail: `Will fetch and prune ${
state.repos.length === 1
? state.repos[0].formattedName
: `${state.repos.length} repositories`
}`,
item: ['--prune']
},
{
label: `${this.title} All`,
description: '--all',
detail: `Will fetch all remotes of ${
state.repos.length === 1
? state.repos[0].formattedName
: `${state.repos.length} repositories`
}`,
item: ['--all']
}
]
);
const selection = yield step;
if (!this.canMoveNext(step, state, selection)) {
if (oneRepo) {
break;
}
]
);
const selection = yield step;
if (!this.canMoveNext(step, state, selection)) {
if (oneRepo) {
break;
continue;
}
continue;
state.flags = selection[0].item;
}
state.flags = selection[0].item;
this.execute(state as State);
break;
}

+ 0
- 88
src/commands/quick/gitCommand.ts Vedi File

@ -1,88 +0,0 @@
'use strict';
import { intersectionWith } from 'lodash';
import { QuickCommandBase } from './quickCommand';
import { GitBranch, GitTag, Repository } from '../../git/git';
import { BranchQuickPickItem, TagQuickPickItem } from '../../quickpicks';
export abstract class GitCommandBase extends QuickCommandBase {
protected async getBranchesAndOrTags(
repos: Repository | Repository[],
includeTags: boolean,
{
filterBranches,
filterTags,
picked
}: { filterBranches?: (b: GitBranch) => boolean; filterTags?: (t: GitTag) => boolean; picked?: string } = {}
) {
let branches: GitBranch[];
let tags: GitTag[] | undefined;
let singleRepo = false;
if (repos instanceof Repository || repos.length === 1) {
singleRepo = true;
const repo = repos instanceof Repository ? repos : repos[0];
[branches, tags] = await Promise.all<GitBranch[], GitTag[] | undefined>([
repo.getBranches({ filter: filterBranches, sort: true }),
includeTags ? repo.getTags({ filter: filterTags, includeRefs: true, sort: true }) : undefined
]);
}
else {
const [branchesByRepo, tagsByRepo] = await Promise.all<GitBranch[][], GitTag[][] | undefined>([
Promise.all(repos.map(r => r.getBranches({ filter: filterBranches, sort: true }))),
includeTags
? Promise.all(repos.map(r => r.getTags({ filter: filterTags, includeRefs: true, sort: true })))
: undefined
]);
branches = GitBranch.sort(
intersectionWith(...branchesByRepo, ((b1: GitBranch, b2: GitBranch) => b1.name === b2.name) as any)
);
if (includeTags) {
tags = GitTag.sort(
intersectionWith(...tagsByRepo!, ((t1: GitTag, t2: GitTag) => t1.name === t2.name) as any)
);
}
}
if (!includeTags) {
return Promise.all(
branches.map(b =>
BranchQuickPickItem.create(b, undefined, {
current: singleRepo ? 'checkmark' : false,
ref: singleRepo,
status: singleRepo,
type: 'remote'
})
)
);
}
return Promise.all<BranchQuickPickItem | TagQuickPickItem>([
...branches!
.filter(b => !b.remote)
.map(b =>
BranchQuickPickItem.create(b, picked != null && b.ref === picked, {
current: singleRepo ? 'checkmark' : false,
ref: singleRepo,
status: singleRepo
})
),
...tags!.map(t =>
TagQuickPickItem.create(t, picked != null && t.ref === picked, {
ref: singleRepo,
type: true
})
),
...branches!
.filter(b => b.remote)
.map(b =>
BranchQuickPickItem.create(b, picked != null && b.ref === picked, {
current: singleRepo ? 'checkmark' : false,
type: 'remote'
})
)
]);
}
}

+ 14
- 8
src/commands/quick/merge.ts Vedi File

@ -3,10 +3,16 @@ import { QuickPickItem } from 'vscode';
import { Container } from '../../container';
import { GitBranch, Repository } from '../../git/gitService';
import { GlyphChars } from '../../constants';
import { CommandAbortError, QuickPickStep } from './quickCommand';
import {
CommandAbortError,
getBranchesAndOrTags,
QuickCommandBase,
QuickInputStep,
QuickPickStep,
StepState
} from './quickCommand';
import { BranchQuickPickItem, RepositoryQuickPickItem } from '../../quickpicks';
import { Strings } from '../../system';
import { GitCommandBase } from './gitCommand';
import { runGitCommandInTerminal } from '../../terminal';
interface State {
@ -16,7 +22,7 @@ interface State {
flags: string[];
}
export class MergeQuickCommand extends GitCommandBase {
export class MergeQuickCommand extends QuickCommandBase<State> {
constructor() {
super('merge', 'Merge', { description: 'via Terminal' });
}
@ -25,8 +31,8 @@ export class MergeQuickCommand extends GitCommandBase {
runGitCommandInTerminal('merge', [...state.flags, state.source.ref].join(' '), state.repo.path, true);
}
async *steps(): AsyncIterableIterator<QuickPickStep> {
const state: Partial<State> & { counter: number } = { counter: 0 };
protected async *steps(): AsyncIterableIterator<QuickPickStep | QuickInputStep> {
const state: StepState<State> = this._initialState === undefined ? { counter: 0 } : this._initialState;
let oneRepo = false;
while (true) {
@ -42,7 +48,7 @@ export class MergeQuickCommand extends GitCommandBase {
else {
const active = state.repo ? state.repo : await Container.git.getActiveRepository();
const step = this.createStep<RepositoryQuickPickItem>({
const step = this.createPickStep<RepositoryQuickPickItem>({
title: this.title,
placeholder: 'Choose a repository',
items: await Promise.all(
@ -71,12 +77,12 @@ export class MergeQuickCommand extends GitCommandBase {
if (state.source === undefined || state.counter < 2) {
const destId = state.destination.id;
const step = this.createStep<BranchQuickPickItem>({
const step = this.createPickStep<BranchQuickPickItem>({
title: `${this.title} into ${state.destination.name}${Strings.pad(GlyphChars.Dot, 2, 2)}${
state.repo.name
}`,
placeholder: `Choose a branch or tag to merge into ${state.destination.name}`,
items: await this.getBranchesAndOrTags(state.repo, true, {
items: await getBranchesAndOrTags(state.repo, true, {
filterBranches: b => b.id !== destId,
picked: state.source && state.source.ref
})

+ 43
- 12
src/commands/quick/pull.ts Vedi File

@ -2,7 +2,7 @@
import { QuickPickItem } from 'vscode';
import { Container } from '../../container';
import { Repository } from '../../git/gitService';
import { CommandAbortError, QuickCommandBase, QuickPickStep } from './quickCommand';
import { CommandAbortError, QuickCommandBase, QuickInputStep, QuickPickStep, StepState } from './quickCommand';
import { RepositoryQuickPickItem } from '../../quickpicks';
import { Strings } from '../../system';
import { GlyphChars } from '../../constants';
@ -12,17 +12,44 @@ interface State {
flags: string[];
}
export class PullQuickCommand extends QuickCommandBase {
constructor() {
export interface CommandArgs {
readonly command: 'pull';
state?: Partial<State>;
skipConfirmation?: boolean;
}
export class PullQuickCommand extends QuickCommandBase<State> {
constructor(args?: CommandArgs) {
super('pull', 'Pull');
if (args === undefined || args.state === undefined) return;
let counter = 0;
if (args.state.repos !== undefined && args.state.repos.length !== 0) {
counter++;
}
if (
args.skipConfirmation === undefined &&
Container.config.gitCommands.skipConfirmations.includes(this.label)
) {
args.skipConfirmation = true;
}
this._initialState = {
counter: counter,
skipConfirmation: counter > 0 && args.skipConfirmation,
...args.state
};
}
execute(state: State) {
return Container.git.pullAll(state.repos, { rebase: state.flags.includes('--rebase') });
}
async *steps(): AsyncIterableIterator<QuickPickStep> {
const state: Partial<State> & { counter: number } = { counter: 0 };
protected async *steps(): AsyncIterableIterator<QuickPickStep | QuickInputStep> {
const state: StepState<State> = this._initialState === undefined ? { counter: 0 } : this._initialState;
let oneRepo = false;
while (true) {
@ -36,17 +63,21 @@ export class PullQuickCommand extends QuickCommandBase {
state.repos = [repos[0]];
}
else {
const step = this.createStep<RepositoryQuickPickItem>({
const step = this.createPickStep<RepositoryQuickPickItem>({
multiselect: true,
title: this.title,
placeholder: 'Choose repositories',
items: await Promise.all(
repos.map(r =>
RepositoryQuickPickItem.create(r, undefined, {
branch: true,
fetched: true,
status: true
})
repos.map(repo =>
RepositoryQuickPickItem.create(
repo,
state.repos ? state.repos.some(r => r.id === repo.id) : undefined,
{
branch: true,
fetched: true,
status: true
}
)
)
)
});

+ 85
- 33
src/commands/quick/push.ts Vedi File

@ -1,26 +1,54 @@
'use strict';
import { Container } from '../../container';
import { Repository } from '../../git/gitService';
import { CommandAbortError, QuickCommandBase, QuickPickStep } from './quickCommand';
import { CommandAbortError, QuickCommandBase, QuickInputStep, QuickPickStep, StepState } from './quickCommand';
import { RepositoryQuickPickItem } from '../../quickpicks';
import { Strings } from '../../system';
import { GlyphChars } from '../../constants';
interface State {
repos: Repository[];
flags: string[];
}
export class PushQuickCommand extends QuickCommandBase {
constructor() {
export interface CommandArgs {
readonly command: 'push';
state?: Partial<State>;
skipConfirmation?: boolean;
}
export class PushQuickCommand extends QuickCommandBase<State> {
constructor(args?: CommandArgs) {
super('push', 'Push');
if (args === undefined || args.state === undefined) return;
let counter = 0;
if (args.state.repos !== undefined && args.state.repos.length !== 0) {
counter++;
}
if (
args.skipConfirmation === undefined &&
Container.config.gitCommands.skipConfirmations.includes(this.label)
) {
args.skipConfirmation = true;
}
this._initialState = {
counter: counter,
skipConfirmation: counter > 0 && args.skipConfirmation,
...args.state
};
}
execute(state: State) {
return Container.git.pushAll(state.repos);
return Container.git.pushAll(state.repos, { force: state.flags.includes('--force') });
}
async *steps(): AsyncIterableIterator<QuickPickStep> {
const state: Partial<State> & { counter: number } = { counter: 0 };
protected async *steps(): AsyncIterableIterator<QuickPickStep | QuickInputStep> {
const state: StepState<State> = this._initialState === undefined ? { counter: 0 } : this._initialState;
let oneRepo = false;
while (true) {
@ -34,17 +62,21 @@ export class PushQuickCommand extends QuickCommandBase {
state.repos = [repos[0]];
}
else {
const step = this.createStep<RepositoryQuickPickItem>({
const step = this.createPickStep<RepositoryQuickPickItem>({
multiselect: true,
title: this.title,
placeholder: 'Choose repositories',
items: await Promise.all(
repos.map(r =>
RepositoryQuickPickItem.create(r, undefined, {
branch: true,
fetched: true,
status: true
})
repos.map(repo =>
RepositoryQuickPickItem.create(
repo,
state.repos ? state.repos.some(r => r.id === repo.id) : undefined,
{
branch: true,
fetched: true,
status: true
}
)
)
)
});
@ -58,30 +90,50 @@ export class PushQuickCommand extends QuickCommandBase {
}
}
const step = this.createConfirmStep(
`Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${
state.repos.length === 1 ? state.repos[0].formattedName : `${state.repos.length} repositories`
}`,
[
{
label: this.title,
description: '',
detail: `Will push ${
state.repos.length === 1
? state.repos[0].formattedName
: `${state.repos.length} repositories`
}`
if (state.skipConfirmation) {
state.flags = [];
}
else {
const step = this.createConfirmStep(
`Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${
state.repos.length === 1
? state.repos[0].formattedName
: `${state.repos.length} repositories`
}`,
[
{
label: this.title,
description: '',
detail: `Will push ${
state.repos.length === 1
? state.repos[0].formattedName
: `${state.repos.length} repositories`
}`,
item: []
},
{
label: `Force ${this.title}`,
description: '',
detail: `Will force push ${
state.repos.length === 1
? state.repos[0].formattedName
: `${state.repos.length} repositories`
}`,
item: ['--force']
}
]
);
const selection = yield step;
if (!this.canMoveNext(step, state, selection)) {
if (oneRepo) {
break;
}
]
);
const selection = yield step;
if (!this.canMoveNext(step, state, selection)) {
if (oneRepo) {
break;
continue;
}
continue;
state.flags = selection[0].item;
}
this.execute(state as State);

+ 74
- 15
src/commands/quick/quickCommand.ts Vedi File

@ -1,5 +1,22 @@
'use strict';
import { QuickInputButton, QuickPick, QuickPickItem } from 'vscode';
import { InputBox, QuickInputButton, QuickPick, QuickPickItem } from 'vscode';
import { Promises } from '../../system/promise';
export * from './quickCommands.helpers';
export interface QuickInputStep {
buttons?: QuickInputButton[];
placeholder?: string;
title?: string;
value?: string;
onDidClickButton?(input: InputBox, button: QuickInputButton): void;
validate?(value: string | undefined): [boolean, string | undefined] | Promise<[boolean, string | undefined]>;
}
export function isQuickInputStep(item: QuickPickStep | QuickInputStep): item is QuickInputStep {
return (item as QuickPickStep).items === undefined;
}
export interface QuickPickStep<T extends QuickPickItem = any> {
buttons?: QuickInputButton[];
@ -8,9 +25,15 @@ export interface QuickPickStep {
multiselect?: boolean;
placeholder?: string;
title?: string;
value?: string;
onDidAccept?(quickpick: QuickPick<T>): Promise<boolean>;
onDidClickButton?(quickpick: QuickPick<T>, button: QuickInputButton): void;
validate?(selection: T[]): boolean;
validate?(selection: T[]): boolean | Promise<boolean>;
}
export function isQuickPickStep(item: QuickPickStep | QuickInputStep): item is QuickPickStep {
return (item as QuickPickStep).items !== undefined;
}
export class CommandAbortError extends Error {
@ -19,7 +42,9 @@ export class CommandAbortError extends Error {
}
}
export abstract class QuickCommandBase implements QuickPickItem {
export type StepState<T> = Partial<T> & { counter: number; skipConfirmation?: boolean };
export abstract class QuickCommandBase<T = any> implements QuickPickItem {
static is(item: QuickPickItem): item is QuickCommandBase {
return item instanceof QuickCommandBase;
}
@ -27,8 +52,8 @@ export abstract class QuickCommandBase implements QuickPickItem {
readonly description?: string;
readonly detail?: string;
private _current: QuickPickStep | undefined;
private _stepsIterator: AsyncIterableIterator<QuickPickStep> | undefined;
private _current: QuickPickStep | QuickInputStep | undefined;
private _stepsIterator: AsyncIterableIterator<QuickPickStep | QuickInputStep> | undefined;
constructor(
public readonly label: string,
@ -42,29 +67,40 @@ export abstract class QuickCommandBase implements QuickPickItem {
this.detail = options.detail;
}
abstract steps(): AsyncIterableIterator<QuickPickStep>;
private _picked: boolean = false;
get picked() {
return this._picked;
}
set picked(value: boolean) {
this._picked = value;
}
protected _initialState?: StepState<T>;
async previous(): Promise<QuickPickStep | undefined> {
protected abstract steps(): AsyncIterableIterator<QuickPickStep | QuickInputStep>;
async previous(): Promise<QuickPickStep | QuickInputStep | undefined> {
// Simulate going back, by having no selection
return (await this.next([])).value;
}
async next(selection?: QuickPickItem[]): Promise<IteratorResult<QuickPickStep>> {
async next(value?: QuickPickItem[] | string): Promise<IteratorResult<QuickPickStep | QuickInputStep>> {
if (this._stepsIterator === undefined) {
this._stepsIterator = this.steps();
}
const result = await this._stepsIterator.next(selection);
const result = await this._stepsIterator.next(value);
this._current = result.value;
if (result.done) {
this._initialState = undefined;
this._stepsIterator = undefined;
}
return result;
}
get value(): QuickPickStep | undefined {
get value(): QuickPickStep | QuickInputStep | undefined {
return this._current;
}
@ -73,7 +109,7 @@ export abstract class QuickCommandBase implements QuickPickItem {
confirmations: T[],
cancellable: boolean = true
): QuickPickStep<T> {
return this.createStep<T>({
return this.createPickStep<T>({
placeholder: `Confirm ${this.title}`,
title: title,
items: cancellable ? [...confirmations, { label: 'Cancel' }] : confirmations,
@ -86,7 +122,11 @@ export abstract class QuickCommandBase implements QuickPickItem {
});
}
protected createStep<T extends QuickPickItem>(step: QuickPickStep<T>): QuickPickStep<T> {
protected createInputStep(step: QuickInputStep): QuickInputStep {
return step;
}
protected createPickStep<T extends QuickPickItem>(step: QuickPickStep<T>): QuickPickStep<T> {
return step;
}
@ -94,8 +134,18 @@ export abstract class QuickCommandBase implements QuickPickItem {
step: QuickPickStep<T>,
state: { counter: number },
selection: T[] | undefined
): selection is T[] {
if (selection === undefined || selection.length === 0) {
): selection is T[];
protected canMoveNext<T extends string>(
step: QuickInputStep,
state: { counter: number },
value: string | undefined
): boolean | Promise<boolean>;
protected canMoveNext<T extends any>(
step: QuickPickStep | QuickInputStep,
state: { counter: number },
value: T[] | string | undefined
) {
if (value === undefined || value.length === 0) {
state.counter--;
if (state.counter < 0) {
state.counter = 0;
@ -103,11 +153,20 @@ export abstract class QuickCommandBase implements QuickPickItem {
return false;
}
if (step.validate === undefined || step.validate(selection)) {
if (step.validate === undefined || (isQuickPickStep(step) && step.validate!(value as T[]))) {
state.counter++;
return true;
}
if (isQuickInputStep(step)) {
const result = step.validate!(value as string);
if (!Promises.isPromise(result)) {
return result[0];
}
return result.then(([valid]) => valid);
}
return false;
}
}

+ 85
- 0
src/commands/quick/quickCommands.helpers.ts Vedi File

@ -0,0 +1,85 @@
'use strict';
import { intersectionWith } from 'lodash-es';
import { GitBranch, GitTag, Repository } from '../../git/git';
import { BranchQuickPickItem, TagQuickPickItem } from '../../quickpicks';
export async function getBranchesAndOrTags(
repos: Repository | Repository[],
includeTags: boolean,
{
filterBranches,
filterTags,
picked
}: { filterBranches?: (b: GitBranch) => boolean; filterTags?: (t: GitTag) => boolean; picked?: string } = {}
) {
let branches: GitBranch[];
let tags: GitTag[] | undefined;
let singleRepo = false;
if (repos instanceof Repository || repos.length === 1) {
singleRepo = true;
const repo = repos instanceof Repository ? repos : repos[0];
[branches, tags] = await Promise.all<GitBranch[], GitTag[] | undefined>([
repo.getBranches({ filter: filterBranches, sort: true }),
includeTags ? repo.getTags({ filter: filterTags, includeRefs: true, sort: true }) : undefined
]);
}
else {
const [branchesByRepo, tagsByRepo] = await Promise.all<GitBranch[][], GitTag[][] | undefined>([
Promise.all(repos.map(r => r.getBranches({ filter: filterBranches, sort: true }))),
includeTags
? Promise.all(repos.map(r => r.getTags({ filter: filterTags, includeRefs: true, sort: true })))
: undefined
]);
branches = GitBranch.sort(
intersectionWith(...branchesByRepo, ((b1: GitBranch, b2: GitBranch) => b1.name === b2.name) as any)
);
if (includeTags) {
tags = GitTag.sort(
intersectionWith(...tagsByRepo!, ((t1: GitTag, t2: GitTag) => t1.name === t2.name) as any)
);
}
}
if (!includeTags) {
return Promise.all(
branches.map(b =>
BranchQuickPickItem.create(b, undefined, {
current: singleRepo ? 'checkmark' : false,
ref: singleRepo,
status: singleRepo,
type: 'remote'
})
)
);
}
return Promise.all<BranchQuickPickItem | TagQuickPickItem>([
...branches!
.filter(b => !b.remote)
.map(b =>
BranchQuickPickItem.create(b, picked != null && b.ref === picked, {
current: singleRepo ? 'checkmark' : false,
ref: singleRepo,
status: singleRepo
})
),
...tags!.map(t =>
TagQuickPickItem.create(t, picked != null && t.ref === picked, {
ref: singleRepo,
type: true
})
),
...branches!
.filter(b => b.remote)
.map(b =>
BranchQuickPickItem.create(b, picked != null && b.ref === picked, {
current: singleRepo ? 'checkmark' : false,
type: 'remote'
})
)
]);
}

+ 14
- 8
src/commands/quick/rebase.ts Vedi File

@ -3,10 +3,16 @@ import { QuickPickItem } from 'vscode';
import { Container } from '../../container';
import { GitBranch, Repository } from '../../git/gitService';
import { GlyphChars } from '../../constants';
import { CommandAbortError, QuickPickStep } from './quickCommand';
import {
CommandAbortError,
getBranchesAndOrTags,
QuickCommandBase,
QuickInputStep,
QuickPickStep,
StepState
} from './quickCommand';
import { BranchQuickPickItem, RepositoryQuickPickItem } from '../../quickpicks';
import { Strings } from '../../system';
import { GitCommandBase } from './gitCommand';
import { runGitCommandInTerminal } from '../../terminal';
interface State {
@ -16,7 +22,7 @@ interface State {
flags: string[];
}
export class RebaseQuickCommand extends GitCommandBase {
export class RebaseQuickCommand extends QuickCommandBase<State> {
constructor() {
super('rebase', 'Rebase', { description: 'via Terminal' });
}
@ -25,8 +31,8 @@ export class RebaseQuickCommand extends GitCommandBase {
runGitCommandInTerminal('rebase', [...state.flags, state.source.ref].join(' '), state.repo.path, true);
}
async *steps(): AsyncIterableIterator<QuickPickStep> {
const state: Partial<State> & { counter: number } = { counter: 0 };
protected async *steps(): AsyncIterableIterator<QuickPickStep | QuickInputStep> {
const state: StepState<State> = this._initialState === undefined ? { counter: 0 } : this._initialState;
let oneRepo = false;
while (true) {
@ -42,7 +48,7 @@ export class RebaseQuickCommand extends GitCommandBase {
else {
const active = state.repo ? state.repo : await Container.git.getActiveRepository();
const step = this.createStep<RepositoryQuickPickItem>({
const step = this.createPickStep<RepositoryQuickPickItem>({
title: this.title,
placeholder: 'Choose a repository',
items: await Promise.all(
@ -71,12 +77,12 @@ export class RebaseQuickCommand extends GitCommandBase {
if (state.source === undefined || state.counter < 2) {
const destId = state.destination.id;
const step = this.createStep<BranchQuickPickItem>({
const step = this.createPickStep<BranchQuickPickItem>({
title: `${this.title} ${state.destination.name}${Strings.pad(GlyphChars.Dot, 2, 2)}${
state.repo.name
}`,
placeholder: `Choose a branch or tag to rebase ${state.destination.name} with`,
items: await this.getBranchesAndOrTags(state.repo, true, {
items: await getBranchesAndOrTags(state.repo, true, {
filterBranches: b => b.id !== destId,
picked: state.source && state.source.ref
})

+ 17
- 6
src/commands/repositories.ts Vedi File

@ -1,6 +1,8 @@
'use strict';
import { commands } from 'vscode';
import { Container } from '../container';
import { command, Command, Commands } from './common';
import { GitCommandsCommandArgs } from '../commands';
@command()
export class FetchRepositoriesCommand extends Command {
@ -8,8 +10,11 @@ export class FetchRepositoriesCommand extends Command {
super(Commands.FetchRepositories);
}
execute() {
return Container.git.fetchAll();
async execute() {
const repositories = await Container.git.getOrderedRepositories();
const args: GitCommandsCommandArgs = { command: 'fetch', state: { repos: repositories } };
return commands.executeCommand(Commands.GitCommands, args);
}
}
@ -19,8 +24,11 @@ export class PullRepositoriesCommand extends Command {
super(Commands.PullRepositories);
}
execute() {
return Container.git.pullAll();
async execute() {
const repositories = await Container.git.getOrderedRepositories();
const args: GitCommandsCommandArgs = { command: 'pull', state: { repos: repositories } };
return commands.executeCommand(Commands.GitCommands, args);
}
}
@ -30,7 +38,10 @@ export class PushRepositoriesCommand extends Command {
super(Commands.PushRepositories);
}
execute() {
return Container.git.pushAll();
async execute() {
const repositories = await Container.git.getOrderedRepositories();
const args: GitCommandsCommandArgs = { command: 'push', state: { repos: repositories } };
return commands.executeCommand(Commands.GitCommands, args);
}
}

+ 3
- 0
src/config.ts Vedi File

@ -32,6 +32,9 @@ export interface Config {
defaultDateSource: DateSource;
defaultDateStyle: DateStyle;
defaultGravatarsStyle: GravatarDefaultStyle;
gitCommands: {
skipConfirmations: string[];
};
heatmap: {
ageThreshold: number;
coldColor: string;

+ 22
- 0
src/git/git.ts Vedi File

@ -449,6 +449,28 @@ export class Git {
return git<string>({ cwd: repoPath, errors: GitErrorHandling.Ignore, local: true }, 'check-mailmap', author);
}
static async check_ref_format(ref: string, repoPath?: string, options: { branch?: boolean } = { branch: true }) {
const params = ['check-ref-format'];
if (options.branch) {
params.push('--branch');
}
else {
params.push('--normalize');
}
try {
const data = await git<string>(
{ cwd: repoPath || emptyStr, errors: GitErrorHandling.Throw, local: true },
...params,
ref
);
return data.trim();
}
catch {
return false;
}
}
static checkout(
repoPath: string,
ref: string,

+ 8
- 3
src/git/gitService.ts Vedi File

@ -595,14 +595,14 @@ export class GitService implements Disposable {
0: (repos?: Repository[]) => (repos === undefined ? false : repos.map(r => r.name).join(', '))
}
})
async pushAll(repositories?: Repository[]) {
async pushAll(repositories?: Repository[], options: { force?: boolean } = {}) {
if (repositories === undefined) {
repositories = await this.getOrderedRepositories();
}
if (repositories.length === 0) return;
if (repositories.length === 1) {
repositories[0].push();
repositories[0].push(options);
return;
}
@ -612,7 +612,7 @@ export class GitService implements Disposable {
location: ProgressLocation.Notification,
title: `Pushing ${repositories.length} repositories`
},
() => Promise.all(repositories!.map(r => r.push({ progress: false })))
() => Promise.all(repositories!.map(r => r.push({ progress: false, ...options })))
);
}
@ -2683,6 +2683,11 @@ export class GitService implements Disposable {
}
@log()
validateBranchName(ref: string, repoPath?: string) {
return Git.check_ref_format(ref, repoPath);
}
@log()
validateReference(repoPath: string, ref: string) {
return Git.cat_file__validate(repoPath, ref);
}

+ 6
- 6
src/git/models/repository.ts Vedi File

@ -237,9 +237,9 @@ export class Repository implements Disposable {
@gate()
@log()
async checkout(ref: string, options: { progress?: boolean } = {}) {
const { progress } = { progress: true, ...options };
if (!progress) return this.checkoutCore(ref);
async checkout(ref: string, options: { createBranch?: string | undefined; progress?: boolean } = {}) {
const { progress, ...opts } = { progress: true, ...options };
if (!progress) return this.checkoutCore(ref, opts);
return void (await window.withProgress(
{
@ -247,13 +247,13 @@ export class Repository implements Disposable {
title: `Checking out ${this.formattedName} to ${ref}...`,
cancellable: false
},
() => this.checkoutCore(ref)
() => this.checkoutCore(ref, opts)
));
}
private async checkoutCore(ref: string, options: { remote?: string } = {}) {
private async checkoutCore(ref: string, options: { createBranch?: string } = {}) {
try {
void (await Container.git.checkout(this.path, ref));
void (await Container.git.checkout(this.path, ref, options));
this.fireChange(RepositoryChange.Repository);
}

+ 23
- 40
src/views/viewCommands.ts Vedi File

@ -7,6 +7,7 @@ import {
DiffWithCommandArgsRevision,
DiffWithPreviousCommandArgs,
DiffWithWorkingCommandArgs,
GitCommandsCommandArgs,
openEditor,
OpenFileInRemoteCommandArgs,
OpenFileRevisionCommandArgs,
@ -45,7 +46,6 @@ import {
} from './nodes';
import { Strings } from '../system/string';
import { runGitCommandInTerminal } from '../terminal';
import { ReferencesQuickPick } from '../quickpicks';
interface CompareSelectedInfo {
ref: string;
@ -199,7 +199,9 @@ export class ViewCommands {
private fetch(node: RemoteNode | RepositoryNode) {
if (node instanceof RemoteNode) return node.fetch();
if (node instanceof RepositoryNode) return node.fetch();
if (node instanceof RepositoryNode) {
return commands.executeCommand(Commands.GitCommands, { command: 'fetch', state: { repos: [node.repo] } });
}
return undefined;
}
@ -210,7 +212,7 @@ export class ViewCommands {
}
if (!(node instanceof RepositoryNode)) return undefined;
return node.pull();
return commands.executeCommand(Commands.GitCommands, { command: 'pull', state: { repos: [node.repo] } });
}
private push(node: RepositoryNode | BranchTrackingStatusNode, force?: boolean) {
@ -219,7 +221,7 @@ export class ViewCommands {
}
if (!(node instanceof RepositoryNode)) return undefined;
return node.push({ force: force });
return commands.executeCommand(Commands.GitCommands, { command: 'push', state: { repos: [node.repo] } });
}
private async applyChanges(node: ViewRefFileNode) {
@ -245,45 +247,26 @@ export class ViewCommands {
return Container.git.checkout(node.repoPath, node.ref, { fileName: node.fileName });
}
if (node instanceof BranchNode) {
let branch = node.branch;
if (branch.current) {
const pick = await new ReferencesQuickPick(node.repoPath).show('Choose a branch to check out to', {
checkmarks: false,
filterBranches: b => !b.current,
include: 'branches'
});
if (pick === undefined) return undefined;
branch = pick.item;
}
if (branch.remote) {
const branches = await Container.git.getBranches(node.repoPath, {
filter: b => {
return b.tracking === branch.name;
}
});
const repo = await Container.git.getRepository(node.repoPath);
if (branches.length !== 0) {
return Container.git.checkout(node.repoPath, branches[0].ref);
}
const name = await window.showInputBox({
prompt: 'Please provide a name for the local branch',
placeHolder: 'Local branch name',
value: branch.getName(),
ignoreFocusOut: true
});
if (name === undefined || name.length === 0) return undefined;
return Container.git.checkout(node.repoPath, branch.ref, { createBranch: name });
}
return Container.git.checkout(branch.repoPath, branch.ref);
let args: GitCommandsCommandArgs;
if (node instanceof BranchNode) {
args = {
command: 'checkout',
state: { repos: [repo!], branchOrTagOrRef: node.branch.current ? undefined : node.branch }
};
}
else if (node instanceof TagNode) {
args = { command: 'checkout', state: { repos: [repo!], branchOrTagOrRef: node.tag } };
}
else {
args = {
command: 'checkout',
state: { repos: [repo!], branchOrTagOrRef: { name: node.ref, ref: node.ref } }
};
}
return Container.git.checkout(node.repoPath, node.ref);
return commands.executeCommand(Commands.GitCommands, args);
}
private async addRemote(node: RemoteNode) {

+ 1
- 0
src/vsls/host.ts Vedi File

@ -23,6 +23,7 @@ const gitWhitelist = new Map boolean>([
['branch', args => args[1] === '--contains'],
['cat-file', defaultWhitelistFn],
['check-mailmap', defaultWhitelistFn],
['check-ref-format', defaultWhitelistFn],
['config', args => args[1] === '--get' || args[1] === '--get-regex'],
['diff', defaultWhitelistFn],
['difftool', defaultWhitelistFn],

Caricamento…
Annulla
Salva