Skip to content
This repository has been archived by the owner on Nov 6, 2020. It is now read-only.

Commit

Permalink
Merge pull request #92 from Microsoft/users/jpricket/0202
Browse files Browse the repository at this point in the history
Adding SCMContentProvider implementation for TFVC
  • Loading branch information
jpricket authored Feb 8, 2017
2 parents 53f175f + cd5a409 commit c2197b2
Show file tree
Hide file tree
Showing 16 changed files with 293 additions and 162 deletions.
15 changes: 8 additions & 7 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,20 @@ import { TfvcSCMProvider } from "./tfvc/tfvcscmprovider";
let _extensionManager: ExtensionManager;
let _scmProvider: TfvcSCMProvider;

export function activate(context: ExtensionContext) {
export async function activate(context: ExtensionContext) {
//TODO: It would be good to have only one ref to Tfvc and Repository via the SCMProvider and pass that into the extention manager here.

// Construct the extension manager that handles Team and Tfvc commands
_extensionManager = new ExtensionManager();
await _extensionManager.Initialize();

// Initialize the SCM provider for TFVC
const disposables: Disposable[] = [];
context.subscriptions.push(new Disposable(() => Disposable.from(...disposables).dispose()));
_scmProvider = new TfvcSCMProvider(this);
_scmProvider = new TfvcSCMProvider(_extensionManager);
_scmProvider.Initialize(disposables)
.catch(err => console.error(err));

//TODO: It would be good to have only one ref to Tfvc and Repository via the SCMProvider and pass that into the extention manager here.

// Construct the extension manager that handles Team and Tfvc commands
_extensionManager = new ExtensionManager();

context.subscriptions.push(commands.registerCommand(CommandNames.GetPullRequests, () => _extensionManager.Team.GetMyPullRequests()));
context.subscriptions.push(commands.registerCommand(CommandNames.Login, () => _extensionManager.Team.Signin()));
context.subscriptions.push(commands.registerCommand(CommandNames.Logout, () => _extensionManager.Team.Signout()));
Expand Down
6 changes: 3 additions & 3 deletions src/extensionmanager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ export class ExtensionManager {
private _teamExtension: TeamExtension;
private _tfvcExtension: TfvcExtension;

constructor() {
this.setupFileSystemWatcherOnConfig();
this.initializeExtension();
public async Initialize(): Promise<void> {
await this.setupFileSystemWatcherOnConfig();
await this.initializeExtension();

// Add the event listener for settings changes, then re-initialized the extension
if (workspace) {
Expand Down
70 changes: 70 additions & 0 deletions src/tfvc/commands/commandhelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,78 @@
"use strict";

import { parseString } from "xml2js";
import { Logger } from "../../helpers/logger";
import { Strings } from "../../helpers/strings";
import { TfvcError, TfvcErrorCodes } from "../tfvcerror";
import { IExecutionResult } from "../interfaces";

export class CommandHelper {
public static RequireStringArgument(argument: string, argumentName: string) {
if (!argument || argument.trim().length === 0) {
throw TfvcError.CreateArgumentMissingError(argumentName);
}
}

public static RequireStringArrayArgument(argument: string[], argumentName: string) {
if (!argument || argument.length === 0) {
throw TfvcError.CreateArgumentMissingError(argumentName);
}
}

public static HasError(result: IExecutionResult, errorPattern: string): boolean {
return new RegExp(errorPattern, "i").test(result.stderr);
}

public static ProcessErrors(command: string, result: IExecutionResult) {
if (result.exitCode) {
let tfvcErrorCode: string = null;
let message: string;

if (/Authentication failed/.test(result.stderr)) {
tfvcErrorCode = TfvcErrorCodes.AuthenticationFailed;
} else if (/workspace could not be determined/.test(result.stderr)) {
tfvcErrorCode = TfvcErrorCodes.NotATfvcRepository;
} else if (/bad config file/.test(result.stderr)) {
tfvcErrorCode = TfvcErrorCodes.BadConfigFile;
} else if (/cannot make pipe for command substitution|cannot create standard input pipe/.test(result.stderr)) {
tfvcErrorCode = TfvcErrorCodes.CantCreatePipe;
} else if (/Repository not found/.test(result.stderr)) {
tfvcErrorCode = TfvcErrorCodes.RepositoryNotFound;
} else if (/unable to access/.test(result.stderr)) {
tfvcErrorCode = TfvcErrorCodes.CantAccessRemote;
} else if (/project collection URL to use could not be determined/i.test(result.stderr)) {
tfvcErrorCode = TfvcErrorCodes.NotATfvcRepository;
message = Strings.NotATfvcRepository;
} else if (/Access denied connecting.*authenticating as OAuth/i.test(result.stderr)) {
tfvcErrorCode = TfvcErrorCodes.AuthenticationFailed;
message = Strings.TokenNotAllScopes;
} else if (/'java' is not recognized as an internal or external command/.test(result.stderr)) {
tfvcErrorCode = TfvcErrorCodes.TfvcNotFound;
message = Strings.TfInitializeFailureError;
} else if (/There is no working folder mapping/i.test(result.stderr)) {
tfvcErrorCode = TfvcErrorCodes.FileNotInMappings;
}

Logger.LogDebug(`TFVC errors: ${result.stderr}`);

return Promise.reject<IExecutionResult>(new TfvcError({
message: message || Strings.TfExecFailedError,
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode,
tfvcErrorCode: tfvcErrorCode,
tfvcCommand: command
}));
}
}

public static GetNewLineCharacter(stdout: string): string {
if (stdout && /\r\n/.test(stdout)) {
return "\r\n";
}
return "\n";
}

public static SplitIntoLines(stdout: string, skipWarnings?: boolean): string[] {
let lines: string[] = stdout.replace(/\r\n/g, "\n").split("\n");
skipWarnings = skipWarnings === undefined ? true : skipWarnings;
Expand Down
9 changes: 4 additions & 5 deletions src/tfvc/commands/findworkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"use strict";

import { IArgumentProvider, IExecutionResult, ITfvcCommand, IWorkspace, IWorkspaceMapping } from "../interfaces";
import { TfvcError } from "../tfvcerror";
import { ArgumentBuilder } from "./argumentbuilder";
import { CommandHelper } from "./commandhelper";

Expand All @@ -19,9 +18,7 @@ export class FindWorkspace implements ITfvcCommand<IWorkspace> {
private _localPath: string;

public constructor(localPath: string) {
if (!localPath) {
throw TfvcError.CreateArgumentMissingError("localPath");
}
CommandHelper.RequireStringArgument(localPath, "localPath");
this._localPath = localPath;
}

Expand All @@ -43,8 +40,10 @@ export class FindWorkspace implements ITfvcCommand<IWorkspace> {
* $/tfsTest_01: D:\tmp\test
*/
public async ParseOutput(executionResult: IExecutionResult): Promise<IWorkspace> {
const stdout = executionResult.stdout;
// Throw if any errors are found in stderr or if exitcode is not 0
CommandHelper.ProcessErrors(this.GetArguments().GetCommand(), executionResult);

const stdout = executionResult.stdout;
if (!stdout) {
return undefined;
}
Expand Down
61 changes: 61 additions & 0 deletions src/tfvc/commands/getfilecontent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
"use strict";

import { TeamServerContext} from "../../contexts/servercontext";
import { IArgumentProvider, IExecutionResult, ITfvcCommand } from "../interfaces";
import { ArgumentBuilder } from "./argumentbuilder";
import { CommandHelper } from "./commandhelper";

/**
* This command calls Print to get the contents of the file at the version provided and returns them as a string
* file.
* <p/>
* This command actually wraps the print command:
* print [/version:<value>] <itemSpec>
*/
export class GetFileContent implements ITfvcCommand<string> {
private _serverContext: TeamServerContext;
private _localPath: string;
private _versionSpec: string;
private _ignoreFileNotFound: boolean;

public constructor(serverContext: TeamServerContext, localPath: string, versionSpec: string, ignoreFileNotFound: boolean) {
CommandHelper.RequireStringArgument(localPath, "localPath");

this._serverContext = serverContext;
this._localPath = localPath;
this._versionSpec = versionSpec;
this._ignoreFileNotFound = ignoreFileNotFound;
}

public GetArguments(): IArgumentProvider {
let builder: ArgumentBuilder = new ArgumentBuilder("print", this._serverContext)
.Add(this._localPath);
if (this._versionSpec) {
builder.AddSwitchWithValue("version", this._versionSpec.toString(), false);
}
return builder;
}

public GetOptions(): any {
return {};
}

public async ParseOutput(executionResult: IExecutionResult): Promise<string> {
// Check for "The specified file does not exist at the specified version" and write out empty string
if (this._ignoreFileNotFound && CommandHelper.HasError(executionResult, "The specified file does not exist at the specified version")) {
// The file doesn't exist, but the ignore flag is set, so we will simply return an emtpy string
return "";
}

// Throw if any OTHER errors are found in stderr or if exitcode is not 0
CommandHelper.ProcessErrors(this.GetArguments().GetCommand(), executionResult);

// Split the lines to take advantage of the WARNing skip logic and rejoin them to return
const lines: string[] = CommandHelper.SplitIntoLines(executionResult.stdout);
return lines.join(CommandHelper.GetNewLineCharacter(executionResult.stdout));
}
}
8 changes: 4 additions & 4 deletions src/tfvc/commands/getinfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import { TeamServerContext} from "../../contexts/servercontext";
import { IArgumentProvider, IExecutionResult, IItemInfo, ITfvcCommand } from "../interfaces";
import { TfvcError } from "../tfvcerror";
import { ArgumentBuilder } from "./argumentbuilder";
import { CommandHelper } from "./commandhelper";

Expand All @@ -20,9 +19,7 @@ export class GetInfo implements ITfvcCommand<IItemInfo[]> {
private _itemPaths: string[];

public constructor(serverContext: TeamServerContext, itemPaths: string[]) {
if (!itemPaths || itemPaths.length === 0) {
throw TfvcError.CreateArgumentMissingError("itemPaths");
}
CommandHelper.RequireStringArrayArgument(itemPaths, "itemPaths");
this._serverContext = serverContext;
this._itemPaths = itemPaths;
}
Expand Down Expand Up @@ -56,6 +53,9 @@ export class GetInfo implements ITfvcCommand<IItemInfo[]> {
* Size: 1385
*/
public async ParseOutput(executionResult: IExecutionResult): Promise<IItemInfo[]> {
// Throw if any errors are found in stderr or if exitcode is not 0
CommandHelper.ProcessErrors(this.GetArguments().GetCommand(), executionResult);

let itemInfos: IItemInfo[] = [];
if (!executionResult.stdout) {
return itemInfos;
Expand Down
3 changes: 3 additions & 0 deletions src/tfvc/commands/getversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ export class GetVersion implements ITfvcCommand<string> {
}

public async ParseOutput(executionResult: IExecutionResult): Promise<string> {
// Throw if any errors are found in stderr or if exitcode is not 0
CommandHelper.ProcessErrors(this.GetArguments().GetCommand(), executionResult);

const lines: string[] = CommandHelper.SplitIntoLines(executionResult.stdout);
// Find just the version number and return it. Ex. Team Explorer Everywhere Command Line Client (Version 14.0.3.201603291047)
if (lines && lines.length > 0) {
Expand Down
8 changes: 7 additions & 1 deletion src/tfvc/commands/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,18 @@ export class Status implements ITfvcCommand<IPendingChange[]> {
* SAMPLE
* <?xml version="1.0" encoding="utf-8"?>
* <status>
* <pending-changes/>
* <pending-changes>
* <pending-change server-item="$/tfsTest_03/Folder333/DemandEquals_renamed.java" version="217" owner="NORTHAMERICA\jpricket" date="2017-02-08T11:12:06.766-0500" lock="none" change-type="rename" workspace="Folder1_00" source-item="$/tfsTest_03/Folder333/DemandEquals.java" computer="JPRICKET-DEV2" local-item="D:\tmp\tfsTest03_44\Folder333\DemandEquals_renamed.java" file-type="windows-1252"/>
* </pending-changes>
* <candidate-pending-changes>
* <pending-change server-item="$/tfsTest_01/test.txt" version="0" owner="jason" date="2016-07-13T12:36:51.060-0400" lock="none" change-type="add" workspace="MyNewWorkspace2" computer="JPRICKET-DEV2" local-item="D:\tmp\test\test.txt"/>
* </candidate-pending-changes>
* </status>
*/
public async ParseOutput(executionResult: IExecutionResult): Promise<IPendingChange[]> {
// Throw if any errors are found in stderr or if exitcode is not 0
CommandHelper.ProcessErrors(this.GetArguments().GetCommand(), executionResult);

let changes: IPendingChange[] = [];
const xml: string = CommandHelper.TrimToXml(executionResult.stdout);
// Parse the xml using xml2js
Expand Down Expand Up @@ -99,6 +104,7 @@ export class Status implements ITfvcCommand<IPendingChange[]> {
computer: jsonChange.computer,
date: jsonChange.date,
localItem: jsonChange.localitem,
sourceItem: jsonChange.sourceitem,
lock: jsonChange.lock,
owner: jsonChange.owner,
serverItem: jsonChange.serveritem,
Expand Down
1 change: 1 addition & 0 deletions src/tfvc/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface IPendingChange {
computer: string;
date: string;
localItem: string;
sourceItem: string;
lock: string;
owner: string;
serverItem: string;
Expand Down
13 changes: 11 additions & 2 deletions src/tfvc/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { GetVersion } from "./commands/getversion";
import { FindWorkspace } from "./commands/findworkspace";
import { Status } from "./commands/status";
import { GetInfo } from "./commands/getinfo";
import { GetFileContent } from "./commands/getfilecontent";

var _ = require("underscore");

Expand Down Expand Up @@ -60,6 +61,12 @@ export class Repository {
new GetInfo(this._serverContext, itemPaths));
}

public async GetFileContent(itemPath: string, versionSpec?: string): Promise<string> {
Logger.LogDebug(`TFVC Repository.GetFileContent`);
return this.RunCommand<string>(
new GetFileContent(this._serverContext, itemPath, versionSpec, true));
}

public async GetStatus(ignoreFiles?: boolean): Promise<IPendingChange[]> {
Logger.LogDebug(`TFVC Repository.GetStatus`);
return this.RunCommand<IPendingChange[]>(
Expand All @@ -80,8 +87,10 @@ export class Repository {
}

public async RunCommand<T>(cmd: ITfvcCommand<T>): Promise<T> {
const result = await this.exec(cmd.GetArguments(), cmd.GetOptions());
return await cmd.ParseOutput(result);
const result: IExecutionResult = await this.exec(cmd.GetArguments(), cmd.GetOptions());
// We will call ParseOutput to give the command a chance to handle any specific errors itself.
const output: T = await cmd.ParseOutput(result);
return output;
}

private async exec(args: IArgumentProvider, options: any = {}): Promise<IExecutionResult> {
Expand Down
21 changes: 12 additions & 9 deletions src/tfvc/scm/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,17 +88,20 @@ export class Model {
const merge: Resource[] = [];

changes.forEach(raw => {
const resource: Resource = new Resource(raw);
const uri = Uri.file(raw.localItem);

switch (raw.changeType.toLowerCase()) {
case "add": return workingTree.push(new Resource(uri, Status.ADD));
case "branch": return workingTree.push(new Resource(uri, Status.BRANCH));
case "delete": return workingTree.push(new Resource(uri, Status.DELETE));
case "edit": return workingTree.push(new Resource(uri, Status.EDIT));
case "lock": return workingTree.push(new Resource(uri, Status.LOCK));
case "merge": return merge.push(new Resource(uri, Status.MERGE));
case "rename": return workingTree.push(new Resource(uri, Status.RENAME));
case "undelete": return workingTree.push(new Resource(uri, Status.UNDELETE));
switch (resource.Type) {
case Status.ADD:
case Status.BRANCH:
case Status.DELETE:
case Status.EDIT:
case Status.RENAME:
case Status.UNDELETE:
return index.push(resource);
case Status.LOCK:
case Status.MERGE:
return merge.push(resource);
}
});

Expand Down
30 changes: 23 additions & 7 deletions src/tfvc/scm/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,38 @@
"use strict";

import { SCMResource, SCMResourceDecorations, Uri } from "vscode";
import { Status } from "./status";
import { IPendingChange } from "../interfaces";
import { TfvcSCMProvider } from "../tfvcscmprovider";
import { CreateStatus, Status } from "./status";
import { DecorationProvider } from "./decorationprovider";

export class Resource implements SCMResource {
private _uri: Uri;
private _type: Status;
private _change: IPendingChange;

constructor(change: IPendingChange) {
this._change = change;
this._uri = Uri.file(change.localItem);
this._type = CreateStatus(change.changeType);
}

public get PendingChange(): IPendingChange { return this._change; }
public get Type(): Status { return this._type; }

/**
* This method gets a vscode file uri that represents the server path and version that the local item is based on.
*/
public GetServerUri(): Uri {
const serverItem: string = this._change.sourceItem ? this._change.sourceItem : this._change.serverItem;
const versionSpec: string = "C" + this._change.version;
return Uri.file(serverItem).with({ scheme: TfvcSCMProvider.scmScheme, query: versionSpec });
}

/* Implement SCMResource */
get uri(): Uri { return this._uri; }
get type(): Status { return this._type; }
get decorations(): SCMResourceDecorations {
// TODO Add conflict type to the resource constructor and pass it here
return DecorationProvider.getDecorations(this._type);
}

constructor(uri: Uri, type: Status) {
this._uri = uri;
this._type = type;
}
}
Loading

0 comments on commit c2197b2

Please sign in to comment.