Переглянути джерело

Adds quick git commands

main
Eric Amodio 5 роки тому
джерело
коміт
bf48a26c1d
19 змінених файлів з 1508 додано та 23 видалено
  1. +9
    -0
      package.json
  2. +1
    -0
      src/commands.ts
  3. +1
    -0
      src/commands/common.ts
  4. +195
    -0
      src/commands/gitCommands.ts
  5. +161
    -0
      src/commands/quick/checkout.ts
  6. +169
    -0
      src/commands/quick/cherry-pick.ts
  7. +125
    -0
      src/commands/quick/fetch.ts
  8. +88
    -0
      src/commands/quick/gitCommand.ts
  9. +165
    -0
      src/commands/quick/merge.ts
  10. +112
    -0
      src/commands/quick/pull.ts
  11. +97
    -0
      src/commands/quick/push.ts
  12. +113
    -0
      src/commands/quick/quickCommand.ts
  13. +157
    -0
      src/commands/quick/rebase.ts
  14. +27
    -4
      src/git/git.ts
  15. +31
    -9
      src/git/gitService.ts
  16. +47
    -8
      src/git/models/repository.ts
  17. +7
    -0
      src/system/string.ts
  18. +2
    -2
      src/views/nodes/repositoryNode.ts
  19. +1
    -0
      src/vsls/host.ts

+ 9
- 0
package.json Переглянути файл

@ -2084,6 +2084,11 @@
"category": "GitLens"
},
{
"command": "gitlens.gitCommands",
"title": "Git Commands",
"category": "GitLens"
},
{
"command": "gitlens.switchMode",
"title": "Switch Mode",
"category": "GitLens"
@ -3182,6 +3187,10 @@
"when": "gitlens:enabled && gitlens:canToggleCodeLens"
},
{
"command": "gitlens.gitCommands",
"when": "gitlens:enabled && !gitlens:readonly"
},
{
"command": "gitlens.switchMode",
"when": "gitlens:enabled"
},

+ 1
- 0
src/commands.ts Переглянути файл

@ -28,6 +28,7 @@ export * from './commands/openFileRevisionFrom';
export * from './commands/openInRemote';
export * from './commands/openRepoInRemote';
export * from './commands/openWorkingFile';
export * from './commands/gitCommands';
export * from './commands/repositories';
export * from './commands/resetSuppressedWarnings';
export * from './commands/searchCommits';

+ 1
- 0
src/commands/common.ts Переглянути файл

@ -68,6 +68,7 @@ export enum Commands {
OpenWorkingFile = 'gitlens.openWorkingFile',
PullRepositories = 'gitlens.pullRepositories',
PushRepositories = 'gitlens.pushRepositories',
GitCommands = 'gitlens.gitCommands',
ResetSuppressedWarnings = 'gitlens.resetSuppressedWarnings',
ShowCommitInView = 'gitlens.showCommitInView',
SearchCommits = 'gitlens.showCommitSearch',

+ 195
- 0
src/commands/gitCommands.ts Переглянути файл

@ -0,0 +1,195 @@
'use strict';
import { Disposable, QuickInputButtons, QuickPickItem, window } from 'vscode';
import { command, Command, Commands } from './common';
import { log } from '../system';
import { CherryPickQuickCommand } from './quick/cherry-pick';
import { QuickCommandBase, QuickPickStep } from './quick/quickCommand';
import { 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 { RebaseQuickCommand } from './quick/rebase';
const sanitizeLabel = /\$\(.+?\)|\W/g;
@command()
export class GitCommandsCommand extends Command {
constructor() {
super(Commands.GitCommands);
}
@log({ args: false, correlate: true, singleLine: true, timed: false })
async execute() {
const commands: QuickCommandBase[] = [
new CheckoutQuickCommand(),
new CherryPickQuickCommand(),
new MergeQuickCommand(),
new FetchQuickCommand(),
new PullQuickCommand(),
new PushQuickCommand(),
new RebaseQuickCommand()
];
const quickpick = window.createQuickPick();
quickpick.ignoreFocusOut = true;
let inCommand: QuickCommandBase | undefined;
function showCommand(command: QuickPickStep | undefined) {
if (command === undefined) {
const previousLabel = inCommand && inCommand.label;
inCommand = undefined;
quickpick.buttons = [];
quickpick.title = 'GitLens';
quickpick.placeholder = 'Select command...';
quickpick.canSelectMany = false;
quickpick.items = commands;
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);
quickpick.items = command.items;
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);
}
// // 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 = [];
// }
}
}
async function next(command: QuickCommandBase, items: QuickPickItem[] | undefined) {
quickpick.busy = true;
// quickpick.enabled = false;
const next = await command.next(items);
if (next.done) {
return false;
}
quickpick.value = '';
showCommand(next.value);
// quickpick.enabled = true;
quickpick.busy = false;
return true;
}
showCommand(undefined);
const disposables: Disposable[] = [];
try {
void (await new Promise<void>(resolve => {
disposables.push(
quickpick.onDidHide(() => resolve()),
quickpick.onDidTriggerButton(async e => {
if (e === QuickInputButtons.Back) {
quickpick.value = '';
if (inCommand !== undefined) {
showCommand(await inCommand.previous());
}
return;
}
const step = inCommand && inCommand.value;
if (step === undefined || step.onDidClickButton === undefined) return;
step.onDidClickButton(quickpick, e);
}),
quickpick.onDidChangeValue(async e => {
if (quickpick.canSelectMany && e === ' ') {
quickpick.value = '';
quickpick.selectedItems =
quickpick.selectedItems.length === quickpick.items.length ? [] : quickpick.items;
return;
}
if (e.endsWith(' ')) {
if (quickpick.canSelectMany && quickpick.selectedItems.length !== 0) {
return;
}
const cmd = quickpick.value.toLowerCase().trim();
let items;
if (inCommand === undefined) {
const command = commands.find(
c => c.label.replace(sanitizeLabel, '').toLowerCase() === cmd
);
if (command === undefined) return;
inCommand = command;
}
else {
const step = inCommand.value;
if (step === undefined) return;
const item = step.items.find(
i => i.label.replace(sanitizeLabel, '').toLowerCase() === cmd
);
if (item === undefined) return;
items = [item];
}
if (!(await next(inCommand, items))) {
resolve();
}
}
}),
quickpick.onDidAccept(async () => {
let items = quickpick.selectedItems;
if (items.length === 0) {
if (!quickpick.canSelectMany || quickpick.activeItems.length === 0) return;
items = quickpick.activeItems;
}
if (inCommand === undefined) {
const command = items[0];
if (!QuickCommandBase.is(command)) return;
inCommand = command;
}
if (!(await next(inCommand, items as QuickPickItem[]))) {
resolve();
}
})
);
quickpick.show();
}));
quickpick.hide();
}
finally {
quickpick.dispose();
disposables.forEach(d => d.dispose());
}
}
}

+ 161
- 0
src/commands/quick/checkout.ts Переглянути файл

@ -0,0 +1,161 @@
'use strict';
/* eslint-disable no-loop-func */
import { ProgressLocation, QuickInputButtons, window } from 'vscode';
import { Container } from '../../container';
import { Repository } from '../../git/gitService';
import { GlyphChars } from '../../constants';
import { GitCommandBase } from './gitCommand';
import { CommandAbortError, QuickPickStep } from './quickCommand';
import { ReferencesQuickPickItem, RepositoryQuickPickItem } from '../../quickpicks';
import { Strings } from '../../system';
interface State {
repos: Repository[];
ref: string;
}
export class CheckoutQuickCommand extends GitCommandBase {
constructor() {
super('checkout', 'Checkout');
}
async execute(state: State) {
return void (await window.withProgress(
{
location: ProgressLocation.Notification,
title: `Checking out ${
state.repos.length === 1 ? state.repos[0].formattedName : `${state.repos.length} repositories`
} to ${state.ref}`
},
() => Promise.all(state.repos.map(r => r.checkout(state.ref, { progress: false })))
));
}
async *steps(): AsyncIterableIterator<QuickPickStep> {
const state: Partial<State> & { counter: number } = { counter: 0 };
let oneRepo = false;
let showTags = false;
while (true) {
try {
if (state.repos === undefined || state.counter < 1) {
const repos = [...(await Container.git.getOrderedRepositories())];
if (repos.length === 1) {
oneRepo = true;
state.counter++;
state.repos = [repos[0]];
}
else {
const step = this.createStep<RepositoryQuickPickItem>({
multiselect: true,
title: this.title,
placeholder: 'Choose repositories',
items: await Promise.all(
repos.map(repo =>
RepositoryQuickPickItem.create(
repo,
state.repos ? state.repos.some(r => r.id === repo.id) : undefined,
{ branch: true, fetched: true, status: true }
)
)
)
});
const selection = yield step;
if (!this.canMoveNext(step, state, selection)) {
break;
}
state.repos = selection.map(i => i.item);
}
}
if (state.ref === undefined || state.counter < 2) {
const includeTags = showTags || state.repos.length === 1;
const items = await this.getBranchesAndOrTags(state.repos, includeTags);
const step = this.createStep<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`,
items: items,
selectedItems: state.ref ? items.filter(ref => ref.label === state.ref) : undefined,
buttons: includeTags
? [QuickInputButtons.Back]
: [
QuickInputButtons.Back,
{
iconPath: {
dark: Container.context.asAbsolutePath('images/dark/icon-tag.svg') as any,
light: Container.context.asAbsolutePath('images/light/icon-tag.svg') as any
},
tooltip: 'Show Tags'
}
],
onDidClickButton: async (quickpick, button) => {
quickpick.busy = true;
quickpick.enabled = false;
if (!showTags) {
showTags = true;
}
quickpick.placeholder = `Choose a branch${showTags ? ' or tag' : ''} to checkout to`;
quickpick.buttons = [QuickInputButtons.Back];
quickpick.items = await this.getBranchesAndOrTags(state.repos!, showTags);
quickpick.busy = false;
quickpick.enabled = true;
}
});
const selection = yield step;
if (!this.canMoveNext(step, state, selection)) {
if (oneRepo) {
break;
}
continue;
}
state.ref = selection[0].item.ref;
}
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 ${
state.repos.length === 1
? state.repos[0].formattedName
: `${state.repos.length} repositories`
} to ${state.ref}`
}
]
);
const selection = yield step;
if (!this.canMoveNext(step, state, selection)) {
continue;
}
this.execute(state as State);
break;
}
catch (ex) {
if (ex instanceof CommandAbortError) break;
throw ex;
}
}
}
}

+ 169
- 0
src/commands/quick/cherry-pick.ts Переглянути файл

@ -0,0 +1,169 @@
'use strict';
/* eslint-disable no-loop-func */
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 { BranchQuickPickItem, CommitQuickPickItem, RepositoryQuickPickItem } from '../../quickpicks';
import { runGitCommandInTerminal } from '../../terminal';
interface State {
repo: Repository;
destination: GitBranch;
source: GitBranch;
commits: GitLogCommit[];
}
export class CherryPickQuickCommand extends GitCommandBase {
constructor() {
super('cherry-pick', 'Cherry Pick');
}
execute(state: State) {
// Ensure the commits are ordered with the oldest first
state.commits.sort((a, b) => a.date.getTime() - b.date.getTime());
runGitCommandInTerminal('cherry-pick', state.commits.map(c => c.sha).join(' '), state.repo.path);
}
async *steps(): AsyncIterableIterator<QuickPickStep> {
const state: Partial<State> & { counter: number } = { counter: 0 };
let oneRepo = false;
while (true) {
try {
if (state.repo === undefined || state.counter < 1) {
const repos = [...(await Container.git.getOrderedRepositories())];
if (repos.length === 1) {
oneRepo = true;
state.counter++;
state.repo = repos[0];
}
else {
const active = state.repo ? state.repo : await Container.git.getActiveRepository();
const step = this.createStep<RepositoryQuickPickItem>({
title: this.title,
placeholder: 'Choose a repository',
items: await Promise.all(
repos.map(r =>
RepositoryQuickPickItem.create(r, r.id === (active && active.id), {
branch: true,
fetched: true,
status: true
})
)
)
});
const selection = yield step;
if (!this.canMoveNext(step, state, selection)) {
break;
}
state.repo = selection[0].item;
}
}
state.destination = await state.repo.getBranch();
if (state.destination === undefined) break;
if (state.source === undefined || state.counter < 2) {
const destId = state.destination.id;
const step = this.createStep<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, {
filterBranches: b => b.id !== destId
})
});
const selection = yield step;
if (!this.canMoveNext(step, state, selection)) {
if (oneRepo) {
break;
}
continue;
}
state.source = selection[0].item;
}
if (state.commits === undefined || state.counter < 3) {
const log = await Container.git.getLog(state.source.repoPath, {
ref: `${state.destination.ref}..${state.source.ref}`,
merges: false
});
const step = this.createStep<CommitQuickPickItem>({
title: `${this.title} onto ${state.destination.name}${Strings.pad(GlyphChars.Dot, 2, 2)}${
state.repo.name
}`,
multiselect: log !== undefined,
placeholder:
log === undefined
? `${state.source.name} has no pickable commits`
: `Choose commits to cherry-pick onto ${state.destination.name}`,
items:
log === undefined
? []
: [
...Iterables.map(log.commits.values(), commit =>
CommitQuickPickItem.create(
commit,
state.commits ? state.commits.some(c => c.sha === commit.sha) : undefined,
{ compact: true }
)
)
]
});
const selection = yield step;
if (!this.canMoveNext(step, state, selection)) {
continue;
}
state.commits = selection.map(i => i.item);
}
const step = this.createConfirmStep(
`Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${state.repo.name}`,
[
{
label: this.title,
description: `${
state.commits.length === 1
? state.commits[0].shortSha
: `${state.commits.length} commits`
} onto ${state.destination.name}`,
detail: `Will apply ${
state.commits.length === 1
? `commit ${state.commits[0].shortSha}`
: `${state.commits.length} commits`
} onto ${state.destination.name}`
}
]
);
const selection = yield step;
if (!this.canMoveNext(step, state, selection)) {
continue;
}
this.execute(state as State);
break;
}
catch (ex) {
if (ex instanceof CommandAbortError) break;
throw ex;
}
}
}
}

+ 125
- 0
src/commands/quick/fetch.ts Переглянути файл

@ -0,0 +1,125 @@
'use strict';
import { QuickPickItem } from 'vscode';
import { Container } from '../../container';
import { Repository } from '../../git/gitService';
import { CommandAbortError, QuickCommandBase, QuickPickStep } from './quickCommand';
import { RepositoryQuickPickItem } from '../../quickpicks';
import { Strings } from '../../system';
import { GlyphChars } from '../../constants';
interface State {
repos: Repository[];
flags: string[];
}
export class FetchQuickCommand extends QuickCommandBase {
constructor() {
super('fetch', 'Fetch');
}
execute(state: State) {
return Container.git.fetchAll(state.repos, {
all: state.flags.includes('--all'),
prune: state.flags.includes('--prune')
});
}
async *steps(): AsyncIterableIterator<QuickPickStep> {
const state: Partial<State> & { counter: number } = { counter: 0 };
let oneRepo = false;
while (true) {
try {
if (state.repos === undefined || state.counter < 1) {
const repos = [...(await Container.git.getOrderedRepositories())];
if (repos.length === 1) {
oneRepo = true;
state.counter++;
state.repos = [repos[0]];
}
else {
const step = this.createStep<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
})
)
)
});
const selection = yield step;
if (!this.canMoveNext(step, state, selection)) {
break;
}
state.repos = selection.map(i => i.item);
}
}
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;
}
continue;
}
state.flags = selection[0].item;
this.execute(state as State);
break;
}
catch (ex) {
if (ex instanceof CommandAbortError) break;
throw ex;
}
}
}
}

+ 88
- 0
src/commands/quick/gitCommand.ts Переглянути файл

@ -0,0 +1,88 @@
'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'
})
)
]);
}
}

+ 165
- 0
src/commands/quick/merge.ts Переглянути файл

@ -0,0 +1,165 @@
'use strict';
import { QuickPickItem } from 'vscode';
import { Container } from '../../container';
import { GitBranch, Repository } from '../../git/gitService';
import { GlyphChars } from '../../constants';
import { CommandAbortError, QuickPickStep } from './quickCommand';
import { BranchQuickPickItem, RepositoryQuickPickItem } from '../../quickpicks';
import { Strings } from '../../system';
import { GitCommandBase } from './gitCommand';
import { runGitCommandInTerminal } from '../../terminal';
interface State {
repo: Repository;
destination: GitBranch;
source: GitBranch;
flags: string[];
}
export class MergeQuickCommand extends GitCommandBase {
constructor() {
super('merge', 'Merge');
}
execute(state: State) {
runGitCommandInTerminal('merge', [...state.flags, state.source.ref].join(' '), state.repo.path);
}
async *steps(): AsyncIterableIterator<QuickPickStep> {
const state: Partial<State> & { counter: number } = { counter: 0 };
let oneRepo = false;
while (true) {
try {
if (state.repo === undefined || state.counter < 1) {
const repos = [...(await Container.git.getOrderedRepositories())];
if (repos.length === 1) {
oneRepo = true;
state.counter++;
state.repo = repos[0];
}
else {
const active = state.repo ? state.repo : await Container.git.getActiveRepository();
const step = this.createStep<RepositoryQuickPickItem>({
title: this.title,
placeholder: 'Choose a repository',
items: await Promise.all(
repos.map(r =>
RepositoryQuickPickItem.create(r, r.id === (active && active.id), {
branch: true,
fetched: true,
status: true
})
)
)
});
const selection = yield step;
if (!this.canMoveNext(step, state, selection)) {
break;
}
state.repo = selection[0].item;
}
}
state.destination = await state.repo.getBranch();
if (state.destination === undefined) break;
if (state.source === undefined || state.counter < 2) {
const destId = state.destination.id;
const step = this.createStep<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, {
filterBranches: b => b.id !== destId,
picked: state.source && state.source.ref
})
});
const selection = yield step;
if (!this.canMoveNext(step, state, selection)) {
if (oneRepo) {
break;
}
continue;
}
state.source = selection[0].item;
}
const count =
(await Container.git.getCommitCount(state.repo.path, [
`${state.destination.name}..${state.source.name}`
])) || 0;
if (count === 0) {
const step = this.createConfirmStep(
`Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${state.repo.name}`,
[
{
label: `Cancel ${this.title}`,
description: '',
detail: `${state.destination.name} is up to date with ${state.source.name}`
}
],
false
);
yield step;
break;
}
const step = this.createConfirmStep<QuickPickItem & { item: string[] }>(
`Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${state.repo.name}`,
[
{
label: this.title,
description: `${state.source.name} into ${state.destination.name}`,
detail: `Will merge ${Strings.pluralize('commit', count)} from ${state.source.name} into ${
state.destination.name
}`,
item: []
},
{
label: `Fast-forward ${this.title}`,
description: `--ff-only ${state.source.name} into ${state.destination.name}`,
detail: `Will fast-forward merge ${Strings.pluralize('commit', count)} from ${
state.source.name
} into ${state.destination.name}`,
item: ['--ff-only']
},
{
label: `No Fast-forward ${this.title}`,
description: `--no-ff ${state.source.name} into ${state.destination.name}`,
detail: `Will create a merge commit when merging ${Strings.pluralize(
'commit',
count
)} from ${state.source.name} into ${state.destination.name}`,
item: ['--no-ff']
}
]
);
const selection = yield step;
if (!this.canMoveNext(step, state, selection)) {
continue;
}
state.flags = selection[0].item;
this.execute(state as State);
break;
}
catch (ex) {
if (ex instanceof CommandAbortError) break;
throw ex;
}
}
}
}

+ 112
- 0
src/commands/quick/pull.ts Переглянути файл

@ -0,0 +1,112 @@
'use strict';
import { QuickPickItem } from 'vscode';
import { Container } from '../../container';
import { Repository } from '../../git/gitService';
import { CommandAbortError, QuickCommandBase, QuickPickStep } from './quickCommand';
import { RepositoryQuickPickItem } from '../../quickpicks';
import { Strings } from '../../system';
import { GlyphChars } from '../../constants';
interface State {
repos: Repository[];
flags: string[];
}
export class PullQuickCommand extends QuickCommandBase {
constructor() {
super('pull', 'Pull');
}
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 };
let oneRepo = false;
while (true) {
try {
if (state.repos === undefined || state.counter < 1) {
const repos = [...(await Container.git.getOrderedRepositories())];
if (repos.length === 1) {
oneRepo = true;
state.counter++;
state.repos = [repos[0]];
}
else {
const step = this.createStep<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
})
)
)
});
const selection = yield step;
if (!this.canMoveNext(step, state, selection)) {
break;
}
state.repos = selection.map(i => i.item);
}
}
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 pull ${
state.repos.length === 1
? state.repos[0].formattedName
: `${state.repos.length} repositories`
}`,
item: []
},
{
label: `${this.title} with Rebase`,
description: '--rebase',
detail: `Will pull with rebase ${
state.repos.length === 1
? state.repos[0].formattedName
: `${state.repos.length} repositories`
}`,
item: ['--rebase']
}
]
);
const selection = yield step;
if (!this.canMoveNext(step, state, selection)) {
if (oneRepo) {
break;
}
continue;
}
state.flags = selection[0].item;
this.execute(state as State);
break;
}
catch (ex) {
if (ex instanceof CommandAbortError) break;
throw ex;
}
}
}
}

+ 97
- 0
src/commands/quick/push.ts Переглянути файл

@ -0,0 +1,97 @@
'use strict';
import { Container } from '../../container';
import { Repository } from '../../git/gitService';
import { CommandAbortError, QuickCommandBase, QuickPickStep } from './quickCommand';
import { RepositoryQuickPickItem } from '../../quickpicks';
import { Strings } from '../../system';
import { GlyphChars } from '../../constants';
interface State {
repos: Repository[];
}
export class PushQuickCommand extends QuickCommandBase {
constructor() {
super('push', 'Push');
}
execute(state: State) {
return Container.git.pushAll(state.repos);
}
async *steps(): AsyncIterableIterator<QuickPickStep> {
const state: Partial<State> & { counter: number } = { counter: 0 };
let oneRepo = false;
while (true) {
try {
if (state.repos === undefined || state.counter < 1) {
const repos = [...(await Container.git.getOrderedRepositories())];
if (repos.length === 1) {
oneRepo = true;
state.counter++;
state.repos = [repos[0]];
}
else {
const step = this.createStep<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
})
)
)
});
const selection = yield step;
if (!this.canMoveNext(step, state, selection)) {
break;
}
state.repos = selection.map(i => i.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`
}`,
[
{
label: this.title,
description: '',
detail: `Will push ${
state.repos.length === 1
? state.repos[0].formattedName
: `${state.repos.length} repositories`
}`
}
]
);
const selection = yield step;
if (!this.canMoveNext(step, state, selection)) {
if (oneRepo) {
break;
}
continue;
}
this.execute(state as State);
break;
}
catch (ex) {
if (ex instanceof CommandAbortError) break;
throw ex;
}
}
}
}

+ 113
- 0
src/commands/quick/quickCommand.ts Переглянути файл

@ -0,0 +1,113 @@
'use strict';
import { QuickInputButton, QuickPick, QuickPickItem } from 'vscode';
export interface QuickPickStep<T extends QuickPickItem = any> {
buttons?: QuickInputButton[];
selectedItems?: QuickPickItem[];
items: QuickPickItem[];
multiselect?: boolean;
placeholder?: string;
title?: string;
onDidClickButton?(quickpick: QuickPick<T>, button: QuickInputButton): void;
validate?(selection: T[]): boolean;
}
export class CommandAbortError extends Error {
constructor() {
super('Abort');
}
}
export abstract class QuickCommandBase implements QuickPickItem {
static is(item: QuickPickItem): item is QuickCommandBase {
return item instanceof QuickCommandBase;
}
readonly description?: string;
readonly detail?: string;
private _current: QuickPickStep | undefined;
private _stepsIterator: AsyncIterableIterator<QuickPickStep> | undefined;
constructor(
public readonly label: string,
public readonly title: string,
options: {
description?: string;
detail?: string;
} = {}
) {
this.description = options.description;
this.detail = options.detail;
}
abstract steps(): AsyncIterableIterator<QuickPickStep>;
async previous(): Promise<QuickPickStep | undefined> {
// Simulate going back, by having no selection
return (await this.next([])).value;
}
async next(selection?: QuickPickItem[]): Promise<IteratorResult<QuickPickStep>> {
if (this._stepsIterator === undefined) {
this._stepsIterator = this.steps();
}
const result = await this._stepsIterator.next(selection);
this._current = result.value;
if (result.done) {
this._stepsIterator = undefined;
}
return result;
}
get value(): QuickPickStep | undefined {
return this._current;
}
protected createConfirmStep<T extends QuickPickItem>(
title: string,
confirmations: T[],
cancellable: boolean = true
): QuickPickStep<T> {
return this.createStep<T>({
placeholder: `Confirm ${this.title}`,
title: title,
items: cancellable ? [...confirmations, { label: 'Cancel' }] : confirmations,
selectedItems: [confirmations[0]],
// eslint-disable-next-line no-loop-func
validate: (selection: T[]) => {
if (selection[0].label === 'Cancel') throw new CommandAbortError();
return true;
}
});
}
protected createStep<T extends QuickPickItem>(step: QuickPickStep<T>): QuickPickStep<T> {
return step;
}
protected canMoveNext<T extends QuickPickItem>(
step: QuickPickStep<T>,
state: { counter: number },
selection: T[] | undefined
): selection is T[] {
if (selection === undefined || selection.length === 0) {
state.counter--;
if (state.counter < 0) {
state.counter = 0;
}
return false;
}
if (step.validate === undefined || step.validate(selection)) {
state.counter++;
return true;
}
return false;
}
}

+ 157
- 0
src/commands/quick/rebase.ts Переглянути файл

@ -0,0 +1,157 @@
'use strict';
import { QuickPickItem } from 'vscode';
import { Container } from '../../container';
import { GitBranch, Repository } from '../../git/gitService';
import { GlyphChars } from '../../constants';
import { CommandAbortError, QuickPickStep } from './quickCommand';
import { BranchQuickPickItem, RepositoryQuickPickItem } from '../../quickpicks';
import { Strings } from '../../system';
import { GitCommandBase } from './gitCommand';
import { runGitCommandInTerminal } from '../../terminal';
interface State {
repo: Repository;
destination: GitBranch;
source: GitBranch;
flags: string[];
}
export class RebaseQuickCommand extends GitCommandBase {
constructor() {
super('rebase', 'Rebase');
}
execute(state: State) {
runGitCommandInTerminal('rebase', [...state.flags, state.source.ref].join(' '), state.repo.path);
}
async *steps(): AsyncIterableIterator<QuickPickStep> {
const state: Partial<State> & { counter: number } = { counter: 0 };
let oneRepo = false;
while (true) {
try {
if (state.repo === undefined || state.counter < 1) {
const repos = [...(await Container.git.getOrderedRepositories())];
if (repos.length === 1) {
oneRepo = true;
state.counter++;
state.repo = repos[0];
}
else {
const active = state.repo ? state.repo : await Container.git.getActiveRepository();
const step = this.createStep<RepositoryQuickPickItem>({
title: this.title,
placeholder: 'Choose a repository',
items: await Promise.all(
repos.map(r =>
RepositoryQuickPickItem.create(r, r.id === (active && active.id), {
branch: true,
fetched: true,
status: true
})
)
)
});
const selection = yield step;
if (!this.canMoveNext(step, state, selection)) {
break;
}
state.repo = selection[0].item;
}
}
state.destination = await state.repo.getBranch();
if (state.destination === undefined) break;
if (state.source === undefined || state.counter < 2) {
const destId = state.destination.id;
const step = this.createStep<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, {
filterBranches: b => b.id !== destId,
picked: state.source && state.source.ref
})
});
const selection = yield step;
if (!this.canMoveNext(step, state, selection)) {
if (oneRepo) {
break;
}
continue;
}
state.source = selection[0].item;
}
const count =
(await Container.git.getCommitCount(state.repo.path, [
`${state.destination.name}..${state.source.name}`
])) || 0;
if (count === 0) {
const step = this.createConfirmStep(
`Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${state.repo.name}`,
[
{
label: `Cancel ${this.title}`,
description: '',
detail: `${state.destination.name} is up to date with ${state.source.name}`
}
],
false
);
yield step;
break;
}
const step = this.createConfirmStep<QuickPickItem & { item: string[] }>(
`Confirm ${this.title}${Strings.pad(GlyphChars.Dot, 2, 2)}${state.repo.name}`,
[
{
label: this.title,
description: `${state.destination.name} with ${state.source.name}`,
detail: `Will update ${state.destination.name} by applying ${Strings.pluralize(
'commit',
count
)} on top of ${state.source.name}`,
item: []
},
{
label: `Interactive ${this.title}`,
description: `--interactive ${state.destination.name} with ${state.source.name}`,
detail: `Will interactively update ${
state.destination.name
} by applying ${Strings.pluralize('commit', count)} on top of ${state.source.name}`,
item: ['--interactive']
}
]
);
const selection = yield step;
if (!this.canMoveNext(step, state, selection)) {
continue;
}
state.flags = selection[0].item;
this.execute(state as State);
break;
}
catch (ex) {
if (ex instanceof CommandAbortError) break;
throw ex;
}
}
}
}

+ 27
- 4
src/git/git.ts Переглянути файл

@ -608,8 +608,12 @@ export class Git {
return git<string>({ cwd: repoPath }, ...params);
}
static fetch(repoPath: string, options: { all?: boolean; remote?: string } = {}) {
static fetch(repoPath: string, options: { all?: boolean; prune?: boolean; remote?: string } = {}) {
const params = ['fetch'];
if (options.prune) {
params.push('--prune');
}
if (options.remote) {
params.push(options.remote);
}
@ -635,22 +639,26 @@ export class Git {
{
authors,
maxCount,
merges,
reverse,
similarityThreshold
}: { authors?: string[]; maxCount?: number; reverse?: boolean; similarityThreshold?: number }
}: { authors?: string[]; maxCount?: number; merges?: boolean; reverse?: boolean; similarityThreshold?: number }
) {
const params = [
'log',
'--name-status',
`--format=${GitLogParser.defaultFormat}`,
'--full-history',
`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`,
'-m'
`-M${similarityThreshold == null ? '' : `${similarityThreshold}%`}`
];
if (maxCount && !reverse) {
params.push(`-n${maxCount}`);
}
if (merges) {
params.push('-m');
}
if (authors) {
params.push('--use-mailmap', ...authors.map(a => `--author=${a}`));
}
@ -858,6 +866,21 @@ export class Git {
return git<string>({ cwd: repoPath }, 'reset', '-q', '--', fileName);
}
static async rev_list(
repoPath: string,
refs: string[],
options: { count?: boolean } = {}
): Promise<number | undefined> {
const params = [];
if (options.count) {
params.push('--count');
}
params.push(...refs);
const data = await git<string>({ cwd: repoPath, errors: GitErrorHandling.Ignore }, 'rev-list', ...params);
return data.length === 0 ? undefined : Number(data.trim()) || undefined;
}
static async rev_parse(repoPath: string, ref: string): Promise<string | undefined> {
const data = await git<string>({ cwd: repoPath, errors: GitErrorHandling.Ignore }, 'rev-parse', ref);
return data.length === 0 ? undefined : data.trim();

+ 31
- 9
src/git/gitService.ts Переглянути файл

@ -520,8 +520,8 @@ export class GitService implements Disposable {
@gate()
@log()
fetch(repoPath: string, remote?: string) {
return Git.fetch(repoPath, { remote: remote });
fetch(repoPath: string, options: { all?: boolean; prune?: boolean; remote?: string } = {}) {
return Git.fetch(repoPath, options);
}
@gate()
@ -530,14 +530,14 @@ export class GitService implements Disposable {
0: (repos?: Repository[]) => (repos === undefined ? false : repos.map(r => r.name).join(', '))
}
})
async fetchAll(repositories?: Repository[]) {
async fetchAll(repositories?: Repository[], options: { all?: boolean; prune?: boolean } = {}) {
if (repositories === undefined) {
repositories = await this.getOrderedRepositories();
}
if (repositories.length === 0) return;
if (repositories.length === 1) {
repositories[0].fetch();
repositories[0].fetch(options);
return;
}
@ -547,7 +547,7 @@ export class GitService implements Disposable {
location: ProgressLocation.Notification,
title: `Fetching ${repositories.length} repositories`
},
() => Promise.all(repositories!.map(r => r.fetch({ progress: false })))
() => Promise.all(repositories!.map(r => r.fetch({ progress: false, ...options })))
);
}
@ -557,14 +557,14 @@ export class GitService implements Disposable {
0: (repos?: Repository[]) => (repos === undefined ? false : repos.map(r => r.name).join(', '))
}
})
async pullAll(repositories?: Repository[]) {
async pullAll(repositories?: Repository[], options: { rebase?: boolean } = {}) {
if (repositories === undefined) {
repositories = await this.getOrderedRepositories();
}
if (repositories.length === 0) return;
if (repositories.length === 1) {
repositories[0].pull();
repositories[0].pull(options);
return;
}
@ -574,7 +574,7 @@ export class GitService implements Disposable {
location: ProgressLocation.Notification,
title: `Pulling ${repositories.length} repositories`
},
() => Promise.all(repositories!.map(r => r.pull({ progress: false })))
() => Promise.all(repositories!.map(r => r.pull({ progress: false, ...options })))
);
}
@ -611,6 +611,19 @@ export class GitService implements Disposable {
editor !== undefined ? `TextEditor(${Logger.toLoggable(editor.document.uri)})` : 'undefined'
}
})
async getActiveRepository(editor?: TextEditor): Promise<Repository | undefined> {
const repoPath = await this.getActiveRepoPath(editor);
if (repoPath === undefined) return undefined;
return this.getRepository(repoPath);
}
@log({
args: {
0: (editor: TextEditor) =>
editor !== undefined ? `TextEditor(${Logger.toLoggable(editor.document.uri)})` : 'undefined'
}
})
async getActiveRepoPath(editor?: TextEditor): Promise<string | undefined> {
editor = editor || window.activeTextEditor;
@ -1112,6 +1125,11 @@ export class GitService implements Disposable {
}
@log()
getCommitCount(repoPath: string, refs: string[]) {
return Git.rev_list(repoPath, refs, { count: true });
}
@log()
async getCommitForFile(
repoPath: string | undefined,
fileName: string,
@ -1357,7 +1375,10 @@ export class GitService implements Disposable {
@log()
async getLog(
repoPath: string,
{ ref, ...options }: { authors?: string[]; maxCount?: number; ref?: string; reverse?: boolean } = {}
{
ref,
...options
}: { authors?: string[]; maxCount?: number; merges?: boolean; ref?: string; reverse?: boolean } = {}
): Promise<GitLog | undefined> {
const maxCount = options.maxCount == null ? Container.config.advanced.maxListItems || 0 : options.maxCount;
@ -1365,6 +1386,7 @@ export class GitService implements Disposable {
const data = await Git.log(repoPath, ref, {
authors: options.authors,
maxCount: maxCount,
merges: options.merges === undefined ? true : options.merges,
reverse: options.reverse,
similarityThreshold: Container.config.advanced.similarityThreshold
});

+ 47
- 8
src/git/models/repository.ts Переглянути файл

@ -233,6 +233,34 @@ 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);
return void (await window.withProgress(
{
location: ProgressLocation.Notification,
title: `Checking out ${this.formattedName} to ${ref}...`,
cancellable: false
},
() => this.checkoutCore(ref)
));
}
private async checkoutCore(ref: string, options: { remote?: string } = {}) {
try {
void (await Container.git.checkout(this.path, ref));
this.fireChange(RepositoryChange.Repository);
}
catch (ex) {
Logger.error(ex);
Messages.showGenericErrorMessage('Unable to checkout repository');
}
}
containsUri(uri: Uri) {
if (GitUri.is(uri)) {
uri = uri.repoPath !== undefined ? GitUri.file(uri.repoPath) : uri.documentUri();
@ -243,7 +271,7 @@ export class Repository implements Disposable {
@gate()
@log()
async fetch(options: { progress?: boolean; remote?: string } = {}) {
async fetch(options: { all?: boolean; progress?: boolean; prune?: boolean; remote?: string } = {}) {
const { progress, ...opts } = { progress: true, ...options };
if (!progress) return this.fetchCore(opts);
@ -256,9 +284,9 @@ export class Repository implements Disposable {
));
}
private async fetchCore(options: { remote?: string } = {}) {
private async fetchCore(options: { all?: boolean; prune?: boolean; remote?: string } = {}) {
try {
void (await Container.git.fetch(this.path, options.remote));
void (await Container.git.fetch(this.path, options));
this.fireChange(RepositoryChange.Repository);
}
@ -279,6 +307,17 @@ export class Repository implements Disposable {
return Container.git.getBranches(this.path, options);
}
getBranchesAndOrTags(
options: {
filterBranches?: (b: GitBranch) => boolean;
filterTags?: (t: GitTag) => boolean;
include?: 'all' | 'branches' | 'tags';
sort?: boolean;
} = {}
) {
return Container.git.getBranchesAndOrTags(this.path, options);
}
getChangedFilesCount(sha?: string): Promise<GitDiffShortStat | undefined> {
return Container.git.getChangedFilesCount(this.path, sha);
}
@ -337,8 +376,8 @@ export class Repository implements Disposable {
@gate()
@log()
async pull(options: { progress?: boolean } = {}) {
const { progress } = { progress: true, ...options };
async pull(options: { progress?: boolean; rebase?: boolean } = {}) {
const { progress, ...opts } = { progress: true, ...options };
if (!progress) return this.pullCore();
return void (await window.withProgress(
@ -346,15 +385,15 @@ export class Repository implements Disposable {
location: ProgressLocation.Notification,
title: `Pulling ${this.formattedName}...`
},
() => this.pullCore()
() => this.pullCore(opts)
));
}
private async pullCore() {
private async pullCore(options: { rebase?: boolean } = {}) {
try {
const tracking = await this.hasTrackingBranch();
if (tracking) {
void (await commands.executeCommand('git.pull', this.path));
void (await commands.executeCommand(options.rebase ? 'git.pullRebase' : 'git.pull', this.path));
}
else if (configuration.getAny<boolean>('git.fetchOnPull', Uri.file(this.path))) {
void (await Container.git.fetch(this.path));

+ 7
- 0
src/system/string.ts Переглянути файл

@ -198,6 +198,13 @@ export namespace Strings {
.digest(encoding);
}
export function splitLast(s: string, splitter: string) {
const index = s.lastIndexOf(splitter);
if (index === -1) return [s];
return [s.substr(index), s.substring(0, index - 1)];
}
export function splitSingle(s: string, splitter: string) {
const parts = s.split(splitter, 1);
const first = parts[0];

+ 2
- 2
src/views/nodes/repositoryNode.ts Переглянути файл

@ -190,12 +190,12 @@ export class RepositoryNode extends SubscribeableViewNode {
}
@log()
fetch(options: { progress?: boolean; remote?: string } = {}) {
fetch(options: { all?: boolean; progress?: boolean; prune?: boolean; remote?: string } = {}) {
return this.repo.fetch(options);
}
@log()
pull(options: { progress?: boolean } = {}) {
pull(options: { progress?: boolean; rebase?: boolean } = {}) {
return this.repo.pull(options);
}

+ 1
- 0
src/vsls/host.ts Переглянути файл

@ -32,6 +32,7 @@ const gitWhitelist = new Map boolean>([
['ls-tree', defaultWhitelistFn],
['merge-base', defaultWhitelistFn],
['remote', args => args[1] === '-v' || args[1] === 'get-url'],
['rev-list', defaultWhitelistFn],
['rev-parse', defaultWhitelistFn],
['shortlog', defaultWhitelistFn],
['show', defaultWhitelistFn],

Завантаження…
Відмінити
Зберегти