From a7e6e4112a1266b3c4fdf941c04ef440a5513f79 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Fri, 14 Aug 2020 03:07:37 -0400 Subject: [PATCH] Adds setUpstream ability to push --- src/@types/git.d.ts | 72 ++++++++++++++++++++++++++++++++++++++++++-- src/commands/git/push.ts | 59 ++++++++++++++++++++++++++++++------ src/git/gitService.ts | 5 ++- src/git/models/repository.ts | 34 ++++++++++++++++----- 4 files changed, 148 insertions(+), 22 deletions(-) diff --git a/src/@types/git.d.ts b/src/@types/git.d.ts index f1b456f..42dfcf1 100644 --- a/src/@types/git.d.ts +++ b/src/@types/git.d.ts @@ -3,7 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, Uri } from 'vscode'; +import { Disposable, Event, ProviderResult, Uri } from 'vscode'; + +export { ProviderResult } from 'vscode'; export interface Git { readonly path: string; @@ -41,7 +43,10 @@ export interface Commit { readonly hash: string; readonly message: string; readonly parents: string[]; - readonly authorEmail?: string | undefined; + readonly authorDate?: Date; + readonly authorName?: string; + readonly authorEmail?: string; + readonly commitDate?: Date; } export interface Submodule { @@ -117,6 +122,22 @@ export interface RepositoryUIState { export interface LogOptions { /** Max number of log entries to retrieve. If not specified, the default is 32. */ readonly maxEntries?: number; + readonly path?: string; +} + +export interface CommitOptions { + all?: boolean | 'tracked'; + amend?: boolean; + signoff?: boolean; + signCommit?: boolean; + empty?: boolean; +} + +export interface BranchQuery { + readonly remote?: boolean; + readonly pattern?: string; + readonly count?: number; + readonly contains?: string; } export interface Repository { @@ -158,6 +179,7 @@ export interface Repository { createBranch(name: string, checkout: boolean, ref?: string): Promise; deleteBranch(name: string, force?: boolean): Promise; getBranch(name: string): Promise; + getBranches(query: BranchQuery): Promise; setBranchUpstream(name: string, upstream: string): Promise; getMergeBase(ref1: string, ref2: string): Promise; @@ -167,6 +189,7 @@ export interface Repository { addRemote(name: string, url: string): Promise; removeRemote(name: string): Promise; + renameRemote(name: string, newName: string): Promise; fetch(remote?: string, ref?: string, depth?: number): Promise; pull(unshallow?: boolean): Promise; @@ -174,13 +197,54 @@ export interface Repository { blame(path: string): Promise; log(options?: LogOptions): Promise; + + commit(message: string, opts?: CommitOptions): Promise; } +export interface RemoteSource { + readonly name: string; + readonly description?: string; + readonly url: string | string[]; +} + +export interface RemoteSourceProvider { + readonly name: string; + readonly icon?: string; // codicon name + readonly supportsQuery?: boolean; + getRemoteSources(query?: string): ProviderResult; + publishRepository?(repository: Repository): Promise; +} + +export interface Credentials { + readonly username: string; + readonly password: string; +} + +export interface CredentialsProvider { + getCredentials(host: Uri): ProviderResult; +} + +export interface PushErrorHandler { + handlePushError(repository: Repository, remote: Remote, refspec: string, error: Error & { gitErrorCode: GitErrorCodes }): Promise; +} + +export type APIState = 'uninitialized' | 'initialized'; + export interface API { + readonly state: APIState; + readonly onDidChangeState: Event; readonly git: Git; readonly repositories: Repository[]; readonly onDidOpenRepository: Event; readonly onDidCloseRepository: Event; + + toGitUri(uri: Uri, ref: string): Uri; + getRepository(uri: Uri): Repository | null; + init(root: Uri): Promise; + + registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; + registerCredentialsProvider(provider: CredentialsProvider): Disposable; + registerPushErrorHandler(handler: PushErrorHandler): Disposable; } export interface GitExtension { @@ -218,6 +282,7 @@ export const enum GitErrorCodes { CantOpenResource = 'CantOpenResource', GitNotFound = 'GitNotFound', CantCreatePipe = 'CantCreatePipe', + PermissionDenied = 'PermissionDenied', CantAccessRemote = 'CantAccessRemote', RepositoryNotFound = 'RepositoryNotFound', RepositoryIsLocked = 'RepositoryIsLocked', @@ -234,5 +299,6 @@ export const enum GitErrorCodes { CantLockRef = 'CantLockRef', CantRebaseMultipleBranches = 'CantRebaseMultipleBranches', PatchDoesNotApply = 'PatchDoesNotApply', - NoPathFound = 'NoPathFound' + NoPathFound = 'NoPathFound', + UnknownPath = 'UnknownPath', } diff --git a/src/commands/git/push.ts b/src/commands/git/push.ts index 049eafb..e366aab 100644 --- a/src/commands/git/push.ts +++ b/src/commands/git/push.ts @@ -1,7 +1,7 @@ 'use strict'; import { configuration } from '../../configuration'; import { Container } from '../../container'; -import { GitReference, Repository } from '../../git/git'; +import { GitBranchReference, GitReference, Repository } from '../../git/git'; import { appendReposToTitle, PartialStepState, @@ -25,7 +25,7 @@ interface Context { title: string; } -type Flags = '--force'; +type Flags = '--force' | '--set-upstream' | string; interface State { repos: Repos; @@ -60,8 +60,15 @@ export class PushGitCommand extends QuickCommand { } execute(state: State) { + let setUpstream: { branch: string; remote: string } | undefined; + if (state.flags.includes('--set-upstream')) { + const index = state.flags.indexOf('--set-upstream'); + setUpstream = { branch: state.flags[index + 1], remote: state.flags[index + 2] }; + } + return Container.git.pushAll(state.repos, { force: state.flags.includes('--force'), + setUpstream: setUpstream, reference: state.reference, }); } @@ -159,14 +166,46 @@ export class PushGitCommand extends QuickCommand { const status = await repo.getStatus(); if (status?.state.ahead === 0) { - step = this.createConfirmStep( - appendReposToTitle(`Confirm ${context.title}`, state, context), - [], - DirectiveQuickPickItem.create(Directive.Cancel, true, { - label: `Cancel ${this.title}`, - detail: 'No commits found to push', - }), - ); + const items: FlagsQuickPickItem[] = []; + + if (state.reference == null && status.upstream == null) { + const branchRef: GitBranchReference = { + refType: 'branch', + name: status.branch, + ref: status.branch, + remote: false, + repoPath: status.repoPath, + }; + + for (const remote of await repo.getRemotes()) { + items.push( + FlagsQuickPickItem.create( + state.flags, + ['--set-upstream', status.branch, remote.name], + { + label: `${this.title} to ${remote.name}`, + detail: `Will push ${GitReference.toString(branchRef)} to ${remote.name}`, + }, + ), + ); + } + } + + if (items.length) { + step = this.createConfirmStep( + appendReposToTitle(`Confirm ${context.title}`, state, context), + items, + ); + } else { + step = this.createConfirmStep( + appendReposToTitle(`Confirm ${context.title}`, state, context), + [], + DirectiveQuickPickItem.create(Directive.Cancel, true, { + label: `Cancel ${this.title}`, + detail: 'No commits found to push', + }), + ); + } } else { let lastFetchedOn = ''; diff --git a/src/git/gitService.ts b/src/git/gitService.ts index 9d5c4c3..54b96db 100644 --- a/src/git/gitService.ts +++ b/src/git/gitService.ts @@ -622,7 +622,10 @@ export class GitService implements Disposable { 0: (repos?: Repository[]) => (repos == null ? false : repos.map(r => r.name).join(', ')), }, }) - async pushAll(repositories?: Repository[], options: { force?: boolean; reference?: GitReference } = {}) { + async pushAll( + repositories?: Repository[], + options: { force?: boolean; reference?: GitReference; setUpstream?: { branch: string; remote: string } } = {}, + ) { if (repositories == null) { repositories = await this.getOrderedRepositories(); } diff --git a/src/git/models/repository.ts b/src/git/models/repository.ts index 52fcac3..a33d73e 100644 --- a/src/git/models/repository.ts +++ b/src/git/models/repository.ts @@ -445,22 +445,40 @@ export class Repository implements Disposable { @gate() @log() - async push(options: { force?: boolean; progress?: boolean; reference?: GitReference } = {}) { - const { force, progress, reference } = { progress: true, ...options }; - if (!progress) return this.pushCore(force, reference); + async push( + options: { + force?: boolean; + progress?: boolean; + reference?: GitReference; + setUpstream?: { branch: string; remote: string }; + } = {}, + ) { + const { progress, ...opts } = { progress: true, ...options }; + if (!progress) return this.pushCore(opts); return void (await window.withProgress( { location: ProgressLocation.Notification, title: `Pushing ${this.formattedName}...`, }, - () => this.pushCore(force, reference), + () => this.pushCore(opts), )); } - private async pushCore(force: boolean = false, reference?: GitReference) { + private async pushCore( + options: { + force?: boolean; + reference?: GitReference; + setUpstream?: { branch: string; remote: string }; + } = {}, + ) { try { - if (reference != null) { + if (options.setUpstream != null) { + const repo = await GitService.getBuiltInGitRepository(this.path); + if (repo == null) return; + + await repo?.push(options.setUpstream.remote, options.setUpstream.branch, true); + } else if (options.reference != null) { const branch = await this.getBranch(); if (branch === undefined) return; @@ -469,11 +487,11 @@ export class Repository implements Disposable { await repo?.push( branch.getRemoteName(), - `${reference.ref}:${branch.getNameWithoutRemote()}`, + `${options.reference.ref}:${branch.getNameWithoutRemote()}`, undefined, ); } else { - void (await commands.executeCommand(force ? 'git.pushForce' : 'git.push', this.path)); + void (await commands.executeCommand(options.force ? 'git.pushForce' : 'git.push', this.path)); } this.fireChange(RepositoryChange.Repository);