diff --git a/icons/dark/download.svg b/icons/dark/download.svg
new file mode 100644
index 00000000..12ad689a
--- /dev/null
+++ b/icons/dark/download.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/icons/dark/repo.svg b/icons/dark/repo.svg
new file mode 100644
index 00000000..b1f87469
--- /dev/null
+++ b/icons/dark/repo.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/icons/light/download.svg b/icons/light/download.svg
new file mode 100644
index 00000000..f271a008
--- /dev/null
+++ b/icons/light/download.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/icons/light/repo.svg b/icons/light/repo.svg
new file mode 100644
index 00000000..4e2368ef
--- /dev/null
+++ b/icons/light/repo.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 953e09a4..14710d4d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -390,7 +390,7 @@
"arr-flatten": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
- "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==",
+ "integrity": "sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=",
"dev": true
},
"arr-union": {
@@ -3073,7 +3073,7 @@
"queue": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/queue/-/queue-4.4.2.tgz",
- "integrity": "sha512-fSMRXbwhMwipcDZ08enW2vl+YDmAmhcNcr43sCJL8DIg+CFOsoRLG23ctxA+fwNk1w55SePSiS7oqQQSgQoVJQ==",
+ "integrity": "sha1-Wpcz2ai4vRs26TS8nFWribKOKcc=",
"dev": true,
"requires": {
"inherits": "2.0.3"
@@ -9143,10 +9143,10 @@
"kind-of": "3.2.2"
}
},
- "source-map": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz",
- "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=",
+ "regex-cache": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz",
+ "integrity": "sha1-db3FiioUls7EihKDW8VMjVYjNt0=",
"dev": true,
"optional": true,
"requires": {
diff --git a/package.json b/package.json
index 56024cdf..afa8c332 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,7 @@
"tools:genReadme": "node ./out/tools/generateConfigSectionForReadme.js",
"precommit": "pretty-quick --staged && npm run lint",
"lint": "tslint -p ./",
+ "lint:fix": "tslint --fix -p ./",
"semantic-release": "semantic-release",
"build": "tsc -p ."
},
@@ -70,6 +71,14 @@
"semantic-release-vsce": "^2.1.2"
},
"contributes": {
+ "views": {
+ "explorer": [
+ {
+ "id": "svn",
+ "name": "SVN"
+ }
+ ]
+ },
"commands": [
{
"command": "svn.add",
@@ -240,6 +249,19 @@
"command": "svn.renameExplorer",
"title": "Rename with SVN",
"category": "SVN"
+ },
+ {
+ "command": "svn.treeview.refreshProvider",
+ "title": "Refresh",
+ "icon": {
+ "light": "icons/light/refresh.svg",
+ "dark": "icons/dark/refresh.svg"
+ }
+ },
+ {
+ "command": "svn.treeview.pullIncomingChange",
+ "title": "Pull incoming change",
+ "category": "SVN"
}
],
"menus": {
@@ -341,6 +363,31 @@
"when": "false"
}
],
+ "view/title": [
+ {
+ "command": "svn.treeview.refreshProvider",
+ "when": "view == svn",
+ "group": "navigation"
+ }
+ ],
+ "view/item/context": [
+ {
+ "command": "svn.openHEADFile",
+ "when": "viewItem =~ /incomingChange:(added|modified)/"
+ },
+ {
+ "command": "svn.openFile",
+ "when": "viewItem =~ /incomingChange:(modified|deleted)/"
+ },
+ {
+ "command": "svn.treeview.pullIncomingChange",
+ "when": "viewItem =~ /incomingChange:*/"
+ },
+ {
+ "command": "svn.openChangeHead",
+ "when": "viewItem == incomingChange:modified"
+ }
+ ],
"scm/title": [
{
"command": "svn.commitWithMessage",
diff --git a/src/commands.ts b/src/commands.ts
index 7f0fcb65..dfa0d950 100644
--- a/src/commands.ts
+++ b/src/commands.ts
@@ -35,6 +35,8 @@ import { inputCommitMessage } from "./messages";
import { Model } from "./model";
import { Repository } from "./repository";
import { Resource } from "./resource";
+import IncommingChangeNode from "./treeView/nodes/incomingChangeNode";
+import IncomingChangeNode from "./treeView/nodes/incomingChangeNode";
import { fromSvnUri, toSvnUri } from "./uri";
import {
fixPathSeparator,
@@ -367,7 +369,7 @@ export class SvnCommands implements IDisposable {
@command("svn.openFile")
public async openFile(
- arg?: Resource | Uri,
+ arg?: Resource | Uri | IncommingChangeNode,
...resourceStates: SourceControlResourceState[]
): Promise {
const preserveFocus = arg instanceof Resource;
@@ -380,6 +382,16 @@ export class SvnCommands implements IDisposable {
} else if (arg.scheme === "file") {
uris = [arg];
}
+ } else if (arg instanceof IncommingChangeNode) {
+ const resource = new Resource(
+ arg.uri,
+ arg.type,
+ undefined,
+ arg.props,
+ true
+ );
+
+ uris = [resource.resourceUri];
} else {
let resource = arg;
@@ -426,13 +438,17 @@ export class SvnCommands implements IDisposable {
}
@command("svn.openHEADFile")
- public async openHEADFile(arg?: Resource | Uri): Promise {
+ public async openHEADFile(
+ arg?: Resource | Uri | IncommingChangeNode
+ ): Promise {
let resource: Resource | undefined;
if (arg instanceof Resource) {
resource = arg;
} else if (arg instanceof Uri) {
resource = this.getSCMResource(arg);
+ } else if (arg instanceof IncommingChangeNode) {
+ resource = new Resource(arg.uri, arg.type, undefined, arg.props, true);
} else {
resource = this.getSCMResource();
}
@@ -472,14 +488,14 @@ export class SvnCommands implements IDisposable {
@command("svn.openChangeHead")
public async openChangeHead(
- arg?: Resource | Uri,
+ arg?: Resource | Uri | IncommingChangeNode,
...resourceStates: SourceControlResourceState[]
): Promise {
return this.openChange(arg, "HEAD", resourceStates);
}
public async openChange(
- arg?: Resource | Uri,
+ arg?: Resource | Uri | IncommingChangeNode,
against?: string,
resourceStates?: SourceControlResourceState[]
): Promise {
@@ -492,6 +508,16 @@ export class SvnCommands implements IDisposable {
if (resource !== undefined) {
resources = [resource];
}
+ } else if (arg instanceof IncommingChangeNode) {
+ const resource = new Resource(
+ arg.uri,
+ arg.type,
+ undefined,
+ arg.props,
+ true
+ );
+
+ resources = [resource];
} else {
let resource: Resource | undefined;
@@ -757,6 +783,29 @@ export class SvnCommands implements IDisposable {
}
}
+ @command("svn.treeview.pullIncomingChange")
+ public async pullIncomingChange(
+ incomingChange: IncomingChangeNode
+ ): Promise {
+ try {
+ const showUpdateMessage = configuration.get(
+ "showUpdateMessage",
+ true
+ );
+
+ const result = await incomingChange.repository.pullIncomingChange(
+ incomingChange.uri.fsPath
+ );
+
+ if (showUpdateMessage) {
+ window.showInformationMessage(result);
+ }
+ } catch (error) {
+ console.error(error);
+ window.showErrorMessage("Unable to update");
+ }
+ }
+
private async showDiffPath(repository: Repository, content: string) {
try {
const tempFile = path.join(repository.root, ".svn", "tmp", "svn.patch");
diff --git a/src/extension.ts b/src/extension.ts
index 028208fd..4bd07a1c 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -4,7 +4,8 @@ import {
ExtensionContext,
OutputChannel,
Uri,
- window
+ window,
+ workspace
} from "vscode";
import { SvnCommands } from "./commands";
import SvnDecorations from "./decorations/svnDecorations";
@@ -13,6 +14,7 @@ import { Model } from "./model";
import { Svn } from "./svn";
import { SvnContentProvider } from "./svnContentProvider";
import { SvnFinder } from "./svnFinder";
+import SvnProvider from "./treeView/dataProviders/svnProvider";
import {
hasSupportToDecorationProvider,
hasSupportToRegisterDiffCommand,
@@ -36,6 +38,10 @@ async function init(
const svnCommands = new SvnCommands(model);
disposables.push(model, contentProvider, svnCommands);
+ const svnProvider = new SvnProvider(model);
+
+ window.registerTreeDataProvider("svn", svnProvider);
+
// First, check the vscode has support to DecorationProvider
if (hasSupportToDecorationProvider()) {
const decoration = new SvnDecorations(model);
diff --git a/src/repository.ts b/src/repository.ts
index 3b3eaff0..309f10f7 100644
--- a/src/repository.ts
+++ b/src/repository.ts
@@ -58,7 +58,7 @@ export class Repository {
public statusBar: SvnStatusBar;
public changes: ISvnResourceGroup;
public unversioned: ISvnResourceGroup;
- public remoteChanged?: ISvnResourceGroup;
+ public remoteChanges?: Resource[];
public changelists: Map = new Map();
public conflicts: ISvnResourceGroup;
public statusIgnored: IFileStatus[] = [];
@@ -122,8 +122,8 @@ export class Repository {
group.resourceStates = [];
});
- if (this.remoteChanged) {
- this.remoteChanged.dispose();
+ if (this.remoteChanges) {
+ this.remoteChanges = [];
}
this.isIncomplete = false;
@@ -321,7 +321,7 @@ export class Repository {
const external: any[] = [];
const conflicts: any[] = [];
const changelists: Map = new Map();
- const remoteChanged: any[] = [];
+ const remoteChanges: any[] = [];
this.statusExternal = [];
this.statusIgnored = [];
@@ -406,7 +406,7 @@ export class Repository {
: undefined;
if (status.reposStatus) {
- remoteChanged.push(
+ remoteChanges.push(
new Resource(
uri,
status.reposStatus.item,
@@ -515,18 +515,14 @@ export class Repository {
/**
* Destroy and create for keep at last position
*/
- if (this.remoteChanged) {
- this.remoteChanged.dispose();
+ if (this.remoteChanges) {
+ this.remoteChanges = [];
}
- this.remoteChanged = this.sourceControl.createResourceGroup(
- "remotechanged",
- "Remote Changes"
- ) as ISvnResourceGroup;
- this.remoteChanged.hideWhenEmpty = true;
- this.remoteChanged.resourceStates = remoteChanged;
-
- if (remoteChanged.length !== this.remoteChangedFiles) {
- this.remoteChangedFiles = remoteChanged.length;
+
+ this.remoteChanges = remoteChanges;
+
+ if (remoteChanges.length !== this.remoteChangedFiles) {
+ this.remoteChangedFiles = remoteChanges.length;
this._onDidChangeRemoteChangedFiles.fire();
}
}
@@ -644,6 +640,14 @@ export class Repository {
});
}
+ public async pullIncomingChange(path: string) {
+ return this.run(Operation.Update, async () => {
+ const response = await this.repository.pullIncomingChange(path);
+ this.updateRemoteChangedFiles();
+ return response;
+ });
+ }
+
public async resolve(files: string[], action: string) {
return this.run(Operation.Resolve, () =>
this.repository.resolve(files, action)
diff --git a/src/svn.ts b/src/svn.ts
index 82792bd7..0f1b9265 100644
--- a/src/svn.ts
+++ b/src/svn.ts
@@ -3,7 +3,7 @@ import { EventEmitter } from "events";
import * as iconv from "iconv-lite";
import isUtf8 = require("is-utf8");
import * as jschardet from "jschardet";
-import { workspace } from "vscode";
+import { Uri, workspace } from "vscode";
import { ICpOptions, IExecutionResult, ISvnOptions } from "./common/types";
import { configuration } from "./helpers/configuration";
import { parseInfoXml } from "./infoParser";
diff --git a/src/svnRepository.ts b/src/svnRepository.ts
index 0af2ea6c..30077fa1 100644
--- a/src/svnRepository.ts
+++ b/src/svnRepository.ts
@@ -366,6 +366,24 @@ export class Repository {
return result.stdout;
}
+ public async pullIncomingChange(path: string): Promise {
+ const args = ["update", path];
+
+ const result = await this.exec(args);
+
+ this.resetInfo();
+
+ const message = result.stdout
+ .trim()
+ .split(/\r?\n/)
+ .pop();
+
+ if (message) {
+ return message;
+ }
+ return result.stdout;
+ }
+
public async patch(files: string[]) {
files = files.map(file => this.removeAbsolutePath(file));
const result = await this.exec(["diff", ...files]);
diff --git a/src/treeView/dataProviders/svnProvider.ts b/src/treeView/dataProviders/svnProvider.ts
new file mode 100644
index 00000000..fccb27a3
--- /dev/null
+++ b/src/treeView/dataProviders/svnProvider.ts
@@ -0,0 +1,54 @@
+import {
+ commands,
+ Event,
+ EventEmitter,
+ TreeDataProvider,
+ TreeItem,
+ window
+} from "vscode";
+import { Model } from "../../model";
+import BaseNode from "../nodes/baseNode";
+import RepositoryNode from "../nodes/repositoryNode";
+
+export default class SvnProvider implements TreeDataProvider {
+ private _onDidChangeTreeData: EventEmitter<
+ BaseNode | undefined
+ > = new EventEmitter();
+ public onDidChangeTreeData: Event = this
+ ._onDidChangeTreeData.event;
+
+ constructor(private model: Model) {
+ commands.registerCommand("svn.treeview.refreshProvider", () =>
+ this.refresh()
+ );
+ }
+
+ public refresh(): void {
+ this._onDidChangeTreeData.fire();
+ }
+
+ public getTreeItem(element: RepositoryNode): TreeItem {
+ return element.getTreeItem();
+ }
+
+ public async getChildren(element?: BaseNode): Promise {
+ if (!this.model || this.model.openRepositories.length === 0) {
+ window.showInformationMessage("No Svn repositories open");
+ return Promise.resolve([]);
+ }
+
+ if (element) {
+ return element.getChildren();
+ }
+
+ const repositories = this.model.openRepositories.map(repository => {
+ return new RepositoryNode(repository.repository, this);
+ });
+
+ return repositories;
+ }
+
+ public update(node: BaseNode): void {
+ this._onDidChangeTreeData.fire(node);
+ }
+}
diff --git a/src/treeView/nodes/baseNode.ts b/src/treeView/nodes/baseNode.ts
new file mode 100644
index 00000000..cfad9190
--- /dev/null
+++ b/src/treeView/nodes/baseNode.ts
@@ -0,0 +1,6 @@
+import { TreeItem } from "vscode";
+
+export default abstract class BaseNode {
+ public abstract getChildren(): BaseNode[] | Promise;
+ public abstract getTreeItem(): TreeItem | Promise;
+}
diff --git a/src/treeView/nodes/incomingChangeNode.ts b/src/treeView/nodes/incomingChangeNode.ts
new file mode 100644
index 00000000..57bfb4ee
--- /dev/null
+++ b/src/treeView/nodes/incomingChangeNode.ts
@@ -0,0 +1,41 @@
+import * as fs from "fs";
+import * as path from "path";
+import { TreeItem, TreeItemCollapsibleState, Uri } from "vscode";
+import { Repository } from "../../repository";
+import { getIconUri } from "../../uri";
+import BaseNode from "./baseNode";
+
+export default class IncomingChangeNode implements BaseNode {
+ constructor(
+ public uri: Uri,
+ public type: string,
+ public repository: Repository
+ ) {}
+
+ get props(): undefined {
+ return undefined;
+ }
+
+ get label() {
+ return path.relative(this.repository.workspaceRoot, this.uri.fsPath);
+ }
+
+ get contextValue() {
+ return `incomingChange:${this.type}`;
+ }
+
+ public getTreeItem(): TreeItem {
+ const item = new TreeItem(this.label, TreeItemCollapsibleState.None);
+ item.iconPath = {
+ dark: getIconUri(`status-${this.type}`, "dark"),
+ light: getIconUri(`status-${this.type}`, "light")
+ };
+ item.contextValue = this.contextValue;
+
+ return item;
+ }
+
+ public getChildren(): Promise {
+ return Promise.resolve([]);
+ }
+}
diff --git a/src/treeView/nodes/incomingChangesNode.ts b/src/treeView/nodes/incomingChangesNode.ts
new file mode 100644
index 00000000..98871017
--- /dev/null
+++ b/src/treeView/nodes/incomingChangesNode.ts
@@ -0,0 +1,43 @@
+import { TreeItem, TreeItemCollapsibleState } from "vscode";
+import { Repository } from "../../repository";
+import { getIconUri } from "../../uri";
+import BaseNode from "./baseNode";
+import IncommingChangeNode from "./incomingChangeNode";
+import NoIncomingChangesNode from "./noIncomingChangesNode";
+
+export default class IncomingChangesNode implements BaseNode {
+ constructor(private repository: Repository) {}
+
+ public getTreeItem(): TreeItem {
+ const item = new TreeItem(
+ "Incoming Changes",
+ TreeItemCollapsibleState.Collapsed
+ );
+ item.iconPath = {
+ dark: getIconUri("download", "dark"),
+ light: getIconUri("download", "light")
+ };
+
+ return item;
+ }
+
+ public async getChildren(): Promise {
+ if (!this.repository.remoteChanges) {
+ return [];
+ }
+
+ const changes = this.repository.remoteChanges.map(remoteChange => {
+ return new IncommingChangeNode(
+ remoteChange.resourceUri,
+ remoteChange.type,
+ this.repository
+ );
+ });
+
+ if (changes.length === 0) {
+ return [new NoIncomingChangesNode()];
+ }
+
+ return changes;
+ }
+}
diff --git a/src/treeView/nodes/noIncomingChangesNode.ts b/src/treeView/nodes/noIncomingChangesNode.ts
new file mode 100644
index 00000000..5d365033
--- /dev/null
+++ b/src/treeView/nodes/noIncomingChangesNode.ts
@@ -0,0 +1,17 @@
+import { TreeItem, TreeItemCollapsibleState } from "vscode";
+import BaseNode from "./baseNode";
+
+export default class NoIncomingChangesNode implements BaseNode {
+ public getTreeItem(): TreeItem {
+ const item = new TreeItem(
+ "No Incoming Changes",
+ TreeItemCollapsibleState.None
+ );
+
+ return item;
+ }
+
+ public async getChildren(): Promise {
+ return [];
+ }
+}
diff --git a/src/treeView/nodes/repositoryNode.ts b/src/treeView/nodes/repositoryNode.ts
new file mode 100644
index 00000000..94a5b7ef
--- /dev/null
+++ b/src/treeView/nodes/repositoryNode.ts
@@ -0,0 +1,36 @@
+import * as path from "path";
+import { TreeItem, TreeItemCollapsibleState, window } from "vscode";
+import { Repository } from "../../repository";
+import { getIconUri } from "../../uri";
+import SvnProvider from "../dataProviders/svnProvider";
+import BaseNode from "./baseNode";
+import IncomingChangesNode from "./incomingChangesNode";
+
+export default class RepositoryNode implements BaseNode {
+ constructor(
+ private repository: Repository,
+ private svnProvider: SvnProvider
+ ) {
+ repository.onDidChangeStatus(() => {
+ svnProvider.update(this);
+ });
+ }
+
+ get label() {
+ return path.basename(this.repository.workspaceRoot);
+ }
+
+ public getTreeItem(): TreeItem {
+ const item = new TreeItem(this.label, TreeItemCollapsibleState.Collapsed);
+ item.iconPath = {
+ dark: getIconUri("repo", "dark"),
+ light: getIconUri("repo", "light")
+ };
+
+ return item;
+ }
+
+ public async getChildren(): Promise {
+ return [new IncomingChangesNode(this.repository)];
+ }
+}
diff --git a/src/uri.ts b/src/uri.ts
index 626145e2..91800723 100644
--- a/src/uri.ts
+++ b/src/uri.ts
@@ -1,3 +1,4 @@
+import * as path from "path";
import { Uri } from "vscode";
import {
ISvnUriExtraParams,
@@ -27,3 +28,8 @@ export function toSvnUri(
query: JSON.stringify(params)
});
}
+
+export function getIconUri(iconName: string, theme: string): Uri {
+ const iconsRootPath = path.join(__dirname, "..", "icons");
+ return Uri.file(path.join(iconsRootPath, theme, `${iconName}.svg`));
+}