diff --git a/extensions/git/package.json b/extensions/git/package.json index 2a47e2dc209..0898b592aa7 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -172,6 +172,21 @@ "title": "%command.branch%", "category": "Git" }, + { + "command": "git.createTag", + "title": "%command.createTag%", + "category": "Git" + }, + { + "command": "git.showTags", + "title": "%command.showTags%", + "category": "Git" + }, + { + "command": "git.pushWithTags", + "title": "%command.pushWithTags%", + "category": "Git" + }, { "command": "git.pull", "title": "%command.pull%", @@ -306,10 +321,22 @@ "command": "git.pullRebase", "when": "config.git.enabled && scmProvider == git && gitState == idle" }, + { + "command": "git.showTags", + "when": "config.git.enabled && scmProvider == git && gitState == idle" + }, + { + "command": "git.createTag", + "when": "config.git.enabled && scmProvider == git && gitState == idle" + }, { "command": "git.push", "when": "config.git.enabled && scmProvider == git && gitState == idle" }, + { + "command": "git.pushWithTags", + "when": "config.git.enabled && scmProvider == git && gitState == idle" + }, { "command": "git.pushTo", "when": "config.git.enabled && scmProvider == git && gitState == idle" diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index efdde34dd7a..18636a195fb 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -21,8 +21,11 @@ "command.undoCommit": "Undo Last Commit", "command.checkout": "Checkout to...", "command.branch": "Create Branch...", + "command.createTag": "Create Tag", + "command.showTags": "Show Tags", "command.pull": "Pull", "command.pullRebase": "Pull (Rebase)", + "command.pushWithTags": "Push With Tags", "command.push": "Push", "command.pushTo": "Push to...", "command.sync": "Sync", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 24f9c35129a..7a858cc00da 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -6,7 +6,7 @@ 'use strict'; import { Uri, commands, scm, Disposable, window, workspace, QuickPickItem, OutputChannel, Range, WorkspaceEdit, Position, LineChange, SourceControlResourceState } from 'vscode'; -import { Ref, RefType, Git, GitErrorCodes } from './git'; +import { Ref, RefType, Git, GitErrorCodes, PushOptions } from './git'; import { Model, Resource, Status, CommitOptions, WorkingTreeGroup, IndexGroup, MergeGroup } from './model'; import { toGitUri, fromGitUri } from './uri'; import { applyLineChanges, intersectDiffWithRange, toLineRanges, invertLineChange } from './staging'; @@ -17,6 +17,10 @@ import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); +class PushOptionsImpl implements PushOptions { + withTags: boolean; +} + class CheckoutItem implements QuickPickItem { protected get shortCommit(): string { return (this.ref.commit || '').substr(0, 8); } @@ -37,6 +41,26 @@ class CheckoutItem implements QuickPickItem { } } +class ShowTag implements QuickPickItem { + + protected get shortCommit(): string { return (this.ref.commit || '').substr(0, 8); } + get label(): string { return this.ref.name || this.shortCommit; } + get description(): string { return this.shortCommit; } + + constructor(protected ref: Ref) { } + + async run(model: Model): Promise { + const result = await model.showObject(this.ref.name || ''); + + if (!result) { + return; + } + + workspace.openTextDocument({ language: 'en', content: result.trim() }) + .then(window.showTextDocument); + } +} + class CheckoutTagItem extends CheckoutItem { get description(): string { @@ -699,6 +723,48 @@ export class CommandCenter { await this.model.branch(name); } + @command('git.showTags') + async showTags(): Promise { + const tags = this.model.refs + .filter(x => x.type === RefType.Tag) + .map(tag => new ShowTag(tag)); + + const placeHolder = 'Select a tag'; + + var choice = await window.showQuickPick(tags, { placeHolder }); + + if (!choice) { + return; + } + + await choice.run(this.model); + } + + @command('git.createTag') + async createTag(): Promise { + const inputTagName = await window.showInputBox({ + placeHolder: localize('tag name', "Tag name"), + prompt: localize('provide tag name', "Please provide a tag name"), + ignoreFocusOut: true + }); + + if (!inputTagName) { + return; + } + + const inputMessage = await window.showInputBox({ + placeHolder: localize('tag message', "Message"), + prompt: localize('provide tag message', "Please provide a message"), + ignoreFocusOut: true + }); + + const name = inputTagName.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$/g, '-'); + const message = inputMessage || name; + await this.model.tag(name, message); + + window.showInformationMessage(localize('tag creation success', "Successfully created tag.")); + } + @command('git.pull') async pull(): Promise { const remotes = this.model.remotes; @@ -735,6 +801,23 @@ export class CommandCenter { await this.model.push(); } + @command('git.pushWithTags') + async pushWithTags(): Promise { + const remotes = this.model.remotes; + + if (remotes.length === 0) { + window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to.")); + return; + } + + let pushOptions = new PushOptionsImpl(); + pushOptions.withTags = true; + + await this.model.push(undefined, undefined, pushOptions); + + window.showInformationMessage(localize('push with tags success', "Successfully pushed with tags.")); + } + @command('git.pushTo') async pushTo(): Promise { const remotes = this.model.remotes; diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 4adc617a3eb..7f820ab16c3 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -23,6 +23,7 @@ export interface IGit { export interface PushOptions { setUpstream?: boolean; + withTags?: boolean; } export interface IFileStatus { @@ -650,6 +651,29 @@ export class Repository { await this.run(args); } + async show(ref: string): Promise { + let args = ['show', '-s', '--format=%H\n%B', ref]; + const result = await this.run(args); + + if (!result) { + return Promise.reject('Invalid reference provided.'); + } + + return result.stdout; + } + + async tag(name: string, message: string, lightweight: boolean): Promise { + let args = ['tag']; + + if (lightweight) { + args.push(name); + } else { + args = args.concat(['-a', name, '-m', message]); + } + + await this.run(args); + } + async clean(paths: string[]): Promise { const pathsByGroup = groupBy(paths, p => path.dirname(p)); const groups = Object.keys(pathsByGroup).map(k => pathsByGroup[k]); @@ -757,8 +781,14 @@ export class Repository { async push(remote?: string, name?: string, options?: PushOptions): Promise { const args = ['push']; - if (options && options.setUpstream) { - args.push('-u'); + if (options) { + if (options.setUpstream) { + args.push('-u'); + } + + if (options.withTags) { + args.push('--tags'); + } } if (remote) { @@ -945,8 +975,8 @@ export class Repository { } async getCommit(ref: string): Promise { - const result = await this.run(['show', '-s', '--format=%H\n%B', ref]); - const match = /^([0-9a-f]{40})\n([^]*)$/m.exec(result.stdout.trim()); + const result = await this.show(ref); + const match = /^([0-9a-f]{40})\n([^]*)$/m.exec(result.trim()); if (!match) { return Promise.reject('bad commit format'); diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index 1d74a87d3a3..a1c6fd267d6 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -211,7 +211,8 @@ export enum Operation { Init = 1 << 12, Show = 1 << 13, Stage = 1 << 14, - GetCommitTemplate = 1 << 15 + GetCommitTemplate = 1 << 15, + Tag = 1 << 16 } // function getOperationName(operation: Operation): string { @@ -454,6 +455,10 @@ export class Model implements Disposable { await this.run(Operation.Branch, () => this.repository.branch(name, true)); } + async tag(name: string, message: string): Promise { + await this.run(Operation.Tag, () => this.repository.tag(name, message, false)); + } + async checkout(treeish: string): Promise { await this.run(Operation.Checkout, () => this.repository.checkout(treeish, [])); } @@ -506,6 +511,10 @@ export class Model implements Disposable { }); } + async showObject(ref: string): Promise { + return await this.run(Operation.Show, () => this.repository.show(ref)); + } + async getCommitTemplate(): Promise { return await this.run(Operation.GetCommitTemplate, async () => this.repository.getCommitTemplate()); }