├── .gitignore ├── res ├── Step1.png ├── defaults.PNG ├── gitflow.png ├── icon_128.png └── icon.svg ├── tslint.json ├── src ├── fail.ts ├── config.ts ├── fs.ts ├── cmd.ts ├── extension.ts ├── git.ts └── flow.ts ├── tsconfig.json ├── .vscode ├── settings.json ├── launch.json └── tasks.json ├── README.md ├── CHANGELOG.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules -------------------------------------------------------------------------------- /res/Step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/vscode-gitflow/HEAD/res/Step1.png -------------------------------------------------------------------------------- /res/defaults.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/vscode-gitflow/HEAD/res/defaults.PNG -------------------------------------------------------------------------------- /res/gitflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/vscode-gitflow/HEAD/res/gitflow.png -------------------------------------------------------------------------------- /res/icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/vscode-gitflow/HEAD/res/icon_128.png -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "quotemark": false, 9 | "no-console": false, 10 | "no-namespace": false, 11 | "variable-name": false, 12 | 13 | }, 14 | "rulesDirectory": [] 15 | } -------------------------------------------------------------------------------- /src/fail.ts: -------------------------------------------------------------------------------- 1 | import {MessageItem} from 'vscode'; 2 | 3 | export namespace fail { 4 | export interface ErrorMessageHandler extends MessageItem { 5 | title: string; 6 | cb: () => Promise; 7 | }; 8 | 9 | export interface IError { 10 | message: string; 11 | handlers?: ErrorMessageHandler[]; 12 | }; 13 | 14 | export function error(exc: IError) { 15 | throw exc; 16 | } 17 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": ".", 11 | "noImplicitReturns": true, 12 | "strictNullChecks": true 13 | }, 14 | "exclude": [ 15 | "node_modules", 16 | ".vscode-test" 17 | ] 18 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | "typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version 10 | } -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | class ConfigReader { 4 | private _readConfig(key: string, default_: T): T { 5 | const val = vscode.workspace.getConfiguration('gitflow').get(key); 6 | if (val === undefined) { 7 | return default_; 8 | } 9 | return val; 10 | } 11 | 12 | get deleteBranchOnFinish(): boolean { 13 | return this._readConfig('deleteBranchOnFinish', true); 14 | } 15 | 16 | get deleteRemoteBranches(): boolean { 17 | return this._readConfig('deleteRemoteBranches', true); 18 | } 19 | 20 | get default_development(): string { 21 | return this._readConfig('default.development', 'develop'); 22 | } 23 | 24 | get default_production(): string { 25 | return this._readConfig('default.production', 'master'); 26 | } 27 | } 28 | 29 | export const config = new ConfigReader(); -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], 11 | "stopOnEntry": false, 12 | "sourceMaps": true, 13 | "outDir": "${workspaceRoot}/out/src", 14 | "preLaunchTask": "npm" 15 | }, 16 | { 17 | "name": "Launch Tests", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "runtimeExecutable": "${execPath}", 21 | "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ], 22 | "stopOnEntry": false, 23 | "sourceMaps": true, 24 | "outDir": "${workspaceRoot}/out/test", 25 | "preLaunchTask": "npm" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gitflow integration for Visual Studio Code 2 | 3 | This extension provides integration and support for [gitflow](http://nvie.com/posts/a-successful-git-branching-model/). 4 | It is based on [this gitflow implementation](https://github.com/nvie/gitflow) 5 | and intends to be fully compatible with it. 6 | 7 | # Getting Started 8 | 9 | If you already have gitflow set up for your repository, just start execcuting 10 | gitflow commands from the Command Palette! 11 | 12 | ![Opening example](res/gitflow.png) 13 | 14 | ## Starting from Scratch 15 | 16 | 1. First, initialize git: 17 | ```sh 18 | $ git init 19 | ``` 20 | 2. Open the VS Code Command Palette and type 'gitflow' 21 | 22 | 3. Select 'Initialize repository for gitflow' 23 | ![Initializing Git Flow](res/Step1.png) 24 | 25 | 4. Follow the command prompts and accept the defaults... 26 | ![Defaults](res/defaults.PNG) 27 | 28 | 5. Setup complete! 29 | 30 | ### Note 31 | 32 | Development is ongoing. Please help support this project by trying it out 33 | and submitting issues and feature requests to [the github page](https://github.com/vector-of-bool/vscode-gitflow). 34 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | 9 | // A task runner that calls a custom npm script that compiles the extension. 10 | { 11 | "version": "0.1.0", 12 | 13 | // we want to run npm 14 | "command": "npm", 15 | 16 | // the command is a shell script 17 | "isShellCommand": true, 18 | 19 | // show the output window only if unrecognized errors occur. 20 | "showOutput": "silent", 21 | 22 | // we run the custom script "compile" as defined in package.json 23 | "args": ["run", "compile", "--loglevel", "silent"], 24 | 25 | // The tsc compiler is started in watching mode 26 | "isWatching": true, 27 | 28 | // use the standard tsc in watch mode problem matcher to find compile problems in the output. 29 | "problemMatcher": "$tsc-watch" 30 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.2.0 2 | 3 | - Add `bugfix` branch support [Thanks Vincent Biret ([baywet](https://github.com/baywet))] 4 | - Fix unhelpful error messages sometimes appearing 5 | - (1.2.1 fixes the changelog to include 1.2.0) 6 | 7 | # 1.1.2 8 | 9 | - *Fix intermittent assertion failure during init* [Thanks `RobDesideri`] 10 | - Respect tag prefix for releases [Thanks `poohnix`] 11 | - Guess the next release version automatically [Thanks `poohnix`] 12 | 13 | # 1.1.1 14 | 15 | - Progress messages while performing git operations 16 | 17 | # 1.1.0 18 | 19 | - Large refactor 20 | - Bugfixes when git is not available on `PATH` but is otherwise installed. 21 | - Shiny new icon. 22 | 23 | # 1.0.0 24 | 25 | - New configuration options: 26 | - `gitflow.deleteBranchOnfinish` 27 | - `gitflow.deleteRemoteBranches` 28 | - `gitflow.default.development` 29 | - `gitflow.default.production` 30 | - Fix issue with hardcoded development branch to `develop`. 31 | - Fix unhelpful errors from git when doing a gitflow operation on an unclean 32 | working tree 33 | 34 | # 0.1.0 35 | 36 | - Update to TypeScript 2.0 and enforce strict `null` checks. May now catch some 37 | latent issues. 38 | 39 | # 0.0.5 40 | 41 | - Fixed missing push of ``master`` and tags after finishing a release or a 42 | hotfix. -------------------------------------------------------------------------------- /src/fs.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as nodefs from 'fs'; 4 | 5 | 6 | export namespace fs { 7 | export function exists(path: string): Promise { 8 | return new Promise((resolve, _) => { 9 | nodefs.exists(path, resolve); 10 | }); 11 | } 12 | 13 | export function readFile(path: string): Promise { 14 | return new Promise((resolve, reject) => { 15 | nodefs.readFile(path, (err, data) => { 16 | if (err) 17 | reject(err); 18 | else 19 | resolve(data); 20 | }); 21 | }) 22 | } 23 | 24 | export function writeFile(path: string, buf: any): Promise { 25 | return new Promise((resolve, reject) => { 26 | nodefs.writeFile(path, buf, (err) => { 27 | if (err) 28 | reject(err); 29 | else 30 | resolve(); 31 | }); 32 | }); 33 | } 34 | 35 | export function remove(path: string) { 36 | return new Promise((resolve, reject) => { 37 | nodefs.unlink(path, (err) => { 38 | if (err) 39 | reject(err); 40 | else 41 | resolve(); 42 | }); 43 | }); 44 | } 45 | } -------------------------------------------------------------------------------- /src/cmd.ts: -------------------------------------------------------------------------------- 1 | import * as proc from 'child_process'; 2 | import * as vscode from 'vscode'; 3 | 4 | import {fail} from './fail'; 5 | 6 | 7 | export namespace cmd { 8 | export interface ExecutionResult { 9 | retc: number; 10 | stdout: string; 11 | stderr: string; 12 | } 13 | 14 | export function execute( 15 | command: string, args: string[], 16 | options?: proc.SpawnOptions): Promise { 17 | return new Promise((resolve, reject) => { 18 | options = options || {}; 19 | options.cwd = options.cwd || vscode.workspace.rootPath; 20 | console.log(`[gitflow] Execute ${command}`, args.join(' ')); 21 | const child = proc.spawn(command, args, options); 22 | child.on('error', (err) => { 23 | reject(err); 24 | }); 25 | let stdout_acc = ''; 26 | let stderr_acc = ''; 27 | child.stdout.on('data', (data: Uint8Array) => { 28 | stdout_acc += data.toString(); 29 | }); 30 | child.stderr.on('data', (data: Uint8Array) => { 31 | stderr_acc += data.toString(); 32 | }); 33 | child.on('close', (retc) => { 34 | console.log(`[gitflow] Command "${command}" returned code ${retc 35 | }: ${stderr_acc}`); 36 | resolve({retc: retc, stdout: stdout_acc, stderr: stderr_acc}); 37 | }); 38 | }); 39 | }; 40 | 41 | export async function 42 | executeRequired(command: string, args: string[], options?: proc.SpawnOptions): 43 | Promise { 44 | const result = await execute(command, args, options); 45 | if (result.retc !== 0) { 46 | fail.error({message: `"${command}" returned status ${result.retc}`}); 47 | } 48 | return result; 49 | } 50 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitflow", 3 | "displayName": "gitflow", 4 | "description": "Gitflow integration and support in Visual Studio Code", 5 | "version": "1.2.1", 6 | "publisher": "vector-of-bool", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/vector-of-bool/vscode-gitflow" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/vector-of-bool/vscode-gitflow/issues" 13 | }, 14 | "homepage": "https://github.com/vector-of-bool/vscode-gitflow", 15 | "keywords": [ 16 | "git", 17 | "flow" 18 | ], 19 | "engines": { 20 | "vscode": "^1.13.0" 21 | }, 22 | "categories": [ 23 | "Other" 24 | ], 25 | "galleryBanner": { 26 | "color": "#09585e", 27 | "theme": "dark" 28 | }, 29 | "icon": "res/icon_128.png", 30 | "activationEvents": [ 31 | "*" 32 | ], 33 | "main": "./out/src/extension", 34 | "contributes": { 35 | "commands": [ 36 | { 37 | "command": "gitflow.initialize", 38 | "title": "Initialize repository for gitflow", 39 | "category": "Gitflow" 40 | }, 41 | { 42 | "command": "gitflow.featureStart", 43 | "title": "Feature: start", 44 | "category": "Gitflow" 45 | }, 46 | { 47 | "command": "gitflow.featureRebase", 48 | "title": "Feature: rebase", 49 | "category": "Gitflow" 50 | }, 51 | { 52 | "command": "gitflow.featureFinish", 53 | "title": "Feature: finish", 54 | "category": "Gitflow" 55 | }, 56 | { 57 | "command": "gitflow.bugfixStart", 58 | "title": "Bugfix: start", 59 | "category": "Gitflow" 60 | }, 61 | { 62 | "command": "gitflow.bugfixRebase", 63 | "title": "Bugfix: rebase", 64 | "category": "Gitflow" 65 | }, 66 | { 67 | "command": "gitflow.bugfixFinish", 68 | "title": "Bugfix: finish", 69 | "category": "Gitflow" 70 | }, 71 | { 72 | "command": "gitflow.releaseStart", 73 | "title": "Release: start", 74 | "category": "Gitflow" 75 | }, 76 | { 77 | "command": "gitflow.releaseFinish", 78 | "title": "Release: finish", 79 | "category": "Gitflow" 80 | }, 81 | { 82 | "command": "gitflow.hotfixStart", 83 | "title": "Hotfix: start", 84 | "category": "Gitflow" 85 | }, 86 | { 87 | "command": "gitflow.hotfixFinish", 88 | "title": "Hotfix: finish", 89 | "category": "Gitflow" 90 | } 91 | ], 92 | "configuration": { 93 | "properties": { 94 | "gitflow.deleteBranchOnFinish": { 95 | "type": "boolean", 96 | "default": true, 97 | "description": "After finishing a branch, delete the branch" 98 | }, 99 | "gitflow.deleteRemoteBranches": { 100 | "type": "boolean", 101 | "default": true, 102 | "description": "If true, and `gitflow.deleteBranchOnFinish` is true, remote branches will be deleted when finishing a branch" 103 | }, 104 | "gitflow.default.development": { 105 | "type": "string", 106 | "default": "develop", 107 | "description": "Default name for the development branch [develop]" 108 | }, 109 | "gitflow.default.production": { 110 | "type": "string", 111 | "default": "master", 112 | "description": "Default name for the production branch [master]" 113 | } 114 | } 115 | } 116 | }, 117 | "scripts": { 118 | "vscode:prepublish": "./node_modules/typescript/bin/tsc -p ./", 119 | "compile": "./node_modules/typescript/bin/tsc -watch -p ./", 120 | "postinstall": "node ./node_modules/vscode/bin/install" 121 | }, 122 | "devDependencies": { 123 | "typescript": "^2.0.3", 124 | "@types/node": "~7.0.31", 125 | "mocha": "^3.2.0", 126 | "@types/mocha": "^2.2.34", 127 | "vscode": "^1.1.0" 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as vscode from 'vscode'; 4 | import {findGit, git} from './git'; 5 | import {flow} from './flow'; 6 | import {fail} from './fail' 7 | 8 | async function runWrapped(fn: (...any) => Thenable, args: any[] = []): Promise { 9 | try { 10 | return await fn(...args); 11 | } catch (e) { 12 | if (!e.handlers && !e.message) { 13 | throw e; 14 | } 15 | 16 | const err: fail.IError = e; 17 | const chosen = await vscode.window.showErrorMessage(err.message, ...(err.handlers || [])); 18 | if (!!chosen) { 19 | return await runWrapped(chosen.cb); 20 | } 21 | return null; 22 | } 23 | } 24 | 25 | async function setup(disposables: vscode.Disposable[]) { 26 | const pathHint = vscode.workspace.getConfiguration('git').get('path'); 27 | git.info = await findGit(pathHint); 28 | vscode.window.setStatusBarMessage( 29 | 'gitflow using git executable: ' + git.info.path + ' with version ' + 30 | git.info.version, 5000); 31 | const commands = [ 32 | vscode.commands.registerCommand( 33 | 'gitflow.initialize', 34 | async () => { 35 | await runWrapped(flow.initialize); 36 | }), 37 | vscode.commands.registerCommand( 38 | 'gitflow.featureStart', 39 | async () => { 40 | await runWrapped(flow.requireFlowEnabled); 41 | await runWrapped(flow.feature.precheck); 42 | const name = await vscode.window.showInputBox({ 43 | placeHolder: 'my-awesome-feature', 44 | prompt: 'A new name for your feature', 45 | }); 46 | if (!name) { return; } 47 | await runWrapped(flow.feature.start, [name.split(' ').join('-'), 'feature']); 48 | }), 49 | vscode.commands.registerCommand( 50 | 'gitflow.featureRebase', 51 | async () => { 52 | await runWrapped(flow.feature.rebase, ['feature']); 53 | }), 54 | vscode.commands.registerCommand( 55 | 'gitflow.featureFinish', 56 | async () => { 57 | await runWrapped(flow.feature.finish, ['feature']); 58 | }), 59 | vscode.commands.registerCommand( 60 | 'gitflow.bugfixStart', 61 | async () => { 62 | await runWrapped(flow.requireFlowEnabled); 63 | await runWrapped(flow.feature.precheck); 64 | const name = await vscode.window.showInputBox({ 65 | placeHolder: 'my-awesome-bugfix', 66 | prompt: 'A new name for your bugfix', 67 | }); 68 | if (!name) { return; } 69 | await runWrapped(flow.feature.start, [name.split(' ').join('-'), 'bugfix']); 70 | }), 71 | vscode.commands.registerCommand( 72 | 'gitflow.bugfixRebase', 73 | async () => { 74 | await runWrapped(flow.feature.rebase, ['bugfix']); 75 | }), 76 | vscode.commands.registerCommand( 77 | 'gitflow.bugfixFinish', 78 | async () => { 79 | await runWrapped(flow.feature.finish, ['bugfix']); 80 | }), 81 | vscode.commands.registerCommand( 82 | 'gitflow.releaseStart', 83 | async () => { 84 | await runWrapped(flow.requireFlowEnabled); 85 | await runWrapped(flow.release.precheck); 86 | const guessedVersion = await runWrapped( 87 | flow.release.guess_new_version) || ''; 88 | const name = await vscode.window.showInputBox({ 89 | placeHolder: guessedVersion, 90 | prompt: 'The name of the release', 91 | value: guessedVersion, 92 | }); 93 | if (!name) { return; } 94 | await runWrapped(flow.release.start, [name.split(' ').join('-')]); 95 | }), 96 | vscode.commands.registerCommand( 97 | 'gitflow.releaseFinish', 98 | async () => { 99 | await runWrapped(flow.release.finish); 100 | }), 101 | vscode.commands.registerCommand( 102 | 'gitflow.hotfixStart', 103 | async () => { 104 | await runWrapped(flow.requireFlowEnabled); 105 | const guessedVersion = await runWrapped( 106 | flow.hotfix.guess_new_version) || ''; 107 | const name = await vscode.window.showInputBox({ 108 | placeHolder: guessedVersion, 109 | prompt: 'The name of the hotfix version', 110 | value: guessedVersion, 111 | }); 112 | if (!name) { return; } 113 | await runWrapped(flow.hotfix.start, [name.split(' ').join('-')]); 114 | }), 115 | vscode.commands.registerCommand( 116 | 'gitflow.hotfixFinish', 117 | async () => { 118 | await runWrapped(flow.hotfix.finish); 119 | }), 120 | ]; 121 | // add disposable 122 | disposables.push(...commands); 123 | } 124 | 125 | export function activate(context: vscode.ExtensionContext) { 126 | const disposables: vscode.Disposable[] = []; 127 | context.subscriptions.push(new vscode.Disposable( 128 | () => vscode.Disposable.from(...disposables).dispose())); 129 | 130 | setup(disposables).catch((err) => console.error(err)); 131 | } 132 | 133 | export function 134 | // tslint:disable-next-line:no-empty 135 | deactivate() {} 136 | -------------------------------------------------------------------------------- /src/git.ts: -------------------------------------------------------------------------------- 1 | import * as cp from 'child_process'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | 5 | import {cmd} from './cmd' 6 | import {fail} from './fail'; 7 | 8 | // Taken from 9 | // https://github.com/Microsoft/vscode/blob/cda3584a99d2832ab9d478c6b65ea45c96fe00c9/extensions/git/src/util.ts 10 | export function denodeify(fn: Function): (...args) => Promise { 11 | return (...args) => new Promise( 12 | (c, e) => fn(...args, (err, r) => err ? e(err) : c(r))); 13 | } 14 | 15 | const readdir = denodeify(fs.readdir); 16 | 17 | // Taken from 18 | // https://github.com/Microsoft/vscode/blob/cda3584a99d2832ab9d478c6b65ea45c96fe00c9/extensions/git/src/git.ts 19 | export interface IGit { 20 | path: string; 21 | version: string; 22 | } 23 | 24 | function parseVersion(raw: string): string { 25 | return raw.replace(/^git version /, ''); 26 | } 27 | 28 | function findSpecificGit(path: string): Promise { 29 | return new Promise((c, e) => { 30 | const buffers: Buffer[] = []; 31 | const child = cp.spawn(path, ['--version']); 32 | child.stdout.on('data', (b: Buffer) => buffers.push(b)); 33 | child.on('error', e); 34 | child.on( 35 | 'exit', 36 | code => code ? e(new Error('Not found')) : c({ 37 | path, 38 | version: parseVersion(Buffer.concat(buffers).toString('utf8').trim()) 39 | })); 40 | }); 41 | } 42 | 43 | function findGitDarwin(): Promise { 44 | return new Promise((c, e) => { 45 | cp.exec('which git', (err, gitPathBuffer) => { 46 | if (err) { 47 | return e('git not found'); 48 | } 49 | 50 | const path = gitPathBuffer.toString().replace(/^\s+|\s+$/g, ''); 51 | 52 | function getVersion(path: string) { 53 | // make sure git executes 54 | cp.exec('git --version', (err: Error, stdout: string) => { 55 | if (err) { 56 | return e('git not found'); 57 | } 58 | 59 | return c( 60 | {path, version: parseVersion(stdout.trim())}); 61 | }); 62 | } 63 | 64 | if (path !== '/usr/bin/git') { 65 | return getVersion(path); 66 | } 67 | 68 | // must check if XCode is installed 69 | cp.exec('xcode-select -p', (err: any) => { 70 | if (err && err.code === 2) { 71 | // git is not installed, and launching /usr/bin/git 72 | // will prompt the user to install it 73 | 74 | return e('git not found'); 75 | } 76 | 77 | getVersion(path); 78 | }); 79 | }); 80 | }); 81 | } 82 | 83 | function findSystemGitWin32(base: string): Promise { 84 | if (!base) { 85 | return Promise.reject('Not found'); 86 | } 87 | 88 | return findSpecificGit(path.join(base, 'Git', 'cmd', 'git.exe')); 89 | } 90 | 91 | function findGitHubGitWin32(): Promise { 92 | const github = path.join(process.env['LOCALAPPDATA'], 'GitHub'); 93 | 94 | return readdir(github).then(children => { 95 | const git = children.filter(child => /^PortableGit/.test(child))[0]; 96 | 97 | if (!git) { 98 | return Promise.reject('Not found'); 99 | } 100 | 101 | return findSpecificGit(path.join(github, git, 'cmd', 'git.exe')); 102 | }); 103 | } 104 | 105 | function id(val: T): T { 106 | return val; 107 | } 108 | 109 | function findGitWin32(): Promise { 110 | return findSystemGitWin32(process.env['ProgramW6432']) 111 | .then(id, () => findSystemGitWin32(process.env['ProgramFiles(x86)'])) 112 | .then(id, () => findSystemGitWin32(process.env['ProgramFiles'])) 113 | .then(id, () => findSpecificGit('git')) 114 | .then(id, () => findGitHubGitWin32()); 115 | } 116 | 117 | export function findGit(hint: string|undefined): Promise { 118 | var first = hint ? findSpecificGit(hint) : Promise.reject(null); 119 | 120 | return first.then(id, () => { 121 | switch (process.platform) { 122 | case 'darwin': 123 | return findGitDarwin(); 124 | case 'win32': 125 | return findGitWin32(); 126 | default: 127 | return findSpecificGit('git'); 128 | } 129 | }); 130 | } 131 | 132 | export namespace git { 133 | export let info: IGit; 134 | /** 135 | * Represents a git remote 136 | */ 137 | export class RemoteRef { 138 | constructor(public name: string) {} 139 | 140 | /// Create a remote reference from a remote's name 141 | public static fromName(name: string) { 142 | return new RemoteRef(name); 143 | } 144 | } 145 | 146 | export namespace config { 147 | /// Get a git config value 148 | export async function get(setting: string): Promise { 149 | const result = await cmd.execute(info.path, ['config', '--get', setting]); 150 | if (result.retc) { 151 | return null; 152 | } 153 | return result.stdout.trim(); 154 | } 155 | 156 | /// Set a git config value 157 | export async function set(setting: string, value: any): Promise { 158 | const result = await cmd.execute(info.path, ['config', setting, value]); 159 | return result.retc; 160 | } 161 | } 162 | 163 | export class TagRef { 164 | constructor(public name: string) {} 165 | 166 | /** 167 | * Get a tag reference by name 168 | */ 169 | public static fromName(name: string) { 170 | return new TagRef(name); 171 | } 172 | 173 | /** 174 | * Parse a list of tags returned by git 175 | */ 176 | public static parseListing(output: string): TagRef[] { 177 | return output.replace('\r\n', '\n') 178 | .trim() 179 | .split('\n') 180 | .filter(line => !!line.length) 181 | .map(line => line.trim()) 182 | .reduce( 183 | (acc, name) => { 184 | if (!(name in acc)) acc.push(name); 185 | return acc; 186 | }, 187 | [] as string[]) 188 | .map(name => new TagRef(name)); 189 | } 190 | 191 | /** 192 | * Get a list of all tags 193 | */ 194 | public static async all() { 195 | const result = await cmd.executeRequired(info.path, ['tag', '-l']); 196 | return TagRef.parseListing(result.stdout); 197 | } 198 | 199 | /** 200 | * Get latest tag 201 | */ 202 | public async latest(): Promise { 203 | let last_tag = ''; 204 | const a_tag_exists = await cmd.executeRequired( 205 | info.path, ['tag', '-l']); 206 | if (a_tag_exists.stdout.trim()) { 207 | const latest_tagged_commit = await cmd.executeRequired( 208 | info.path, ['rev-list', '--tags', '--max-count=1']); 209 | const result = await cmd.executeRequired( 210 | info.path, 211 | ['describe', '--tags', latest_tagged_commit.stdout.trim()]); 212 | last_tag = result.stdout.trim(); 213 | } 214 | return last_tag; 215 | } 216 | 217 | /** 218 | * Check if the tag exists 219 | */ 220 | public async exists(): Promise { 221 | const self: TagRef = this; 222 | const all = await TagRef.all(); 223 | return all.some(tag => tag.name === self.name); 224 | } 225 | } 226 | 227 | export class BranchRef { 228 | constructor(public name: string) {} 229 | 230 | /** 231 | * Create a branch reference from a string name 232 | */ 233 | public static fromName(name: string) { 234 | return new BranchRef(name); 235 | } 236 | 237 | /** 238 | * Parse a list of branches returned by git stdout 239 | */ 240 | public static parseListing(output: string): BranchRef[] { 241 | return output.replace('\r\n', '\n') 242 | .trim() 243 | .split('\n') 244 | .filter(line => !!line.length) 245 | .filter(line => line !== 'no branch') 246 | .map(line => line.trim()) 247 | .map(line => line.replace(/^\* /, '')) 248 | .reduce( 249 | (acc, name) => { 250 | if (!(name in acc)) acc.push(name); 251 | return acc; 252 | }, 253 | [] as string[]) 254 | .map(name => new BranchRef(name)); 255 | } 256 | 257 | /** 258 | * Get a list of branches available in the current directory 259 | */ 260 | public static async all() { 261 | const local_result = 262 | await cmd.execute(info.path, ['branch', '--no-color']); 263 | const local_stdout = local_result.stdout; 264 | const remote_result = 265 | await cmd.execute(info.path, ['branch', '-r', '--no-color']); 266 | const remote_stdout = remote_result.stdout; 267 | const filter = 268 | (output) => { 269 | return output; 270 | } 271 | 272 | return BranchRef.parseListing( 273 | local_stdout + remote_stdout) 274 | } 275 | 276 | /** 277 | * Test if a given branch exists 278 | */ 279 | public async exists(): Promise { 280 | const self: BranchRef = this; 281 | const all = await BranchRef.all(); 282 | return !!(all.find((branch: BranchRef) => branch.name === self.name)); 283 | } 284 | 285 | /** 286 | * Get the git hash that the branch points to 287 | */ 288 | public async ref(): Promise { 289 | const self: BranchRef = this; 290 | const result = await cmd.execute(info.path, ['rev-parse', self.name]); 291 | return result.stdout.trim(); 292 | } 293 | 294 | /** 295 | * Get the name of the branch at a remote 296 | */ 297 | public remoteAt(remote: RemoteRef): BranchRef { 298 | return BranchRef.fromName(`${remote.name}/${this.name}`); 299 | } 300 | }; 301 | 302 | /** 303 | * Get a reference to the currently checked out branch 304 | */ 305 | export async function currentBranch(): Promise { 306 | const result = await cmd.executeRequired( 307 | info.path, ['rev-parse', '--abbrev-ref', 'HEAD']); 308 | const name = result.stdout.trim(); 309 | if (name === 'HEAD') { 310 | // We aren't attached to a branch at the moment 311 | return null; 312 | } 313 | return BranchRef.fromName(name); 314 | } 315 | 316 | /** 317 | * Pull updates from the given ``remote`` for ``branch`` 318 | */ 319 | export async function pull(remote: RemoteRef, branch: BranchRef): 320 | Promise { 321 | const result = 322 | await cmd.execute(info.path, ['pull', remote.name, branch.name]); 323 | if (result.retc !== 0) { 324 | fail.error({message: 'Failed to pull from remote. See git output'}); 325 | } 326 | return result.retc; 327 | } 328 | 329 | /** 330 | * Push updates to ``remote`` at ``branch`` 331 | */ 332 | export async function push(remote: RemoteRef, branch: BranchRef): 333 | Promise { 334 | const result = 335 | await cmd.execute(info.path, ['push', remote.name, branch.name]); 336 | if (result.retc !== 0) { 337 | fail.error({ 338 | message: 'Failed to push to remote. See git output', 339 | }); 340 | } 341 | return result.retc; 342 | } 343 | 344 | /** 345 | * Check if we have any unsaved changes 346 | */ 347 | export async function isClean(): Promise { 348 | const diff_res = await cmd.execute(info.path, [ 349 | 'diff', '--no-ext-diff', '--ignore-submodules', '--quiet', '--exit-code' 350 | ]); 351 | if (!!diff_res.retc) { 352 | return false; 353 | } 354 | const diff_index_res = await cmd.execute(info.path, [ 355 | 'diff-index', '--cached', '--quiet', '--ignore-submodules', 'HEAD', '--' 356 | ]); 357 | if (!!diff_index_res.retc) { 358 | return false; 359 | } 360 | return true; 361 | } 362 | 363 | /** 364 | * Detect if the branch "subject" was merged into "base" 365 | */ 366 | export async function isMerged(subject: BranchRef, base: BranchRef) { 367 | const result = await cmd.executeRequired( 368 | info.path, ['branch', '--no-color', '--contains', subject.name]); 369 | const branches = BranchRef.parseListing(result.stdout); 370 | return branches.some((br) => br.name === base.name); 371 | } 372 | 373 | /** 374 | * Checkout the given branch 375 | */ 376 | export function checkout(branch: BranchRef) { 377 | return checkoutRef(branch.name); 378 | } 379 | 380 | /** 381 | * Checkout the given git hash 382 | */ 383 | export function checkoutRef(ref: string) { 384 | return cmd.executeRequired(info.path, ['checkout', ref]); 385 | } 386 | 387 | /** 388 | * Merge one branch into the currently checked out branch 389 | */ 390 | export function merge(other: BranchRef) { 391 | return cmd.executeRequired(info.path, ['merge', '--no-ff', other.name]); 392 | } 393 | 394 | interface IRebaseParameters { 395 | branch: BranchRef; 396 | onto: BranchRef; 397 | } 398 | ; 399 | 400 | /** 401 | * Rebase one branch onto another 402 | */ 403 | export function rebase(args: IRebaseParameters) { 404 | return cmd.executeRequired( 405 | info.path, ['rebase', args.onto.name, args.branch.name]); 406 | } 407 | 408 | /** 409 | * Require that two branches point to the same commit. 410 | * 411 | * If given ``true`` for ``offer_pull``, will offer the use the ability 412 | * to quickly pull from 'origin' onto the ``a`` branch. 413 | */ 414 | export async function 415 | requireEqual(a: BranchRef, b: BranchRef, offer_pull: boolean = false) { 416 | const aref = await a.ref(); 417 | const bref = await b.ref(); 418 | 419 | if (aref !== bref) { 420 | fail.error({ 421 | message: `Branch "${a.name}" has diverged from ${b.name}`, 422 | handlers: !offer_pull ? [] : 423 | [ 424 | { 425 | title: 'Pull now', 426 | cb: async function() { 427 | git.pull(primaryRemote(), a); 428 | }, 429 | }, 430 | ], 431 | }); 432 | } 433 | } 434 | 435 | export async function requireClean() { 436 | if (!(await isClean())) { 437 | fail.error({ 438 | message: 439 | 'Unsaved changes detected. Please commit or stash your changes and try again' 440 | }); 441 | } 442 | } 443 | 444 | export function primaryRemote() { 445 | return RemoteRef.fromName('origin'); 446 | } 447 | } 448 | -------------------------------------------------------------------------------- /src/flow.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import * as vscode from 'vscode'; 4 | 5 | import * as path from 'path'; 6 | 7 | import {fail} from './fail'; 8 | import {git} from './git'; 9 | import {cmd} from './cmd'; 10 | import {fs} from './fs'; 11 | import {config} from './config'; 12 | 13 | const withProgress = vscode.window.withProgress; 14 | 15 | export namespace flow { 16 | export const gitDir = path.join(vscode.workspace.rootPath!, '.git'); 17 | export const gitflowDir = path.join(gitDir, '.gitflow'); 18 | 19 | /** 20 | * Get the release branch prefix 21 | */ 22 | export function releasePrefix() { 23 | return git.config.get('gitflow.prefix.release'); 24 | } 25 | 26 | /** 27 | * Get the tag prefix 28 | */ 29 | export function tagPrefix() { 30 | return git.config.get('gitflow.prefix.versiontag'); 31 | } 32 | 33 | /** 34 | * Get develop branch name 35 | */ 36 | export function developBranch(): Promise { 37 | return git.config.get('gitflow.branch.develop') 38 | .then(git.BranchRef.fromName); 39 | } 40 | 41 | /** 42 | * Get the master branch name 43 | */ 44 | export function masterBranch(): Promise { 45 | return git.config.get('gitflow.branch.master').then(git.BranchRef.fromName); 46 | } 47 | 48 | export async function flowEnabled(): Promise { 49 | const master = await git.config.get('gitflow.branch.master'); 50 | const develop = await git.config.get('gitflow.branch.develop'); 51 | return !!(master) && !!(develop); 52 | } 53 | 54 | export async function requireFlowEnabled() { 55 | if (!(await flowEnabled())) { 56 | // Ask the user to enable gitflow 57 | fail.error({ 58 | message: 'Gitflow is not initialized for this project', 59 | handlers: [{ 60 | title: 'Enable now', 61 | cb: flow.initialize, 62 | }] 63 | }) 64 | } 65 | } 66 | 67 | export async function 68 | requireNoSuchBranch(br: git.BranchRef, err: fail.IError) { 69 | if (await br.exists()) { 70 | fail.error(err); 71 | } 72 | } 73 | 74 | export function throwNotInitializedError(): never { 75 | throw fail.error({ 76 | message: 'Gitflow has not been initialized for this repository', 77 | handlers: [{ 78 | title: 'Initialize', 79 | cb() { 80 | return flow.initialize(); 81 | } 82 | }] 83 | }); 84 | } 85 | 86 | export async function initialize() { 87 | console.log('Init'); 88 | if (await flowEnabled()) { 89 | const do_reinit = !!(await vscode.window.showWarningMessage( 90 | 'Gitflow has already been initialized for this repository. Would you like to re-initialize?', 91 | 'Yes')); 92 | if (!do_reinit) return; 93 | } 94 | 95 | const branchNonEmpty = str => !!str ? '' : 'A branch name is required' 96 | const master_name = await vscode.window.showInputBox({ 97 | prompt: 'Enter a name for the production branch', 98 | value: config.default_production, 99 | validateInput: branchNonEmpty, 100 | }); 101 | if (!master_name) return; 102 | const develop_name = await vscode.window.showInputBox({ 103 | prompt: 'Enter a name for the development branch', 104 | value: config.default_development, 105 | validateInput: branchNonEmpty, 106 | }); 107 | if (!develop_name) return; 108 | if (master_name === develop_name) { 109 | fail.error({ 110 | message: 'Production and development branches must differ', 111 | }); 112 | } 113 | 114 | const develop = git.BranchRef.fromName(develop_name); 115 | const master = git.BranchRef.fromName(master_name); 116 | 117 | const remote_develop = git.BranchRef.fromName('origin/' + develop_name); 118 | const remote_master = git.BranchRef.fromName('origin/' + master_name); 119 | 120 | // Check if the repository needs to be initialized before we proceed 121 | if (!!(await cmd.execute(git.info.path, [ 122 | 'rev-parse', '--quiet', '--verify', 'HEAD' 123 | ])).retc) { 124 | await cmd.executeRequired( 125 | git.info.path, ['symbolic-ref', 'HEAD', `refs/heads/${master.name}`]); 126 | await cmd.executeRequired( 127 | git.info.path, 128 | ['commit', '--allow-empty', '--quiet', '-m', 'Initial commit']); 129 | } 130 | 131 | // Ensure the develop branch exists 132 | if (!(await develop.exists())) { 133 | if (await remote_develop.exists()) { 134 | // If there is a remote with the branch, set up our local copy to track 135 | // that one 136 | cmd.executeRequired( 137 | git.info.path, ['branch', develop.name, remote_develop.name]); 138 | } else { 139 | // Otherwise, create it on top of the master branch 140 | cmd.executeRequired( 141 | git.info.path, ['branch', '--no-track', develop.name, master.name]); 142 | } 143 | // Checkout develop since we just created it 144 | await git.checkout(develop); 145 | } 146 | 147 | // Create the branch prefixes and store those in git config 148 | for (const what of ['bugfix', 'feature', 'release', 'hotfix', 'support']) { 149 | const prefix = await vscode.window.showInputBox({ 150 | prompt: `Enter a prefix for "${what}" branches`, 151 | value: `${what}/`, 152 | validateInput: branchNonEmpty, 153 | }); 154 | if (!prefix) return; 155 | await git.config.set(`gitflow.prefix.${what}`, prefix); 156 | } 157 | 158 | const version_tag_prefix = await vscode.window.showInputBox({ 159 | prompt: 'Enter a prefix for version tags (optional)', 160 | }); 161 | if (version_tag_prefix === null) return; 162 | await git.config.set('gitflow.prefix.versiontag', version_tag_prefix); 163 | 164 | // Set the main branches, and gitflow is officially 'enabled' 165 | await git.config.set('gitflow.branch.master', master.name); 166 | await git.config.set('gitflow.branch.develop', develop.name); 167 | 168 | console.assert(await flowEnabled()); 169 | 170 | vscode.window.showInformationMessage( 171 | 'Gitflow has been initialized for this repository!'); 172 | } 173 | } 174 | 175 | export namespace flow.feature { 176 | /** 177 | * Get the feature/bugfix branch prefix 178 | */ 179 | export function prefix(branchType: string) { 180 | return git.config.get(`gitflow.prefix.${branchType}`); 181 | } 182 | 183 | /** 184 | * Get the current feature/bugfix branch as well as its name. 185 | */ 186 | export async function current( 187 | msg: string = 'Not working on a feature or bugfix branch', branchType: string) { 188 | const current_branch = await git.currentBranch(); 189 | const prefix = await feature.prefix(branchType); 190 | if (!prefix) { 191 | throw throwNotInitializedError(); 192 | } 193 | if (!current_branch || !current_branch.name.startsWith(prefix)) { 194 | throw fail.error({message: msg}); 195 | } 196 | const name = current_branch.name.substr(prefix.length); 197 | return {branch: current_branch, name: name}; 198 | } 199 | 200 | export async function precheck() { 201 | const local_develop = await developBranch(); 202 | const remote_develop = 203 | git.BranchRef.fromName(`origin/${local_develop.name}`); 204 | const local_ref = await local_develop.ref(); 205 | if (await remote_develop.exists()) { 206 | await git.requireEqual(local_develop, remote_develop, true); 207 | } 208 | } 209 | 210 | export async function start(feature_name: string, branchType: string) { 211 | console.assert(!!feature_name); 212 | await requireFlowEnabled(); 213 | const prefix = await feature.prefix(branchType); 214 | if (!prefix) { 215 | throw throwNotInitializedError(); 216 | } 217 | const new_branch = git.BranchRef.fromName(`${prefix}${feature_name}`); 218 | await requireNoSuchBranch( 219 | new_branch, {message: `The ${branchType} "${feature_name}" already exists`}); 220 | 221 | // Create our new branch 222 | const local_develop = await developBranch(); 223 | await cmd.executeRequired( 224 | git.info.path, ['checkout', '-b', new_branch.name, local_develop.name]); 225 | vscode.window.showInformationMessage( 226 | `New branch "${new_branch.name}" was created`); 227 | } 228 | 229 | /** 230 | * Rebase the current feature branch on develop 231 | */ 232 | export async function rebase(branchType: string) { 233 | await requireFlowEnabled(); 234 | const {branch: feature_branch} = await current( 235 | `You must checkout the ${branchType} branch you wish to rebase on develop`, branchType); 236 | 237 | const remote = feature_branch.remoteAt(git.primaryRemote()); 238 | const develop = await developBranch(); 239 | if (await remote.exists() && !(await git.isMerged(remote, develop))) { 240 | const do_rebase = !!(await vscode.window.showWarningMessage( 241 | `A remote branch for ${feature_branch.name} exists, and rebasing ` + 242 | `will rewrite history for this branch that may be visible to ` + 243 | `other users!`, 244 | 'Rebase anyway')); 245 | if (!do_rebase) return; 246 | } 247 | 248 | await git.requireClean(); 249 | const result = await git.rebase({branch: feature_branch, onto: develop}); 250 | if (result.retc) { 251 | const abort_result = 252 | await cmd.executeRequired(git.info.path, ['rebase', '--abort']); 253 | fail.error({ 254 | message: `Rebase command failed with exit code ${result.retc}. ` + 255 | `The rebase has been aborted: Please perform this rebase from ` + 256 | `the command line and resolve the appearing errors.` 257 | }); 258 | } 259 | await vscode.window.showInformationMessage( 260 | `${feature_branch.name} has been rebased onto ${develop.name}`); 261 | } 262 | 263 | export async function finish(branchType: string) { 264 | return withProgress({ 265 | location: vscode.ProgressLocation.Window, 266 | title: `Finishing ${branchType}`, 267 | }, async (pr) => { 268 | pr.report({message: 'Getting current branch...'}) 269 | const {branch: feature_branch, name: feature_name} = await current( 270 | `You must checkout the ${branchType} branch you wish to finish`, branchType); 271 | 272 | pr.report({message: 'Checking for cleanliness...'}) 273 | const is_clean = await git.isClean(); 274 | 275 | pr.report({message: 'Checking for incomplete merge...'}) 276 | const merge_base_file = path.join(gitflowDir, 'MERGE_BASE'); 277 | if (await fs.exists(merge_base_file)) { 278 | const merge_base = git.BranchRef.fromName( 279 | (await fs.readFile(merge_base_file)).toString()); 280 | if (is_clean) { 281 | // The user must have resolved the conflict themselves, so 282 | // all we need to do is delete the merge file 283 | await fs.remove(merge_base_file); 284 | if (await git.isMerged(feature_branch, merge_base)) { 285 | // The user already merged this feature branch. We'll just exit! 286 | await finishCleanup(feature_branch, branchType); 287 | return; 288 | } 289 | } else { 290 | // They have an unresolved merge conflict. Tell them what they must do 291 | fail.error({ 292 | message: 293 | `You have merge conflicts! Resolve them before trying to finish ${branchType} branch.` 294 | }); 295 | } 296 | } 297 | 298 | await git.requireClean(); 299 | 300 | pr.report({message: 'Checking remotes...'}) 301 | const all_branches = await git.BranchRef.all(); 302 | // Make sure that the local feature and the remote feature haven't diverged 303 | const remote_branch = 304 | all_branches.find(br => br.name === 'origin/' + feature_branch.name); 305 | if (remote_branch) { 306 | await git.requireEqual(feature_branch, remote_branch, true); 307 | } 308 | // Make sure the local develop and remote develop haven't diverged either 309 | const develop = await developBranch(); 310 | const remote_develop = git.BranchRef.fromName('origin/' + develop.name); 311 | if (await remote_develop.exists()) { 312 | await git.requireEqual(develop, remote_develop, true); 313 | } 314 | 315 | pr.report({message: `Merging ${feature_branch.name} into ${develop}...`}); 316 | // Switch to develop and merge in the feature branch 317 | await git.checkout(develop); 318 | const result = await cmd.execute( 319 | git.info.path, ['merge', '--no-ff', feature_branch.name]); 320 | if (result.retc) { 321 | // Merge conflict. Badness 322 | await fs.writeFile(gitflowDir, develop.name); 323 | fail.error({ 324 | message: `There were conflicts while merging into ${develop.name 325 | }. Fix the issues before trying to finish the ${branchType} branch` 326 | }); 327 | } 328 | pr.report({message: 'Cleaning up...'}); 329 | await finishCleanup(feature_branch, branchType); 330 | }); 331 | } 332 | 333 | async function finishCleanup(branch: git.BranchRef, branchType: string) { 334 | console.assert(await branch.exists()); 335 | console.assert(await git.isClean()); 336 | const origin = git.RemoteRef.fromName('origin'); 337 | const remote = git.BranchRef.fromName(origin.name + '/' + branch.name); 338 | if (config.deleteBranchOnFinish) { 339 | if (config.deleteRemoteBranches && await remote.exists()) { 340 | // Delete the branch on the remote 341 | await git.push( 342 | git.RemoteRef.fromName('origin'), 343 | git.BranchRef.fromName(`:refs/heads/${branch.name}`)); 344 | } 345 | await cmd.executeRequired(git.info.path, ['branch', '-d', branch.name]); 346 | } 347 | vscode.window.showInformationMessage( 348 | `${branchType.substring(0, 1).toUpperCase()}${branchType.substring(1)} branch ${branch.name} has been closed`); 349 | } 350 | } 351 | 352 | export namespace flow.release { 353 | export async function current() { 354 | const branches = await git.BranchRef.all(); 355 | const prefix = await releasePrefix(); 356 | if (!prefix) { 357 | throw throwNotInitializedError(); 358 | } 359 | return branches.find(br => br.name.startsWith(prefix)); 360 | } 361 | 362 | export async function precheck() { 363 | await git.requireClean(); 364 | 365 | const develop = await developBranch(); 366 | const remote_develop = develop.remoteAt(git.primaryRemote()); 367 | if (await remote_develop.exists()) { 368 | await git.requireEqual(develop, remote_develop); 369 | } 370 | } 371 | 372 | /** 373 | * Get the tag for a new release branch 374 | */ 375 | export async function guess_new_version() { 376 | const tag = git.TagRef.fromName("_start_new_release"); 377 | const tag_prefix = await tagPrefix() || ''; 378 | let version_tag = await tag.latest() || '0.0.0'; 379 | version_tag = version_tag.replace(tag_prefix, ''); 380 | if (version_tag.match(/^\d+\.\d+\.\d+$/)) { 381 | let version_numbers = version_tag.split('.'); 382 | version_numbers[1] = String(Number(version_numbers[1]) + 1); 383 | version_numbers[2] = "0"; 384 | version_tag = version_numbers.join('.'); 385 | } 386 | return version_tag; 387 | } 388 | 389 | export async function start(name: string) { 390 | await requireFlowEnabled(); 391 | const current_release = await release.current(); 392 | if (!!current_release) { 393 | fail.error({ 394 | message: `There is an existing release branch "${current_release.name 395 | }". Finish that release before starting a new one.` 396 | }); 397 | } 398 | 399 | const tag = git.TagRef.fromName(name); 400 | if (await tag.exists()) { 401 | fail.error({ 402 | message: `The tag "${name 403 | }" is an existing tag. Please chose another release name.` 404 | }); 405 | } 406 | 407 | const prefix = await releasePrefix(); 408 | const new_branch = git.BranchRef.fromName(`${prefix}${name}`); 409 | const develop = await developBranch(); 410 | await cmd.executeRequired( 411 | git.info.path, ['checkout', '-b', new_branch.name, develop.name]); 412 | await vscode.window.showInformationMessage( 413 | `New branch ${new_branch.name} has been created. ` + 414 | `Now is the time to update your version numbers and fix any ` + 415 | `last minute bugs.`); 416 | } 417 | 418 | export async function finish() { 419 | await requireFlowEnabled(); 420 | const prefix = await releasePrefix(); 421 | if (!prefix) { 422 | throw throwNotInitializedError(); 423 | } 424 | const current_release = await release.current(); 425 | if (!current_release) { 426 | throw fail.error({message: 'No active release branch to finish'}); 427 | } 428 | await finalizeWithBranch(prefix, current_release, finish); 429 | } 430 | 431 | export async function finalizeWithBranch( 432 | rel_prefix: string, branch: git.BranchRef, reenter: Function) { 433 | return withProgress({ 434 | location: vscode.ProgressLocation.Window, 435 | title: 'Finishing release branch' 436 | }, async (pr) => { 437 | await requireFlowEnabled(); 438 | pr.report({message: 'Getting current branch...'}); 439 | const current_branch = await git.currentBranch(); 440 | if (!current_branch) { 441 | throw fail.error({message: 'Unable to detect a current git branch.'}); 442 | } 443 | if (current_branch.name !== branch.name) { 444 | fail.error({ 445 | message: `You are not currently on the "${branch.name}" branch`, 446 | handlers: [{ 447 | title: `Checkout ${branch.name} and continue.`, 448 | cb: async function() { 449 | await git.checkout(branch); 450 | await reenter(); 451 | } 452 | }] 453 | }); 454 | } 455 | 456 | pr.report({message: 'Checking cleanliness...'}); 457 | await git.requireClean(); 458 | 459 | pr.report({message: 'Checking remotes...'}); 460 | const master = await masterBranch(); 461 | const remote_master = master.remoteAt(git.primaryRemote()); 462 | if (await remote_master.exists()) { 463 | await git.requireEqual(master, remote_master); 464 | } 465 | 466 | const develop = await developBranch(); 467 | const remote_develop = develop.remoteAt(git.primaryRemote()); 468 | if (await remote_develop.exists()) { 469 | await git.requireEqual(develop, remote_develop); 470 | } 471 | 472 | // Get the name of the tag we will use. Default is the branch's flow name 473 | pr.report({message: 'Getting a tag message...'}); 474 | const tag_message = await vscode.window.showInputBox({ 475 | prompt: 'Enter a tag message (optional)', 476 | }); 477 | if (tag_message === undefined) return; 478 | 479 | // Now the crux of the logic, after we've done all our sanity checking 480 | pr.report({message: 'Switching to master...'}); 481 | await git.checkout(master); 482 | 483 | // Merge the branch into the master branch 484 | if (!(await git.isMerged(branch, master))) { 485 | pr.report({message: `Merging ${branch} into ${master}...`}); 486 | await git.merge(branch); 487 | } 488 | 489 | // Create a tag for the release 490 | const tag_prefix = await tagPrefix() || ''; 491 | const release_name = tag_prefix.concat(branch.name.substr( 492 | rel_prefix.length)); 493 | pr.report({message: `Tagging ${master}: ${release_name}...`}); 494 | await cmd.executeRequired( 495 | git.info.path, ['tag', '-m', tag_message, release_name, master.name]); 496 | 497 | // Merge the release into develop 498 | pr.report({message: `Checking out ${develop}...`}); 499 | await git.checkout(develop); 500 | if (!(await git.isMerged(branch, develop))) { 501 | pr.report({message: `Merging ${branch} into ${develop}...`}); 502 | await git.merge(branch); 503 | } 504 | 505 | if (config.deleteBranchOnFinish) { 506 | // Delete the release branch 507 | pr.report({message: `Deleting ${branch.name}...`}); 508 | await cmd.executeRequired(git.info.path, ['branch', '-d', branch.name]); 509 | if (config.deleteRemoteBranches && await remote_develop.exists() && 510 | await remote_master.exists()) { 511 | const remote = git.primaryRemote(); 512 | pr.report({message: `Pushing to ${remote.name}/${develop.name}...`}); 513 | await git.push(remote, develop); 514 | pr.report({message: `Pushing to ${remote.name}/${master.name}...`}); 515 | await git.push(remote, master); 516 | const remote_branch = branch.remoteAt(remote); 517 | pr.report({message: `Pushing tag ${release_name}...`}); 518 | cmd.executeRequired(git.info.path, ['push', '--tags', remote.name]); 519 | if (await remote_branch.exists()) { 520 | // Delete the remote branch 521 | pr.report({message: `Deleting remote ${remote.name}/${branch.name}`}); 522 | await git.push(remote, git.BranchRef.fromName(':' + branch.name)); 523 | } 524 | } 525 | } 526 | 527 | vscode.window.showInformationMessage( 528 | `The release "${release_name 529 | }" has been created. You are now on the ${develop.name} branch.`); 530 | }); 531 | } 532 | } 533 | 534 | export namespace flow.hotfix { 535 | /** 536 | * Get the hotfix branch prefix 537 | */ 538 | export function prefix() { 539 | return git.config.get('gitflow.prefix.hotfix'); 540 | } 541 | 542 | /** 543 | * Get the current hotfix branch, or null if there is nonesuch 544 | */ 545 | export async function current() { 546 | const branches = await git.BranchRef.all(); 547 | const prefix = await hotfix.prefix(); 548 | if (!prefix) { 549 | throw throwNotInitializedError(); 550 | } 551 | return branches.find(br => br.name.startsWith(prefix)); 552 | } 553 | 554 | /** 555 | * Get the tag for a new hotfix branch 556 | */ 557 | export async function guess_new_version() { 558 | const tag = git.TagRef.fromName("_start_new_hotfix"); 559 | const tag_prefix = await tagPrefix() || ''; 560 | let version_tag = await tag.latest() || '0.0.0'; 561 | version_tag = version_tag.replace(tag_prefix, ''); 562 | if (version_tag.match(/^\d+\.\d+\.\d+$/)) { 563 | let version_numbers = version_tag.split('.'); 564 | version_numbers[2] = String(Number(version_numbers[2]) + 1); 565 | version_tag = version_numbers.join('.'); 566 | } 567 | return version_tag; 568 | } 569 | 570 | export async function start(name: string) { 571 | await requireFlowEnabled(); 572 | const current_hotfix = await current(); 573 | if (!!current_hotfix) { 574 | fail.error({ 575 | message: `There is an existing hotfix branch "${current_hotfix.name 576 | }". Finish that one first.` 577 | }); 578 | } 579 | 580 | await git.requireClean(); 581 | 582 | const master = await masterBranch(); 583 | const remote_master = master.remoteAt(git.primaryRemote()); 584 | if (await remote_master.exists()) { 585 | await git.requireEqual(master, remote_master); 586 | } 587 | 588 | const tag = git.TagRef.fromName(name); 589 | if (await tag.exists()) { 590 | fail.error({ 591 | message: `The tag "${tag.name 592 | }" is an existing tag. Choose another hotfix name.` 593 | }); 594 | } 595 | 596 | const prefix = await hotfix.prefix(); 597 | const new_branch = git.BranchRef.fromName(`${prefix}${name}`); 598 | if (await new_branch.exists()) { 599 | fail.error( 600 | {message: `"${new_branch.name}" is the name of an existing branch`}); 601 | } 602 | await cmd.executeRequired( 603 | git.info.path, ['checkout', '-b', new_branch.name, master.name]); 604 | } 605 | 606 | export async function finish() { 607 | await requireFlowEnabled(); 608 | const prefix = await hotfix.prefix(); 609 | if (!prefix) { 610 | throw throwNotInitializedError(); 611 | } 612 | const current_hotfix = await hotfix.current(); 613 | if (!current_hotfix) { 614 | throw fail.error({message: 'No active hotfix branch to finish'}); 615 | } 616 | await release.finalizeWithBranch(prefix, current_hotfix, finish); 617 | } 618 | } 619 | -------------------------------------------------------------------------------- /res/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 27 | 31 | 35 | 39 | 43 | 44 | 47 | 51 | 55 | 56 | 59 | 63 | 67 | 71 | 75 | 76 | 79 | 83 | 87 | 91 | 95 | 96 | 98 | 102 | 106 | 107 | 114 | 119 | 120 | 127 | 132 | 133 | 140 | 145 | 146 | 153 | 158 | 159 | 166 | 171 | 172 | 182 | 191 | 202 | 213 | 224 | 235 | 246 | 249 | 255 | 256 | 259 | 266 | 267 | 270 | 276 | 277 | 280 | 287 | 288 | 291 | 297 | 298 | 301 | 308 | 309 | 312 | 318 | 319 | 322 | 328 | 329 | 332 | 339 | 340 | 343 | 350 | 351 | 354 | 361 | 362 | 365 | 372 | 373 | 376 | 382 | 383 | 394 | 395 | 421 | 425 | 426 | 428 | 429 | 431 | image/svg+xml 432 | 434 | 435 | 436 | 437 | 438 | 443 | 450 | 456 | 462 | 468 | 471 | 474 | 479 | 483 | 490 | 497 | 504 | 505 | 513 | 517 | 524 | 531 | 538 | 539 | 547 | 551 | 558 | 565 | 572 | 573 | 581 | 589 | 597 | 602 | 609 | 610 | 618 | 626 | 634 | 642 | 650 | 658 | 659 | 660 | --------------------------------------------------------------------------------