Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@
"description": "Whether svn is enabled",
"default": true
}
},
"svn.path": {
"type": [
"string",
"null"
],
"description": "Path to the svn executable",
"default": null,
"isExecutable": true
}
}
}
Expand Down
33 changes: 26 additions & 7 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
import { ExtensionContext, Disposable, window } from "vscode";
import { Svn } from "./svn";
import { ExtensionContext, Disposable, workspace, window } from "vscode";
import { Svn, findSvn } from "./svn";
import { SvnContentProvider } from "./svnContentProvider";
import { SvnCommands } from "./commands";
import { Model } from "./model";
import { toDisposable } from "./util";

function activate(context: ExtensionContext) {
const disposables: Disposable[] = [];

async function init(context: ExtensionContext, disposables: Disposable[]) {
const outputChannel = window.createOutputChannel("Svn");
disposables.push(outputChannel);

const svn = new Svn();
const config = workspace.getConfiguration('svn');
const enabled = config.get<boolean>('enabled') === true;
const pathHint = config.get<string>('path');

let info = null;
try {
info = await findSvn(pathHint);
} catch (error) {
outputChannel.appendLine(error);
return;
}

const svn = new Svn({svnPath: info.path, version: info.version});
const model = new Model(svn);
const contentProvider = new SvnContentProvider(model);
const commands = new SvnCommands(model);
disposables.push(model);

outputChannel.appendLine("svn-scm is now active!");
outputChannel.appendLine("Using svn " + info.version + " from " + info.path);

context.subscriptions.push(
new Disposable(() => Disposable.from(...disposables).dispose())
Expand All @@ -29,6 +39,15 @@ function activate(context: ExtensionContext) {
toDisposable(() => svn.onOutput.removeListener("log", onOutput))
);
}

function activate(context: ExtensionContext): any {
const disposables: Disposable[] = [];
context.subscriptions.push(new Disposable(() => Disposable.from(...disposables).dispose()));

init(context, disposables)
.catch(err => console.error(err));
}

exports.activate = activate;

// this method is called when your extension is deactivated
Expand Down
177 changes: 175 additions & 2 deletions src/svn.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,194 @@
import { EventEmitter } from "events";
import { window } from "vscode";
import * as cp from "child_process";
import * as iconv from "iconv-lite";
import * as jschardet from "jschardet";
import { EventEmitter } from "events";
import * as path from 'path';

interface CpOptions {
cwd?: string;
encoding?: string;
log?: boolean;
}

export interface ISvn {
path: string;
version: string;
}

function parseVersion(raw: string): string {
const match = raw.match(/(\d+\.\d+\.\d+ \(r\d+\))/);

if(match && match[0]) {
return match[0];
}
return raw.split(/[\r\n]+/)[0];
}

function findSpecificSvn(path: string): Promise<ISvn> {
return new Promise<ISvn>((c, e) => {
const buffers: Buffer[] = [];
const child = cp.spawn(path, ['--version']);
child.stdout.on('data', (b: Buffer) => buffers.push(b));
child.on('error', cpErrorHandler(e));
child.on('exit', code => code ? e(new Error('Not found')) : c({ path, version: parseVersion(Buffer.concat(buffers).toString('utf8').trim()) }));
});
}

function findSvnDarwin(): Promise<ISvn> {
return new Promise<ISvn>((c, e) => {
cp.exec('which svn', (err, svnPathBuffer) => {
if (err) {
return e('svn not found');
}

const path = svnPathBuffer.toString().replace(/^\s+|\s+$/g, '');

function getVersion(path: string) {
// make sure svn executes
cp.exec('svn --version', (err, stdout) => {
if (err) {
return e('svn not found');
}

return c({ path, version: parseVersion(stdout.trim()) });
});
}

if (path !== '/usr/bin/svn') {
return getVersion(path);
}

// must check if XCode is installed
cp.exec('xcode-select -p', (err: any) => {
if (err && err.code === 2) {
// svn is not installed, and launching /usr/bin/svn
// will prompt the user to install it

return e('svn not found');
}

getVersion(path);
});
});
});
}

function findSystemSvnWin32(base: string): Promise<ISvn> {
if (!base) {
return Promise.reject<ISvn>('Not found');
}

return findSpecificSvn(path.join(base, 'TortoiseSVN', 'bin', 'svn.exe'));
}

function findSvnWin32(): Promise<ISvn> {
return findSystemSvnWin32(process.env['ProgramW6432'])
.then(void 0, () => findSystemSvnWin32(process.env['ProgramFiles(x86)']))
.then(void 0, () => findSystemSvnWin32(process.env['ProgramFiles']))
.then(void 0, () => findSpecificSvn('svn'));
}

export function findSvn(hint: string | undefined): Promise<ISvn> {
var first = hint ? findSpecificSvn(hint) : Promise.reject<ISvn>(null);

return first
.then(void 0, () => {
switch (process.platform) {
case 'darwin': return findSvnDarwin();
case 'win32': return findSvnWin32();
default: return findSpecificSvn('svn');
}
})
.then(null, () => Promise.reject(new Error('Svn installation not found.')));
}

function cpErrorHandler(cb: (reason?: any) => void): (reason?: any) => void {
return err => {
if (/ENOENT/.test(err.message)) {
err = new SvnError({
error: err,
message: 'Failed to execute svn (ENOENT)',
svnErrorCode: 'NotASvnRepository'
});
}

cb(err);
};
}

export interface ISvnErrorData {
error?: Error;
message?: string;
stdout?: string;
stderr?: string;
exitCode?: number;
svnErrorCode?: string;
svnCommand?: string;
}

export class SvnError {

error?: Error;
message: string;
stdout?: string;
stderr?: string;
exitCode?: number;
svnErrorCode?: string;
svnCommand?: string;

constructor(data: ISvnErrorData) {
if (data.error) {
this.error = data.error;
this.message = data.error.message;
} else {
this.error = void 0;
}

this.message = this.message || data.message || 'SVN error';
this.stdout = data.stdout;
this.stderr = data.stderr;
this.exitCode = data.exitCode;
this.svnErrorCode = data.svnErrorCode;
this.svnCommand = data.svnCommand;
}

toString(): string {
let result = this.message + ' ' + JSON.stringify({
exitCode: this.exitCode,
svnErrorCode: this.svnErrorCode,
svnCommand: this.svnCommand,
stdout: this.stdout,
stderr: this.stderr
}, null, 2);

if (this.error) {
result += (<any>this.error).stack;
}

return result;
}
}

export interface ISvnOptions {
svnPath: string;
version: string;
}

export class Svn {
private svnPath: string;
private version: string;

private _onOutput = new EventEmitter();
get onOutput(): EventEmitter {
return this._onOutput;
}

constructor(options: ISvnOptions) {
this.svnPath = options.svnPath;
this.version = options.version;
}

private log(output: string): void {
this._onOutput.emit("log", output);
}
Expand All @@ -29,7 +202,7 @@ export class Svn {
this.log(`svn ${args.join(" ")}\n`);
}

let process = cp.spawn("svn", args, options);
let process = cp.spawn(this.svnPath, args, options);

let [exitCode, stdout, stderr] = await Promise.all<any>([
new Promise<number>((resolve, reject) => {
Expand Down