Skip to content

Commit

Permalink
#33 Support for git repositories in subfolders.
Browse files Browse the repository at this point in the history
  • Loading branch information
mhutchie committed Apr 25, 2019
1 parent 0757579 commit 4cfbebf
Show file tree
Hide file tree
Showing 13 changed files with 316 additions and 117 deletions.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@
"default": 100,
"description": "Specifies the number of commits to load when the \"Load More Commits\" button is pressed (only shown when more commits are available)."
},
"git-graph.maxDepthOfRepoSearch": {
"type": "number",
"default": 0,
"description": "Specifies the maximum depth of subfolders to search when discovering repositories in the workspace."
},
"git-graph.showCurrentBranchByDefault": {
"type": "boolean",
"default": false,
Expand Down
4 changes: 4 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ class Config {
return this.workspaceConfiguration.get('loadMoreCommits', 75);
}

public maxDepthOfRepoSearch() {
return this.workspaceConfiguration.get('maxDepthOfRepoSearch', 0);
}

public showCurrentBranchByDefault() {
return this.workspaceConfiguration.get('showCurrentBranchByDefault', false);
}
Expand Down
39 changes: 12 additions & 27 deletions src/dataSource.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import * as cp from 'child_process';
import * as vscode from 'vscode';
import { getConfig } from './config';
import { ExtensionState } from './extensionState';
import { GitCommandStatus, GitCommit, GitCommitDetails, GitCommitNode, GitFileChangeType, GitRefData, GitRepoSet, GitResetMode, GitUnsavedChanges } from './types';
import { GitCommandStatus, GitCommit, GitCommitDetails, GitCommitNode, GitFileChangeType, GitRefData, GitResetMode, GitUnsavedChanges } from './types';
import { getPathFromStr } from './utils';

const eolRegex = /\r\n|\r|\n/g;
const headRegex = /^\(HEAD detached at [0-9A-Za-z]+\)/g;
const gitLogSeparator = 'XX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb';

export class DataSource {
private readonly extensionState: ExtensionState;
private gitPath!: string;
private gitExecPath!: string;
private gitLogFormat!: string;
private gitCommitDetailsFormat!: string;

constructor(extensionState: ExtensionState) {
this.extensionState = extensionState;
constructor() {
this.registerGitPath();
this.generateGitCommandFormats();
}
Expand All @@ -32,18 +29,6 @@ export class DataSource {
this.gitCommitDetailsFormat = ['%H', '%P', '%an', '%ae', dateType, '%cn'].join(gitLogSeparator) + '%n%B';
}

public async getRepos() {
let rootFolders = vscode.workspace.workspaceFolders, repoConfig = this.extensionState.getRepoConfig();
let repos: GitRepoSet = {}, i, path;
if (typeof rootFolders !== 'undefined') {
for (i = 0; i < rootFolders.length; i++) {
path = rootFolders[i].uri.fsPath.replace(/\\/g, '/');
if (await this.isGitRepository(path)) repos[path] = typeof repoConfig[path] !== 'undefined' ? repoConfig[path] : { columnWidths: null };
}
}
return repos;
}

public getBranches(repo: string, showRemoteBranches: boolean) {
return new Promise<{ branches: string[], head: string | null }>((resolve) => {
this.execGit('branch' + (showRemoteBranches ? ' -a' : ''), repo, (err, stdout) => {
Expand Down Expand Up @@ -138,7 +123,7 @@ export class DataSource {
for (let i = 1; i < lines.length - 1; i++) {
let line = lines[i].split('\t');
if (line.length < 2) break;
let oldFilePath = line[1].replace(/\\/g, '/'), newFilePath = line[line.length - 1].replace(/\\/g, '/');
let oldFilePath = getPathFromStr(line[1]), newFilePath = getPathFromStr(line[line.length - 1]);
fileLookup[newFilePath] = details.fileChanges.length;
details.fileChanges.push({ oldFilePath: oldFilePath, newFilePath: newFilePath, type: <GitFileChangeType>line[0][0], additions: null, deletions: null });
}
Expand Down Expand Up @@ -186,6 +171,14 @@ export class DataSource {
});
}

public isGitRepository(path: string) {
return new Promise<boolean>(resolve => {
this.execGit('rev-parse --git-dir', path, (err) => {
resolve(!err);
});
});
}

public addTag(repo: string, tagName: string, commitHash: string, lightweight: boolean, message: string) {
let args = ['tag'];
if (lightweight) {
Expand Down Expand Up @@ -349,14 +342,6 @@ export class DataSource {
});
}

private isGitRepository(folder: string) {
return new Promise<boolean>((resolve) => {
this.execGit('rev-parse --git-dir', folder, (err) => {
resolve(!err);
});
});
}

private execGit(command: string, repo: string, callback: { (error: Error | null, stdout: string, stderr: string): void }) {
cp.exec(this.gitExecPath + ' ' + command, { cwd: repo }, callback);
}
Expand Down
3 changes: 2 additions & 1 deletion src/diffDocProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as vscode from 'vscode';
import { DataSource } from './dataSource';
import { getPathFromStr } from './utils';

export class DiffDocProvider implements vscode.TextDocumentContentProvider {
static scheme = 'git-graph';
Expand Down Expand Up @@ -49,7 +50,7 @@ class DiffDocument {
}

export function encodeDiffDocUri(repo: string, path: string, commit: string): vscode.Uri {
return vscode.Uri.parse(DiffDocProvider.scheme + ':' + path.replace(/\\/g, '/') + '?commit=' + encodeURIComponent(commit) + '&repo=' + encodeURIComponent(repo));
return vscode.Uri.parse(DiffDocProvider.scheme + ':' + getPathFromStr(path) + '?commit=' + encodeURIComponent(commit) + '&repo=' + encodeURIComponent(repo));
}

export function decodeDiffDocUri(uri: vscode.Uri) {
Expand Down
37 changes: 19 additions & 18 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,37 @@ import { DataSource } from './dataSource';
import { DiffDocProvider } from './diffDocProvider';
import { ExtensionState } from './extensionState';
import { GitGraphView } from './gitGraphView';
import { RepoManager } from './repoManager';
import { StatusBarItem } from './statusBarItem';

export function activate(context: vscode.ExtensionContext) {
const extensionState = new ExtensionState(context);
const dataSource = new DataSource(extensionState);
const dataSource = new DataSource();
const avatarManager = new AvatarManager(dataSource, extensionState);
const statusBarItem = new StatusBarItem(context, dataSource);
const statusBarItem = new StatusBarItem(context);
const repoManager = new RepoManager(dataSource, extensionState, statusBarItem);

context.subscriptions.push(
vscode.commands.registerCommand('git-graph.view', () => {
GitGraphView.createOrShow(context.extensionPath, dataSource, extensionState, avatarManager);
GitGraphView.createOrShow(context.extensionPath, dataSource, extensionState, avatarManager, repoManager);
}),
vscode.commands.registerCommand('git-graph.clearAvatarCache', () => {
avatarManager.clearCache();
})
}),
vscode.workspace.registerTextDocumentContentProvider(DiffDocProvider.scheme, new DiffDocProvider(dataSource)),
vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('git-graph.showStatusBarItem')) {
statusBarItem.refresh();
} else if (e.affectsConfiguration('git-graph.dateType')) {
dataSource.generateGitCommandFormats();
} else if (e.affectsConfiguration('git-graph.maxDepthOfRepoSearch')) {
repoManager.maxDepthOfRepoSearchChanged();
} else if (e.affectsConfiguration('git.path')) {
dataSource.registerGitPath();
}
}),
repoManager
);

context.subscriptions.push(vscode.Disposable.from(
vscode.workspace.registerTextDocumentContentProvider(DiffDocProvider.scheme, new DiffDocProvider(dataSource))
));

context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('git-graph.showStatusBarItem')) {
statusBarItem.refresh();
} else if (e.affectsConfiguration('git-graph.dateType')) {
dataSource.generateGitCommandFormats();
} else if (e.affectsConfiguration('git.path')) {
dataSource.registerGitPath();
}
}));
}

export function deactivate() { }
23 changes: 11 additions & 12 deletions src/extensionState.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as fs from 'fs';
import { ExtensionContext, Memento } from 'vscode';
import { Avatar, AvatarCache, GitRepoSet, GitRepoState } from './types';
import { Avatar, AvatarCache, GitRepoSet } from './types';
import { getPathFromStr } from './utils';

const AVATAR_STORAGE_FOLDER = '/avatars';
const AVATAR_CACHE = 'avatarCache';
Expand All @@ -17,7 +18,7 @@ export class ExtensionState {
this.globalState = context.globalState;
this.workspaceState = context.workspaceState;

this.globalStoragePath = context.globalStoragePath.replace(/\\/g, '/');
this.globalStoragePath = getPathFromStr(context.globalStoragePath);
fs.stat(this.globalStoragePath + AVATAR_STORAGE_FOLDER, (err) => {
if (!err) {
this.avatarStorageAvailable = true;
Expand All @@ -31,6 +32,14 @@ export class ExtensionState {
});
}

/* Discovered Repos */
public getRepos() {
return this.workspaceState.get<GitRepoSet>(REPO_STATES, {});
}
public saveRepos(gitRepoSet: GitRepoSet) {
this.workspaceState.update(REPO_STATES, gitRepoSet);
}

/* Last Active Repo */
public getLastActiveRepo() {
return this.workspaceState.get<string | null>(LAST_ACTIVE_REPO, null);
Expand All @@ -39,16 +48,6 @@ export class ExtensionState {
this.workspaceState.update(LAST_ACTIVE_REPO, repo);
}

/* Repo Config */
public getRepoConfig() {
return this.workspaceState.get<GitRepoSet>(REPO_STATES, {});
}
public setRepoState(repo: string, state: GitRepoState) {
let config = this.getRepoConfig();
config[repo] = state;
this.workspaceState.update(REPO_STATES, config);
}

/* Avatars */
public isAvatarStorageAvailable() {
return this.avatarStorageAvailable;
Expand Down
32 changes: 14 additions & 18 deletions src/gitGraphView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { DataSource } from './dataSource';
import { encodeDiffDocUri } from './diffDocProvider';
import { ExtensionState } from './extensionState';
import { RepoFileWatcher } from './repoFileWatcher';
import { RepoFolderWatcher } from './repoFolderWatcher';
import { RepoManager } from './repoManager';
import { GitFileChangeType, GitGraphViewState, GitRepoSet, RequestMessage, ResponseMessage } from './types';
import { abbrevCommit, copyToClipboard } from './utils';

Expand All @@ -19,13 +19,13 @@ export class GitGraphView {
private readonly dataSource: DataSource;
private readonly extensionState: ExtensionState;
private readonly repoFileWatcher: RepoFileWatcher;
private readonly repoFolderWatcher: RepoFolderWatcher;
private readonly repoManager: RepoManager;
private disposables: vscode.Disposable[] = [];
private isGraphViewLoaded: boolean = false;
private isPanelVisible: boolean = true;
private currentRepo: string | null = null;

public static createOrShow(extensionPath: string, dataSource: DataSource, extensionState: ExtensionState, avatarManager: AvatarManager) {
public static createOrShow(extensionPath: string, dataSource: DataSource, extensionState: ExtensionState, avatarManager: AvatarManager, repoManager: RepoManager) {
const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined;

if (GitGraphView.currentPanel) {
Expand All @@ -40,15 +40,16 @@ export class GitGraphView {
]
});

GitGraphView.currentPanel = new GitGraphView(panel, extensionPath, dataSource, extensionState, avatarManager);
GitGraphView.currentPanel = new GitGraphView(panel, extensionPath, dataSource, extensionState, avatarManager, repoManager);
}

private constructor(panel: vscode.WebviewPanel, extensionPath: string, dataSource: DataSource, extensionState: ExtensionState, avatarManager: AvatarManager) {
private constructor(panel: vscode.WebviewPanel, extensionPath: string, dataSource: DataSource, extensionState: ExtensionState, avatarManager: AvatarManager, repoManager: RepoManager) {
this.panel = panel;
this.extensionPath = extensionPath;
this.avatarManager = avatarManager;
this.dataSource = dataSource;
this.extensionState = extensionState;
this.repoManager = repoManager;
this.avatarManager.registerView(this);

panel.iconPath = getConfig().tabIconColourTheme() === 'colour'
Expand All @@ -61,11 +62,9 @@ export class GitGraphView {
if (this.panel.visible !== this.isPanelVisible) {
if (this.panel.visible) {
this.update();
this.repoFolderWatcher.start();
} else {
this.currentRepo = null;
this.repoFileWatcher.stop();
this.repoFolderWatcher.stop();
}
this.isPanelVisible = this.panel.visible;
}
Expand All @@ -76,9 +75,8 @@ export class GitGraphView {
this.sendMessage({ command: 'refresh' });
}
});
this.repoFolderWatcher = new RepoFolderWatcher(async () => {
let repos = await this.dataSource.getRepos();
let numRepos = Object.keys(repos).length;
this.repoManager.registerViewCallback((repos: GitRepoSet, numRepos: number) => {
if (!this.panel.visible) return;
if ((numRepos === 0 && this.isGraphViewLoaded) || (numRepos > 0 && !this.isGraphViewLoaded)) {
this.update();
} else {
Expand Down Expand Up @@ -168,7 +166,7 @@ export class GitGraphView {
});
break;
case 'loadRepos':
this.respondLoadRepos(await this.dataSource.getRepos());
this.respondLoadRepos(await this.repoManager.getRepos(true));
break;
case 'mergeBranch':
this.sendMessage({
Expand Down Expand Up @@ -207,7 +205,7 @@ export class GitGraphView {
});
break;
case 'saveRepoState':
this.extensionState.setRepoState(msg.repo, msg.state);
this.repoManager.setRepoState(msg.repo, msg.state);
break;
case 'viewDiff':
this.sendMessage({
Expand All @@ -229,12 +227,10 @@ export class GitGraphView {
this.panel.dispose();
this.avatarManager.deregisterView();
this.repoFileWatcher.stop();
this.repoFolderWatcher.stop();
this.repoManager.deregisterViewCallback();
while (this.disposables.length) {
const x = this.disposables.pop();
if (x) {
x.dispose();
}
if (x) x.dispose();
}
}

Expand All @@ -254,14 +250,14 @@ export class GitGraphView {
initialLoadCommits: config.initialLoadCommits(),
lastActiveRepo: this.extensionState.getLastActiveRepo(),
loadMoreCommits: config.loadMoreCommits(),
repos: await this.dataSource.getRepos(),
repos: await this.repoManager.getRepos(true),
showCurrentBranchByDefault: config.showCurrentBranchByDefault()
};

let body, numRepos = Object.keys(viewState.repos).length, colorVars = '', colorParams = '';
for (let i = 0; i < viewState.graphColours.length; i++) {
colorVars += '--git-graph-color' + i + ':' + viewState.graphColours[i] + '; ';
colorParams += '[data-color="'+i+'"]{--git-graph-color:var(--git-graph-color'+i+');} ';
colorParams += '[data-color="' + i + '"]{--git-graph-color:var(--git-graph-color' + i + ');} ';
}
if (numRepos > 0) {
body = `<body style="${colorVars}">
Expand Down
6 changes: 3 additions & 3 deletions src/repoFileWatcher.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as vscode from 'vscode';
import { getPathFromUri } from './utils';

const fileChangeRegex = /(^\.git\/(config|index|HEAD|refs\/stash|refs\/heads\/.*|refs\/remotes\/.*|refs\/tags\/.*)$)|(^(?!\.git).*$)|(^\.git[^\/]+$)/;

Expand All @@ -20,8 +21,7 @@ export class RepoFileWatcher {
}

this.repo = repo;
let ws = vscode.workspace.workspaceFolders!.find(f => f.uri.fsPath.replace(/\\/g, '/') === repo)!;
this.fsWatcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(ws, '**'));
this.fsWatcher = vscode.workspace.createFileSystemWatcher(repo + '/**');
this.fsWatcher.onDidCreate(uri => this.refesh(uri));
this.fsWatcher.onDidChange(uri => this.refesh(uri));
this.fsWatcher.onDidDelete(uri => this.refesh(uri));
Expand All @@ -45,7 +45,7 @@ export class RepoFileWatcher {

private async refesh(uri: vscode.Uri) {
if (this.muted) return;
if (!uri.fsPath.replace(/\\/g, '/').replace(this.repo + '/', '').match(fileChangeRegex)) return;
if (!getPathFromUri(uri).replace(this.repo + '/', '').match(fileChangeRegex)) return;
if ((new Date()).getTime() < this.resumeAt) return;

if (this.refreshTimeout !== null) {
Expand Down
Loading

0 comments on commit 4cfbebf

Please sign in to comment.