diff --git a/src/git/git.ts b/src/git/git.ts index 0251662..05675f0 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -10,6 +10,7 @@ import * as iconv from 'iconv-lite'; export { IGit }; export * from './models/models'; export * from './parsers/blameParser'; +export * from './parsers/branchParser'; export * from './parsers/diffParser'; export * from './parsers/logParser'; export * from './parsers/stashParser'; @@ -33,10 +34,11 @@ const GitWarnings = [ /no such path/, /does not have any commits/, /Path \'.*?\' does not exist in/, - /Path \'.*?\' exists on disk, but not in/ + /Path \'.*?\' exists on disk, but not in/, + /no upstream configured for branch/ ]; -async function gitCommand(options: { cwd: string, encoding?: string }, ...args: any[]) { +async function gitCommand(options: { cwd: string, encoding?: string, onError?: (ex: Error) => string | undefined }, ...args: any[]) { try { // Fixes https://github.com/eamodio/vscode-gitlens/issues/73 // See https://stackoverflow.com/questions/4144417/how-to-handle-asian-characters-in-file-names-in-git-on-os-x @@ -50,6 +52,11 @@ async function gitCommand(options: { cwd: string, encoding?: string }, ...args: return iconv.decode(Buffer.from(s, 'binary'), opts.encoding); } catch (ex) { + if (options.onError !== undefined) { + const result = options.onError(ex); + if (result !== undefined) return result; + } + const msg = ex && ex.toString(); if (msg) { for (const warning of GitWarnings) { @@ -168,15 +175,27 @@ export class Git { return gitCommand({ cwd: root }, ...params, `--`, file); } - static branch(repoPath: string, all: boolean) { - const params = [`branch`]; - if (all) { + static branch(repoPath: string, options: { all: boolean } = { all: false }) { + const params = [`branch`, `-vv`]; + if (options.all) { params.push(`-a`); } return gitCommand({ cwd: repoPath }, ...params); } + static branch_current(repoPath: string) { + const params = [`rev-parse`, `--abbrev-ref`, `--symbolic-full-name`, `@`, `@{u}`]; + const onError = (ex: Error) => { + if (/no upstream configured for branch/.test(ex && ex.toString())) { + return ex.message.split('\n')[0]; + } + + return undefined; + }; + return gitCommand({ cwd: repoPath, onError: onError }, ...params); + } + static checkout(repoPath: string, fileName: string, sha: string) { const [file, root] = Git.splitPath(fileName, repoPath); diff --git a/src/git/models/branch.ts b/src/git/models/branch.ts index 4cd889e..39fffab 100644 --- a/src/git/models/branch.ts +++ b/src/git/models/branch.ts @@ -5,15 +5,9 @@ export class GitBranch { current: boolean; name: string; remote: boolean; + tracking?: string; - constructor(branch: string) { - branch = branch.trim(); - - if (branch.startsWith('* ')) { - branch = branch.substring(2); - this.current = true; - } - + constructor(public readonly repoPath: string, branch: string, current: boolean = false, tracking?: string) { if (branch.startsWith('remotes/')) { branch = branch.substring(8); this.remote = true; @@ -24,6 +18,24 @@ export class GitBranch { branch = branch.substring(0, index); } + this.current = current; this.name = branch; + this.tracking = tracking; + } + + getName(): string { + return this.remote + ? this.name.substring(this.name.indexOf('/') + 1) + : this.name; + } + + getRemote(): string | undefined { + if (this.remote) return GitBranch.getRemote(this.name); + if (this.tracking !== undefined) return GitBranch.getRemote(this.tracking); + return undefined; + } + + static getRemote(branch: string): string { + return branch.substring(0, branch.indexOf('/')); } } \ No newline at end of file diff --git a/src/git/parsers/branchParser.ts b/src/git/parsers/branchParser.ts new file mode 100644 index 0000000..63bb375 --- /dev/null +++ b/src/git/parsers/branchParser.ts @@ -0,0 +1,24 @@ +'use strict'; +import { GitBranch } from './../git'; +const branchWithTrackingRegex = /^(\*?)\s+(.+?)\s+([0-9,a-f]+)\s+(?:\[(.*?\/.*?)(?:\:|\]))?/gm; + +export class GitBranchParser { + + static parse(data: string, repoPath: string): GitBranch[] | undefined { + if (!data) return undefined; + + const branches: GitBranch[] = []; + + let match: RegExpExecArray | null = null; + do { + match = branchWithTrackingRegex.exec(data); + if (match == null) break; + + branches.push(new GitBranch(repoPath, match[2], match[1] === '*', match[4])); + } while (match != null); + + if (!branches.length) return undefined; + + return branches; + } +} \ No newline at end of file diff --git a/src/gitService.ts b/src/gitService.ts index c942ba9..b7e716a 100644 --- a/src/gitService.ts +++ b/src/gitService.ts @@ -3,7 +3,7 @@ import { Functions, Iterables, Objects } from './system'; import { Disposable, Event, EventEmitter, FileSystemWatcher, Location, Position, Range, TextDocument, TextDocumentChangeEvent, TextEditor, Uri, workspace } from 'vscode'; import { IConfig } from './configuration'; import { DocumentSchemes, ExtensionKey, GlyphChars } from './constants'; -import { Git, GitAuthor, GitBlame, GitBlameCommit, GitBlameLine, GitBlameLines, GitBlameParser, GitBranch, GitCommit, GitDiff, GitDiffChunkLine, GitDiffParser, GitLog, GitLogCommit, GitLogParser, GitRemote, GitStash, GitStashParser, GitStatus, GitStatusFile, GitStatusParser, IGit, setDefaultEncoding } from './git/git'; +import { Git, GitAuthor, GitBlame, GitBlameCommit, GitBlameLine, GitBlameLines, GitBlameParser, GitBranch, GitBranchParser, GitCommit, GitDiff, GitDiffChunkLine, GitDiffParser, GitLog, GitLogCommit, GitLogParser, GitRemote, GitStash, GitStashParser, GitStatus, GitStatusFile, GitStatusParser, IGit, setDefaultEncoding } from './git/git'; import { GitUri, IGitCommitInfo, IGitUriData } from './git/gitUri'; import { Logger } from './logger'; import * as fs from 'fs'; @@ -572,17 +572,16 @@ export class GitService extends Disposable { async getBranch(repoPath: string): Promise { Logger.log(`getBranch('${repoPath}')`); - const data = await Git.branch(repoPath, false); - const branches = data.split('\n').filter(_ => !!_).map(_ => new GitBranch(_)); - return branches.find(_ => _.current); + const data = await Git.branch_current(repoPath); + const branch = data.split('\n'); + return new GitBranch(repoPath, branch[0], true, branch[1]); } async getBranches(repoPath: string): Promise { Logger.log(`getBranches('${repoPath}')`); - const data = await Git.branch(repoPath, true); - const branches = data.split('\n').filter(_ => !!_).map(_ => new GitBranch(_)); - return branches; + const data = await Git.branch(repoPath, { all: true }); + return GitBranchParser.parse(data, repoPath) || []; } getCacheEntryKey(fileName: string): string; diff --git a/src/quickPicks/commitFileDetails.ts b/src/quickPicks/commitFileDetails.ts index 91e4c47..b6bb1c1 100644 --- a/src/quickPicks/commitFileDetails.ts +++ b/src/quickPicks/commitFileDetails.ts @@ -4,7 +4,7 @@ import { QuickPickItem, QuickPickOptions, Uri, window } from 'vscode'; import { Commands, CopyMessageToClipboardCommandArgs, CopyShaToClipboardCommandArgs, DiffWithPreviousCommandArgs, DiffWithWorkingCommandArgs, ShowQuickCommitDetailsCommandArgs, ShowQuickCommitFileDetailsCommandArgs, ShowQuickFileHistoryCommandArgs } from '../commands'; import { CommandQuickPickItem, getQuickPickIgnoreFocusOut, KeyCommandQuickPickItem, OpenFileCommandQuickPickItem } from './common'; import { GlyphChars } from '../constants'; -import { GitBranch, GitLog, GitLogCommit, GitService, GitUri, RemoteResource } from '../gitService'; +import { GitLog, GitLogCommit, GitService, GitUri, RemoteResource } from '../gitService'; import { Keyboard, KeyCommand, KeyNoopCommand } from '../keyboard'; import { OpenRemotesCommandQuickPickItem } from './remotes'; import * as moment from 'moment'; @@ -128,11 +128,11 @@ export class CommitFileDetailsQuickPick { const remotes = Arrays.uniqueBy(await git.getRemotes(commit.repoPath), _ => _.url, _ => !!_.provider); if (remotes.length) { if (commit.workingFileName && commit.status !== 'D') { - const branch = await git.getBranch(commit.repoPath || git.repoPath) as GitBranch; + const branch = await git.getBranch(commit.repoPath || git.repoPath); items.push(new OpenRemotesCommandQuickPickItem(remotes, { type: 'file', fileName: commit.workingFileName, - branch: branch.name + branch: branch!.name } as RemoteResource, currentCommand)); } if (!stash) {