'use strict';
|
|
import { StarredBranches, WorkspaceState } from '../../constants';
|
|
import { Container } from '../../container';
|
|
import { GitRemote, GitRevision } from '../git';
|
|
import { GitStatus } from './status';
|
|
import { Dates, memoize } from '../../system';
|
|
import { GitBranchReference, GitReference } from './models';
|
|
import { BranchSorting, configuration, DateStyle } from '../../configuration';
|
|
|
|
const whitespaceRegex = /\s/;
|
|
const detachedHEADRegex = /^(?=.*\bHEAD\b)(?=.*\bdetached\b).*$/;
|
|
|
|
export const BranchDateFormatting = {
|
|
dateFormat: undefined! as string | null,
|
|
dateStyle: undefined! as DateStyle,
|
|
|
|
reset: () => {
|
|
BranchDateFormatting.dateFormat = configuration.get('defaultDateFormat');
|
|
BranchDateFormatting.dateStyle = configuration.get('defaultDateStyle');
|
|
},
|
|
};
|
|
|
|
export interface GitTrackingState {
|
|
ahead: number;
|
|
behind: number;
|
|
}
|
|
|
|
export class GitBranch implements GitBranchReference {
|
|
static is(branch: any): branch is GitBranch {
|
|
return branch instanceof GitBranch;
|
|
}
|
|
|
|
static isOfRefType(branch: GitReference | undefined) {
|
|
return branch?.refType === 'branch';
|
|
}
|
|
|
|
static sort(branches: GitBranch[], options?: { current: boolean }) {
|
|
const order = configuration.get('sortBranchesBy');
|
|
|
|
const opts = { current: true, ...options };
|
|
|
|
switch (order) {
|
|
case BranchSorting.DateAsc:
|
|
return branches.sort(
|
|
(a, b) =>
|
|
(opts.current ? (a.current ? -1 : 1) - (b.current ? -1 : 1) : 0) ||
|
|
(a.starred ? -1 : 1) - (b.starred ? -1 : 1) ||
|
|
(b.remote ? -1 : 1) - (a.remote ? -1 : 1) ||
|
|
(a.date == null ? -1 : a.date.getTime()) - (b.date == null ? -1 : b.date.getTime()),
|
|
);
|
|
case BranchSorting.DateDesc:
|
|
return branches.sort(
|
|
(a, b) =>
|
|
(opts.current ? (a.current ? -1 : 1) - (b.current ? -1 : 1) : 0) ||
|
|
(a.starred ? -1 : 1) - (b.starred ? -1 : 1) ||
|
|
(b.remote ? -1 : 1) - (a.remote ? -1 : 1) ||
|
|
(b.date == null ? -1 : b.date.getTime()) - (a.date == null ? -1 : a.date.getTime()),
|
|
);
|
|
case BranchSorting.NameAsc:
|
|
return branches.sort(
|
|
(a, b) =>
|
|
(opts.current ? (a.current ? -1 : 1) - (b.current ? -1 : 1) : 0) ||
|
|
(a.starred ? -1 : 1) - (b.starred ? -1 : 1) ||
|
|
(a.name === 'main' ? -1 : 1) - (b.name === 'main' ? -1 : 1) ||
|
|
(a.name === 'master' ? -1 : 1) - (b.name === 'master' ? -1 : 1) ||
|
|
(a.name === 'develop' ? -1 : 1) - (b.name === 'develop' ? -1 : 1) ||
|
|
(b.remote ? -1 : 1) - (a.remote ? -1 : 1) ||
|
|
b.name.localeCompare(a.name, undefined, { numeric: true, sensitivity: 'base' }),
|
|
);
|
|
default:
|
|
return branches.sort(
|
|
(a, b) =>
|
|
(opts.current ? (a.current ? -1 : 1) - (b.current ? -1 : 1) : 0) ||
|
|
(a.starred ? -1 : 1) - (b.starred ? -1 : 1) ||
|
|
(a.name === 'main' ? -1 : 1) - (b.name === 'main' ? -1 : 1) ||
|
|
(a.name === 'master' ? -1 : 1) - (b.name === 'master' ? -1 : 1) ||
|
|
(a.name === 'develop' ? -1 : 1) - (b.name === 'develop' ? -1 : 1) ||
|
|
(b.remote ? -1 : 1) - (a.remote ? -1 : 1) ||
|
|
a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' }),
|
|
);
|
|
}
|
|
}
|
|
|
|
readonly refType = 'branch';
|
|
readonly detached: boolean;
|
|
readonly id: string;
|
|
readonly tracking?: string;
|
|
readonly state: GitTrackingState;
|
|
|
|
constructor(
|
|
public readonly repoPath: string,
|
|
public readonly name: string,
|
|
public readonly remote: boolean,
|
|
public readonly current: boolean,
|
|
public readonly date: Date | undefined,
|
|
public readonly sha?: string,
|
|
tracking?: string,
|
|
ahead: number = 0,
|
|
behind: number = 0,
|
|
detached: boolean = false,
|
|
) {
|
|
this.id = `${repoPath}|${remote ? 'remotes/' : 'heads/'}${name}`;
|
|
|
|
this.detached = detached || (this.current ? GitBranch.isDetached(name) : false);
|
|
if (this.detached) {
|
|
this.name = GitBranch.formatDetached(this.sha!);
|
|
}
|
|
|
|
this.tracking = tracking == null || tracking.length === 0 ? undefined : tracking;
|
|
this.state = {
|
|
ahead: ahead,
|
|
behind: behind,
|
|
};
|
|
}
|
|
|
|
get formattedDate(): string {
|
|
return BranchDateFormatting.dateStyle === DateStyle.Absolute
|
|
? this.formatDate(BranchDateFormatting.dateFormat)
|
|
: this.formatDateFromNow();
|
|
}
|
|
|
|
get ref() {
|
|
return this.detached ? this.sha! : this.name;
|
|
}
|
|
|
|
@memoize()
|
|
private get dateFormatter(): Dates.DateFormatter | undefined {
|
|
return this.date == null ? undefined : Dates.getFormatter(this.date);
|
|
}
|
|
|
|
@memoize<GitBranch['formatDate']>(format => (format == null ? 'MMMM Do, YYYY h:mma' : format))
|
|
formatDate(format?: string | null): string {
|
|
if (format == null) {
|
|
format = 'MMMM Do, YYYY h:mma';
|
|
}
|
|
|
|
return this.dateFormatter?.format(format) ?? '';
|
|
}
|
|
|
|
formatDateFromNow(): string {
|
|
return this.dateFormatter?.fromNow() ?? '';
|
|
}
|
|
|
|
@memoize()
|
|
getBasename(): string {
|
|
const name = this.getNameWithoutRemote();
|
|
const index = name.lastIndexOf('/');
|
|
return index !== -1 ? name.substring(index + 1) : name;
|
|
}
|
|
|
|
@memoize()
|
|
getNameWithoutRemote(): string {
|
|
return this.remote ? this.name.substring(this.name.indexOf('/') + 1) : this.name;
|
|
}
|
|
|
|
@memoize()
|
|
getTrackingWithoutRemote(): string | undefined {
|
|
return this.tracking?.substring(this.tracking.indexOf('/') + 1);
|
|
}
|
|
|
|
@memoize()
|
|
async getRemote(): Promise<GitRemote | undefined> {
|
|
const remoteName = this.getRemoteName();
|
|
if (remoteName == null) return undefined;
|
|
|
|
const remotes = await Container.git.getRemotes(this.repoPath);
|
|
if (remotes.length === 0) return undefined;
|
|
|
|
return remotes.find(r => r.name === remoteName);
|
|
}
|
|
|
|
@memoize()
|
|
getRemoteName(): string | undefined {
|
|
if (this.remote) return GitBranch.getRemote(this.name);
|
|
if (this.tracking != null) return GitBranch.getRemote(this.tracking);
|
|
|
|
return undefined;
|
|
}
|
|
|
|
getTrackingStatus(options?: {
|
|
empty?: string;
|
|
expand?: boolean;
|
|
prefix?: string;
|
|
separator?: string;
|
|
suffix?: string;
|
|
}): string {
|
|
return GitStatus.getUpstreamStatus(this.tracking, this.state, options);
|
|
}
|
|
|
|
get starred() {
|
|
const starred = Container.context.workspaceState.get<StarredBranches>(WorkspaceState.StarredBranches);
|
|
return starred !== undefined && starred[this.id] === true;
|
|
}
|
|
|
|
star(updateViews: boolean = true) {
|
|
return this.updateStarred(true, updateViews);
|
|
}
|
|
|
|
unstar(updateViews: boolean = true) {
|
|
return this.updateStarred(false, updateViews);
|
|
}
|
|
|
|
private async updateStarred(star: boolean, updateViews: boolean = true) {
|
|
let starred = Container.context.workspaceState.get<StarredBranches>(WorkspaceState.StarredBranches);
|
|
if (starred === undefined) {
|
|
starred = Object.create(null) as StarredBranches;
|
|
}
|
|
|
|
if (star) {
|
|
starred[this.id] = true;
|
|
} else {
|
|
const { [this.id]: _, ...rest } = starred;
|
|
starred = rest;
|
|
}
|
|
await Container.context.workspaceState.update(WorkspaceState.StarredBranches, starred);
|
|
|
|
// TODO@eamodio this is UGLY
|
|
if (updateViews) {
|
|
void (await Container.branchesView.refresh());
|
|
void (await Container.remotesView.refresh());
|
|
void (await Container.repositoriesView.refresh());
|
|
}
|
|
}
|
|
|
|
static formatDetached(sha: string): string {
|
|
return `(${GitRevision.shorten(sha)}...)`;
|
|
}
|
|
|
|
static getNameWithoutRemote(name: string): string {
|
|
return name.substring(name.indexOf('/') + 1);
|
|
}
|
|
|
|
static getRemote(name: string): string {
|
|
return name.substring(0, name.indexOf('/'));
|
|
}
|
|
|
|
static isDetached(name: string): boolean {
|
|
// If there is whitespace in the name assume this is not a valid branch name
|
|
// Deals with detached HEAD states
|
|
return whitespaceRegex.test(name) || detachedHEADRegex.test(name);
|
|
}
|
|
}
|