diff --git a/.gitignore b/.gitignore index 975111a6..158ec8db 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ media node_modules out *.vsix +yarn.lock +package-lock.json diff --git a/package.json b/package.json index 3d024431..cb0a74f2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "git-graph", "displayName": "Git Graph", - "version": "1.30.0", + "version": "1.30.1", "publisher": "mhutchie", "author": { "name": "Michael Hutchison", @@ -1072,6 +1072,11 @@ "default": true, "description": "Show Commits that are only referenced by tags in Git Graph." }, + "git-graph.repository.showReverseCommits": { + "type": "boolean", + "default": false, + "description": "Show Commits in reverse order by default. This can be overridden per repository from the Git Graph View's Control Bar." + }, "git-graph.repository.showRemoteBranches": { "type": "boolean", "default": true, @@ -1515,7 +1520,7 @@ "vscode:prepublish": "npm run compile", "vscode:uninstall": "node ./out/life-cycle/uninstall.js", "clean": "node ./.vscode/clean.js", - "compile": "npm run lint && npm run clean && npm run compile-src && npm run compile-web", + "compile": "npm run lint && npm run clean && npm run compile-src && npm run compile-web-debug", "compile-src": "tsc -p ./src && node ./.vscode/package-src.js", "compile-web": "tsc -p ./web && node ./.vscode/package-web.js", "compile-web-debug": "tsc -p ./web && node ./.vscode/package-web.js debug", diff --git a/src/config.ts b/src/config.ts index 8d777ca8..d557677b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -457,6 +457,13 @@ class Config { return !!this.getRenamedExtensionSetting('repository.showCommitsOnlyReferencedByTags', 'showCommitsOnlyReferencedByTags', true); } + /** + * Get the value of the `git-graph.repository.showReverseCommits` Extension Setting. + */ + get showReverseCommits() { + return !!this.config.get('repository.showReverseCommits', false); + } + /** * Get the value of the `git-graph.repository.showRemoteBranches` Extension Setting. */ diff --git a/src/dataSource.ts b/src/dataSource.ts index 10a3429f..78ea45ad 100644 --- a/src/dataSource.ts +++ b/src/dataSource.ts @@ -12,7 +12,7 @@ import { Disposable } from './utils/disposable'; import { Event } from './utils/event'; const DRIVE_LETTER_PATH_REGEX = /^[a-z]:\//; -const EOL_REGEX = /\r\n|\r|\n/g; +const EOL_REGEX = /\r\n|\n/g; const INVALID_BRANCH_REGEXP = /^\(.* .*\)$/; const REMOTE_HEAD_BRANCH_REGEXP = /^remotes\/.*\/HEAD$/; const GIT_LOG_SEPARATOR = 'XX7Nal-YARtTpjCikii9nJxER19D6diSyk-AWkPb'; @@ -161,114 +161,123 @@ export class DataSource extends Disposable { * @param stashes An array of all stashes in the repository. * @returns The commits in the repository. */ - public getCommits(repo: string, branches: ReadonlyArray | null, maxCommits: number, showTags: boolean, showRemoteBranches: boolean, includeCommitsMentionedByReflogs: boolean, onlyFollowFirstParent: boolean, commitOrdering: CommitOrdering, remotes: ReadonlyArray, hideRemotes: ReadonlyArray, stashes: ReadonlyArray): Promise { + public getCommits(repo: string, branches: ReadonlyArray | null, maxCommits: number, showTags: boolean, showReverseCommits: boolean, showRemoteBranches: boolean, includeCommitsMentionedByReflogs: boolean, onlyFollowFirstParent: boolean, commitOrdering: CommitOrdering, remotes: ReadonlyArray, hideRemotes: ReadonlyArray, stashes: ReadonlyArray): Promise { const config = getConfig(); - return Promise.all([ - this.getLog(repo, branches, maxCommits + 1, showTags && config.showCommitsOnlyReferencedByTags, showRemoteBranches, includeCommitsMentionedByReflogs, onlyFollowFirstParent, commitOrdering, remotes, hideRemotes, stashes), - this.getRefs(repo, showRemoteBranches, config.showRemoteHeads, hideRemotes).then((refData: GitRefData) => refData, (errorMessage: string) => errorMessage) - ]).then(async (results) => { - let commits: GitCommitRecord[] = results[0], refData: GitRefData | string = results[1], i; - let moreCommitsAvailable = commits.length === maxCommits + 1; - if (moreCommitsAvailable) commits.pop(); - - // It doesn't matter if getRefs() was rejected if no commits exist - if (typeof refData === 'string') { - // getRefs() returned an error message (string) - if (commits.length > 0) { - // Commits exist, throw the error - throw refData; - } else { - // No commits exist, so getRefs() will always return an error. Set refData to the default value - refData = { head: null, heads: [], tags: [], remotes: [] }; + + return this.getCommitsCount(repo).then((count) => { + return Promise.all([ + this.getLog(repo, branches, maxCommits + 1, count, showTags && config.showCommitsOnlyReferencedByTags, showReverseCommits, showRemoteBranches, includeCommitsMentionedByReflogs, onlyFollowFirstParent, commitOrdering, remotes, hideRemotes, stashes), + this.getRefs(repo, showRemoteBranches, config.showRemoteHeads, hideRemotes).then((refData: GitRefData) => refData, (errorMessage: string) => errorMessage) + ]).then(async (results) => { + let commits: GitCommitRecord[] = results[0], refData: GitRefData | string = results[1], i; + let moreCommitsAvailable = commits.length === maxCommits + 1; + if (moreCommitsAvailable) commits.pop(); + + // It doesn't matter if getRefs() was rejected if no commits exist + if (typeof refData === 'string') { + // getRefs() returned an error message (string) + if (commits.length > 0) { + // Commits exist, throw the error + throw refData; + } else { + // No commits exist, so getRefs() will always return an error. Set refData to the default value + refData = { head: null, heads: [], tags: [], remotes: [] }; + } } - } - if (refData.head !== null && config.showUncommittedChanges) { - for (i = 0; i < commits.length; i++) { - if (refData.head === commits[i].hash) { - const numUncommittedChanges = await this.getUncommittedChanges(repo); - if (numUncommittedChanges > 0) { - commits.unshift({ hash: UNCOMMITTED, parents: [refData.head], author: '*', email: '', date: Math.round((new Date()).getTime() / 1000), message: 'Uncommitted Changes (' + numUncommittedChanges + ')' }); + if (refData.head !== null && config.showUncommittedChanges) { + for (i = 0; i < commits.length; i++) { + if (refData.head === commits[i].hash) { + const numUncommittedChanges = await this.getUncommittedChanges(repo); + if (numUncommittedChanges > 0) { + const cn = { hash: UNCOMMITTED, parents: [refData.head], author: '*', email: '', date: Math.round((new Date()).getTime() / 1000), message: 'Uncommitted Changes (' + numUncommittedChanges + ')' }; + if (showReverseCommits) { + commits.push(cn); + } else { + commits.unshift(cn); + } + } + break; } - break; } } - } - let commitNodes: DeepWriteable[] = []; - let commitLookup: { [hash: string]: number } = {}; + let commitNodes: DeepWriteable[] = []; + let commitLookup: { [hash: string]: number } = {}; - for (i = 0; i < commits.length; i++) { - commitLookup[commits[i].hash] = i; - commitNodes.push({ ...commits[i], heads: [], tags: [], remotes: [], stash: null }); - } - - /* Insert Stashes */ - let toAdd: { index: number, data: GitStash }[] = []; - for (i = 0; i < stashes.length; i++) { - if (typeof commitLookup[stashes[i].hash] === 'number') { - commitNodes[commitLookup[stashes[i].hash]].stash = { - selector: stashes[i].selector, - baseHash: stashes[i].baseHash, - untrackedFilesHash: stashes[i].untrackedFilesHash - }; - } else if (typeof commitLookup[stashes[i].baseHash] === 'number') { - toAdd.push({ index: commitLookup[stashes[i].baseHash], data: stashes[i] }); + for (i = 0; i < commits.length; i++) { + commitLookup[commits[i].hash] = i; + commitNodes.push({ ...commits[i], heads: [], tags: [], remotes: [], stash: null }); } - } - toAdd.sort((a, b) => a.index !== b.index ? a.index - b.index : b.data.date - a.data.date); - for (i = toAdd.length - 1; i >= 0; i--) { - let stash = toAdd[i].data; - commitNodes.splice(toAdd[i].index, 0, { - hash: stash.hash, - parents: [stash.baseHash], - author: stash.author, - email: stash.email, - date: stash.date, - message: stash.message, - heads: [], tags: [], remotes: [], - stash: { - selector: stash.selector, - baseHash: stash.baseHash, - untrackedFilesHash: stash.untrackedFilesHash + + /* Insert Stashes */ + // OK for reverse order? + let toAdd: { index: number, data: GitStash }[] = []; + for (i = 0; i < stashes.length; i++) { + if (typeof commitLookup[stashes[i].hash] === 'number') { + commitNodes[commitLookup[stashes[i].hash]].stash = { + selector: stashes[i].selector, + baseHash: stashes[i].baseHash, + untrackedFilesHash: stashes[i].untrackedFilesHash + }; + } else if (typeof commitLookup[stashes[i].baseHash] === 'number') { + toAdd.push({ index: commitLookup[stashes[i].baseHash], data: stashes[i] }); } - }); - } - for (i = 0; i < commitNodes.length; i++) { - // Correct commit lookup after stashes have been spliced in - commitLookup[commitNodes[i].hash] = i; - } + } + toAdd.sort((a, b) => a.index !== b.index ? a.index - b.index : b.data.date - a.data.date); + for (i = toAdd.length - 1; i >= 0; i--) { + let stash = toAdd[i].data; + commitNodes.splice(toAdd[i].index, 0, { + hash: stash.hash, + parents: [stash.baseHash], + author: stash.author, + email: stash.email, + date: stash.date, + message: stash.message, + heads: [], tags: [], remotes: [], + stash: { + selector: stash.selector, + baseHash: stash.baseHash, + untrackedFilesHash: stash.untrackedFilesHash + } + }); + } + for (i = 0; i < commitNodes.length; i++) { + // Correct commit lookup after stashes have been spliced in + commitLookup[commitNodes[i].hash] = i; + } - /* Annotate Heads */ - for (i = 0; i < refData.heads.length; i++) { - if (typeof commitLookup[refData.heads[i].hash] === 'number') commitNodes[commitLookup[refData.heads[i].hash]].heads.push(refData.heads[i].name); - } + /* Annotate Heads */ + for (i = 0; i < refData.heads.length; i++) { + if (typeof commitLookup[refData.heads[i].hash] === 'number') commitNodes[commitLookup[refData.heads[i].hash]].heads.push(refData.heads[i].name); + } - /* Annotate Tags */ - if (showTags) { - for (i = 0; i < refData.tags.length; i++) { - if (typeof commitLookup[refData.tags[i].hash] === 'number') commitNodes[commitLookup[refData.tags[i].hash]].tags.push({ name: refData.tags[i].name, annotated: refData.tags[i].annotated }); + /* Annotate Tags */ + if (showTags) { + for (i = 0; i < refData.tags.length; i++) { + if (typeof commitLookup[refData.tags[i].hash] === 'number') commitNodes[commitLookup[refData.tags[i].hash]].tags.push({ name: refData.tags[i].name, annotated: refData.tags[i].annotated }); + } } - } - /* Annotate Remotes */ - for (i = 0; i < refData.remotes.length; i++) { - if (typeof commitLookup[refData.remotes[i].hash] === 'number') { - let name = refData.remotes[i].name; - let remote = remotes.find(remote => name.startsWith(remote + '/')); - commitNodes[commitLookup[refData.remotes[i].hash]].remotes.push({ name: name, remote: remote ? remote : null }); + /* Annotate Remotes */ + for (i = 0; i < refData.remotes.length; i++) { + if (typeof commitLookup[refData.remotes[i].hash] === 'number') { + let name = refData.remotes[i].name; + let remote = remotes.find(remote => name.startsWith(remote + '/')); + commitNodes[commitLookup[refData.remotes[i].hash]].remotes.push({ name: name, remote: remote ? remote : null }); + } } - } - return { - commits: commitNodes, - head: refData.head, - tags: unique(refData.tags.map((tag) => tag.name)), - moreCommitsAvailable: moreCommitsAvailable, - error: null - }; - }).catch((errorMessage) => { - return { commits: [], head: null, tags: [], moreCommitsAvailable: false, error: errorMessage }; + return { + commits: commitNodes, + head: refData.head, + tags: unique(refData.tags.map((tag) => tag.name)), + moreCommitsAvailable: moreCommitsAvailable, + error: null + }; + }).catch((errorMessage) => { + return { commits: [], head: null, tags: [], moreCommitsAvailable: false, error: errorMessage }; + }); }); } @@ -452,6 +461,11 @@ export class DataSource extends Disposable { /* Get Data Methods - General */ + public getCommitsCount(repo: string): Promise { + return this.spawnGit(['rev-list', '--all', '--count'], repo, (stdout) => { + return Number.parseInt(stdout); + }).then((count) => count, () => 0); + } /** * Get the subject of a commit. * @param repo The path of the repository. @@ -1496,8 +1510,16 @@ export class DataSource extends Disposable { * @param stashes An array of all stashes in the repository. * @returns An array of commits. */ - private getLog(repo: string, branches: ReadonlyArray | null, num: number, includeTags: boolean, includeRemotes: boolean, includeCommitsMentionedByReflogs: boolean, onlyFollowFirstParent: boolean, order: CommitOrdering, remotes: ReadonlyArray, hideRemotes: ReadonlyArray, stashes: ReadonlyArray) { - const args = ['-c', 'log.showSignature=false', 'log', '--max-count=' + num, '--format=' + this.gitFormatLog, '--' + order + '-order']; + private getLog(repo: string, branches: ReadonlyArray | null, num: number, allcount: number, includeTags: boolean, showReverseCommits: boolean, includeRemotes: boolean, includeCommitsMentionedByReflogs: boolean, onlyFollowFirstParent: boolean, order: CommitOrdering, remotes: ReadonlyArray, hideRemotes: ReadonlyArray, stashes: ReadonlyArray) { + let args = []; + if (showReverseCommits) { + let skipnum = allcount - num; + if (skipnum <= 0) skipnum = 0; + args = ['-c', 'log.showSignature=false', 'log', '--skip=' + skipnum, '--format=' + this.gitFormatLog, '--date-order', '--reverse']; + } else { + args = ['-c', 'log.showSignature=false', 'log', '--max-count=' + num, '--format=' + this.gitFormatLog, '--' + order + '-order']; + } + // const args = ['-c', 'log.showSignature=false', 'log', '--max-count=' + num, '--format=' + this.gitFormatLog, '--' + order + '-order']; if (onlyFollowFirstParent) { args.push('--first-parent'); } diff --git a/src/extensionState.ts b/src/extensionState.ts index 721857a4..fb767568 100644 --- a/src/extensionState.ts +++ b/src/extensionState.ts @@ -32,6 +32,7 @@ export const DEFAULT_REPO_STATE: GitRepoState = { onRepoLoadShowCheckedOutBranch: BooleanOverride.Default, onRepoLoadShowSpecificBranches: null, pullRequestConfig: null, + showReverseCommits: false, showRemoteBranches: true, showRemoteBranchesV2: BooleanOverride.Default, showStashes: BooleanOverride.Default, diff --git a/src/gitGraphView.ts b/src/gitGraphView.ts index 1acba6ff..c248b741 100644 --- a/src/gitGraphView.ts +++ b/src/gitGraphView.ts @@ -409,7 +409,8 @@ export class GitGraphView extends Disposable { command: 'loadCommits', refreshId: msg.refreshId, onlyFollowFirstParent: msg.onlyFollowFirstParent, - ...await this.dataSource.getCommits(msg.repo, msg.branches, msg.maxCommits, msg.showTags, msg.showRemoteBranches, msg.includeCommitsMentionedByReflogs, msg.onlyFollowFirstParent, msg.commitOrdering, msg.remotes, msg.hideRemotes, msg.stashes) + showReverseCommits: msg.showReverseCommits, + ...await this.dataSource.getCommits(msg.repo, msg.branches, msg.maxCommits, msg.showTags, msg.showReverseCommits, msg.showRemoteBranches, msg.includeCommitsMentionedByReflogs, msg.onlyFollowFirstParent, msg.commitOrdering, msg.remotes, msg.hideRemotes, msg.stashes) }); break; case 'loadConfig': @@ -690,6 +691,7 @@ export class GitGraphView extends Disposable { onRepoLoad: config.onRepoLoad, referenceLabels: config.referenceLabels, repoDropdownOrder: config.repoDropdownOrder, + showReverseCommits: config.showReverseCommits, showRemoteBranches: config.showRemoteBranches, showStashes: config.showStashes, showTags: config.showTags @@ -720,6 +722,7 @@ export class GitGraphView extends Disposable {
Repo: Branches: +
diff --git a/src/types.ts b/src/types.ts index c543fa9f..6a56ef52 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,6 @@ /* Git Interfaces / Types */ + export interface GitCommit { readonly hash: string; readonly parents: ReadonlyArray; @@ -212,6 +213,7 @@ export interface GitRepoState { onRepoLoadShowCheckedOutBranch: BooleanOverride; onRepoLoadShowSpecificBranches: string[] | null; pullRequestConfig: PullRequestConfig | null; + showReverseCommits: boolean; showRemoteBranches: boolean; showRemoteBranchesV2: BooleanOverride; showStashes: BooleanOverride; @@ -257,6 +259,7 @@ export interface GitGraphViewConfig { readonly onRepoLoad: OnRepoLoadConfig; readonly referenceLabels: ReferenceLabelsConfig; readonly repoDropdownOrder: RepoDropdownOrder; + readonly showReverseCommits: boolean; readonly showRemoteBranches: boolean; readonly showStashes: boolean; readonly showTags: boolean; @@ -904,6 +907,7 @@ export interface RequestLoadCommits extends RepoRequest { readonly branches: ReadonlyArray | null; // null => Show All readonly maxCommits: number; readonly showTags: boolean; + readonly showReverseCommits: boolean; readonly showRemoteBranches: boolean; readonly includeCommitsMentionedByReflogs: boolean; readonly onlyFollowFirstParent: boolean; @@ -920,6 +924,7 @@ export interface ResponseLoadCommits extends ResponseWithErrorInfo { readonly tags: string[]; readonly moreCommitsAvailable: boolean; readonly onlyFollowFirstParent: boolean; + readonly showReverseCommits: boolean; } export interface RequestLoadConfig extends RepoRequest { diff --git a/web/graph.ts b/web/graph.ts index 415dfd2e..564baefc 100644 --- a/web/graph.ts +++ b/web/graph.ts @@ -72,11 +72,15 @@ class Branch { /* Rendering */ - public draw(svg: SVGElement, config: GG.GraphConfig, expandAt: number) { - let colour = config.colours[this.colour % config.colours.length], i, x1, y1, x2, y2, lines: PlacedLine[] = [], curPath = '', d = config.grid.y * (config.style === GG.GraphStyle.Angular ? 0.38 : 0.8), line, nextLine; + public draw(svg: SVGElement, config: GG.GraphConfig, expandAt: number, showReverseCommits: boolean) { + let colour = config.colours[this.colour % config.colours.length], i, x1, y1, x2, y2, lines: PlacedLine[] = [], curPath = '', line, nextLine; + let di = config.grid.y * (config.style === GG.GraphStyle.Angular ? 0.38 : 0.8); + let d = showReverseCommits ? -di : di; // Convert branch lines into pixel coordinates, respecting expanded commit extensions - for (i = 0; i < this.lines.length; i++) { + for (showReverseCommits ? i = this.lines.length - 1 : i = 0; + showReverseCommits ? i >= 0 : i < this.lines.length; + showReverseCommits ? i-- : i++) { line = this.lines[i]; x1 = line.p1.x * config.grid.x + config.grid.offsetX; y1 = line.p1.y * config.grid.y + config.grid.offsetY; x2 = line.p2.x * config.grid.x + config.grid.offsetX; y2 = line.p2.y * config.grid.y + config.grid.offsetY; @@ -192,7 +196,6 @@ class Vertex { return this.children; } - /* Parents */ public addParent(vertex: Vertex) { @@ -347,6 +350,7 @@ class Graph { private commitLookup: { [hash: string]: number } = {}; private onlyFollowFirstParent: boolean = false; private expandedCommitIndex: number = -1; + private showReverseCommits: boolean; private readonly viewElem: HTMLElement; private readonly contentElem: HTMLElement; @@ -361,10 +365,11 @@ class Graph { private tooltipTimeout: NodeJS.Timer | null = null; private tooltipVertex: HTMLElement | null = null; - constructor(id: string, viewElem: HTMLElement, config: GG.GraphConfig, muteConfig: GG.MuteCommitsConfig) { + constructor(id: string, viewElem: HTMLElement, config: GG.GraphConfig, muteConfig: GG.MuteCommitsConfig, showReverseCommits: boolean) { this.viewElem = viewElem; this.config = config; this.muteConfig = muteConfig; + this.showReverseCommits = showReverseCommits; const elem = document.getElementById(id)!; this.contentElem = elem.parentElement!; @@ -387,10 +392,13 @@ class Graph { elem.appendChild(this.svg); } + public setShowReverseCommits(flag: boolean) { + this.showReverseCommits = flag; + } /* Graph Operations */ - public loadCommits(commits: ReadonlyArray, commitHead: string | null, commitLookup: { [hash: string]: number }, onlyFollowFirstParent: boolean) { + public loadCommits(commits: ReadonlyArray, commitHead: string | null, commitLookup: { [hash: string]: number }, onlyFollowFirstParent: boolean, showReverseCommits: boolean) { this.commits = commits; this.commitHead = commitHead; this.commitLookup = commitLookup; @@ -419,22 +427,28 @@ class Graph { } } - if (commits[0].hash === UNCOMMITTED) { - this.vertices[0].setNotCommitted(); + let top = showReverseCommits ? commits.length - 1 : 0; + if (commits[top].hash === UNCOMMITTED) { + this.vertices[top].setNotCommitted(); } - if (commits[0].hash === UNCOMMITTED && this.config.uncommittedChanges === GG.GraphUncommittedChangesStyle.OpenCircleAtTheUncommittedChanges) { - this.vertices[0].setCurrent(); + if (commits[top].hash === UNCOMMITTED && this.config.uncommittedChanges === GG.GraphUncommittedChangesStyle.OpenCircleAtTheUncommittedChanges) { + this.vertices[top].setCurrent(); } else if (commitHead !== null && typeof commitLookup[commitHead] === 'number') { this.vertices[commitLookup[commitHead]].setCurrent(); } - i = 0; - while (i < this.vertices.length) { - if (this.vertices[i].getNextParent() !== null || this.vertices[i].isNotOnBranch()) { - this.determinePath(i); + i = showReverseCommits ? this.vertices.length - 1 : 0; + while (showReverseCommits ? i >= 0 : i < this.vertices.length) { + const nv = this.vertices[i].getNextParent(); + if (nv !== null || this.vertices[i].isNotOnBranch()) { + this.determinePath(i, showReverseCommits); } else { - i++; + if (showReverseCommits) { + i--; + } else { + i++; + } } } } @@ -444,8 +458,10 @@ class Graph { let group = document.createElementNS(SVG_NAMESPACE, 'g'), i, contentWidth = this.getContentWidth(); group.setAttribute('mask', 'url(#GraphMask)'); - for (i = 0; i < this.branches.length; i++) { - this.branches[i].draw(group, this.config, this.expandedCommitIndex); + for (this.showReverseCommits ? i = this.branches.length - 1 : i = 0; + this.showReverseCommits ? i >= 0 : i < this.branches.length; + this.showReverseCommits ? i-- : i++) { + this.branches[i].draw(group, this.config, this.expandedCommitIndex, this.showReverseCommits); } const overListener = (e: MouseEvent) => this.vertexOver(e), outListener = (e: MouseEvent) => this.vertexOut(e); @@ -702,7 +718,7 @@ class Graph { /* Graph Layout Methods */ - private determinePath(startAt: number) { + private determinePath(startAt: number, showReverseCommits: boolean) { let i = startAt; let vertex = this.vertices[i], parentVertex = this.vertices[i].getNextParent(), curVertex; let lastPoint = vertex.isNotOnBranch() ? vertex.getNextPoint() : vertex.getPoint(), curPoint; @@ -710,7 +726,9 @@ class Graph { if (parentVertex !== null && parentVertex.id !== NULL_VERTEX_ID && vertex.isMerge() && !vertex.isNotOnBranch() && !parentVertex.isNotOnBranch()) { // Branch is a merge between two vertices already on branches let foundPointToParent = false, parentBranch = parentVertex.getBranch()!; - for (i = startAt + 1; i < this.vertices.length; i++) { + for (showReverseCommits ? i = startAt - 1 : i = startAt + 1; + showReverseCommits ? i >= 0 : i < this.vertices.length; + showReverseCommits ? i-- : i++) { curVertex = this.vertices[i]; curPoint = curVertex.getPointConnectingTo(parentVertex, parentBranch); // Check if there is already a point connecting the ith vertex to the required parent if (curPoint !== null) { @@ -732,7 +750,9 @@ class Graph { let branch = new Branch(this.getAvailableColour(startAt)); vertex.addToBranch(branch, lastPoint.x); vertex.registerUnavailablePoint(lastPoint.x, vertex, branch); - for (i = startAt + 1; i < this.vertices.length; i++) { + for (showReverseCommits ? i = startAt - 1 : i = startAt + 1; + showReverseCommits ? i >= 0 : i < this.vertices.length; + showReverseCommits ? i-- : i++) { curVertex = this.vertices[i]; curPoint = parentVertex === curVertex && !parentVertex.isNotOnBranch() ? curVertex.getPoint() : curVertex.getNextPoint(); branch.addLine(lastPoint, curPoint, vertex.getIsCommitted(), lastPoint.x < curPoint.x); @@ -752,7 +772,9 @@ class Graph { } } } - if (i === this.vertices.length && parentVertex !== null && parentVertex.id === NULL_VERTEX_ID) { + if ((showReverseCommits ? i === -1 : i === this.vertices.length) + && parentVertex !== null + && parentVertex.id === NULL_VERTEX_ID) { // Vertex is the last in the graph, so no more branch can be formed to the parent vertex.registerParentProcessed(); } diff --git a/web/main.ts b/web/main.ts index 29c23c82..2768a9a5 100644 --- a/web/main.ts +++ b/web/main.ts @@ -50,6 +50,7 @@ class GitGraphView { private readonly controlsElem: HTMLElement; private readonly tableElem: HTMLElement; private readonly footerElem: HTMLElement; + private readonly showReverseCommitsElem: HTMLInputElement; private readonly showRemoteBranchesElem: HTMLInputElement; private readonly refreshBtnElem: HTMLElement; private readonly scrollShadowElem: HTMLElement; @@ -77,7 +78,7 @@ class GitGraphView { viewElem.focus(); - this.graph = new Graph('commitGraph', viewElem, this.config.graph, this.config.mute); + this.graph = new Graph('commitGraph', viewElem, this.config.graph, this.config.mute, this.config.showReverseCommits); this.repoDropdown = new Dropdown('repoDropdown', true, false, 'Repos', (values) => { this.loadRepo(values[0]); @@ -91,6 +92,14 @@ class GitGraphView { this.requestLoadRepoInfoAndCommits(true, true); }); + this.showReverseCommitsElem = document.getElementById('showReverseCommitsCheckbox')!; + this.showReverseCommitsElem.addEventListener('change', () => { + let reverse: boolean = this.showReverseCommitsElem.checked; + this.saveRepoStateValue(this.currentRepo, 'showReverseCommits', reverse); + this.graph.setShowReverseCommits(reverse); + this.refresh(true); + }); + this.showRemoteBranchesElem = document.getElementById('showRemoteBranchesCheckbox')!; this.showRemoteBranchesElem.addEventListener('change', () => { this.saveRepoStateValue(this.currentRepo, 'showRemoteBranchesV2', this.showRemoteBranchesElem.checked ? GG.BooleanOverride.Enabled : GG.BooleanOverride.Disabled); @@ -126,9 +135,14 @@ class GitGraphView { this.avatars = prevState.avatars; this.gitConfig = prevState.gitConfig; this.loadRepoInfo(prevState.gitBranches, prevState.gitBranchHead, prevState.gitRemotes, prevState.gitStashes, true); - this.loadCommits(prevState.commits, prevState.commitHead, prevState.gitTags, prevState.moreCommitsAvailable, prevState.onlyFollowFirstParent); this.findWidget.restoreState(prevState.findWidget); this.settingsWidget.restoreState(prevState.settingsWidget); + + let rc = this.gitRepos[prevState.currentRepo].showReverseCommits; + this.loadCommits(prevState.commits, prevState.commitHead, prevState.gitTags, prevState.moreCommitsAvailable, prevState.onlyFollowFirstParent, rc); + this.showReverseCommitsElem.checked = rc; + this.graph.setShowReverseCommits(rc); + this.showRemoteBranchesElem.checked = getShowRemoteBranches(this.gitRepos[prevState.currentRepo].showRemoteBranchesV2); } @@ -207,6 +221,8 @@ class GitGraphView { private loadRepo(repo: string) { this.currentRepo = repo; this.currentRepoLoading = true; + this.showReverseCommitsElem.checked = this.gitRepos[this.currentRepo].showReverseCommits; + this.graph.setShowReverseCommits(this.showReverseCommitsElem.checked); this.showRemoteBranchesElem.checked = getShowRemoteBranches(this.gitRepos[this.currentRepo].showRemoteBranchesV2); this.maxCommits = this.config.initialLoadCommits; this.gitConfig = null; @@ -300,7 +316,7 @@ class GitGraphView { } } - private loadCommits(commits: GG.GitCommit[], commitHead: string | null, tags: ReadonlyArray, moreAvailable: boolean, onlyFollowFirstParent: boolean) { + private loadCommits(commits: GG.GitCommit[], commitHead: string | null, tags: ReadonlyArray, moreAvailable: boolean, onlyFollowFirstParent: boolean, showReverseCommits: boolean) { // This list of tags is just used to provide additional information in the dialogs. Tag information included in commits is used for all other purposes (e.g. rendering, context menus) const tagsChanged = !arraysStrictlyEqual(this.gitTags, tags); this.gitTags = tags; @@ -372,7 +388,7 @@ class GitGraphView { this.saveState(); - this.graph.loadCommits(this.commits, this.commitHead, this.commitLookup, this.onlyFollowFirstParent); + this.graph.loadCommits(this.commits, this.commitHead, this.commitLookup, this.onlyFollowFirstParent, showReverseCommits); this.render(); if (currentRepoLoading && this.config.onRepoLoad.scrollToHead && this.commitHead !== null) { @@ -453,7 +469,7 @@ class GitGraphView { this.renderedGitBranchHead = null; this.closeCommitDetails(false); this.saveState(); - this.graph.loadCommits(this.commits, this.commitHead, this.commitLookup, this.onlyFollowFirstParent); + this.graph.loadCommits(this.commits, this.commitHead, this.commitLookup, this.onlyFollowFirstParent, this.showReverseCommitsElem.checked); this.tableElem.innerHTML = ''; this.footerElem.innerHTML = ''; this.renderGraph(); @@ -475,7 +491,7 @@ class GitGraphView { if (msg.error === null) { const refreshState = this.currentRepoRefreshState; if (refreshState.inProgress && refreshState.loadCommitsRefreshId === msg.refreshId) { - this.loadCommits(msg.commits, msg.head, msg.tags, msg.moreCommitsAvailable, msg.onlyFollowFirstParent); + this.loadCommits(msg.commits, msg.head, msg.tags, msg.moreCommitsAvailable, msg.onlyFollowFirstParent, msg.showReverseCommits); } } else { const error = this.gitBranches.length === 0 && msg.error.indexOf('bad revision \'HEAD\'') > -1 @@ -611,6 +627,7 @@ class GitGraphView { branches: this.currentBranches === null || (this.currentBranches.length === 1 && this.currentBranches[0] === SHOW_ALL_BRANCHES) ? null : this.currentBranches, maxCommits: this.maxCommits, showTags: getShowTags(repoState.showTags), + showReverseCommits: repoState.showReverseCommits, showRemoteBranches: getShowRemoteBranches(repoState.showRemoteBranchesV2), includeCommitsMentionedByReflogs: getIncludeCommitsMentionedByReflogs(repoState.includeCommitsMentionedByReflogs), onlyFollowFirstParent: getOnlyFollowFirstParent(repoState.onlyFollowFirstParent), @@ -3758,6 +3775,12 @@ function getCommitOrdering(repoValue: GG.RepoCommitOrdering): GG.CommitOrdering } } +// function getShowReverseCommits(repoValue: GG.BooleanOverride) { +// return repoValue === GG.BooleanOverride.Default +// ? initialState.config.showReverseCommits +// : repoValue === GG.BooleanOverride.Enabled; +// } + function getShowRemoteBranches(repoValue: GG.BooleanOverride) { return repoValue === GG.BooleanOverride.Default ? initialState.config.showRemoteBranches