// The JavaScript code used to build and control the graph // Get the graph container HTML element. const graphContainer = document.getElementById("graph-container"); // Instantiate the graph. // By the way, none of this really seems to be documented anywhere. // For the styles, try and work it out from the typescript file: // https://github.com/nicoespeon/gitgraph.js/blob/9239bab87f6f/packages/gitgraph-core/src/template.ts const font = "normal 14px sans-serif"; const smallFont = "normal 8px sans-serif"; const gitgraph = GitgraphJS.createGitgraph(graphContainer, { template: GitgraphJS.templateExtend(GitgraphJS.TemplateName.Metro, { // Make everything not-absurdly-huge commit: { dot: { size: 6, }, spacing: 22, message: { displayAuthor: false, font: font, // Our custom colours don't look very nice on text, and // it'd also be a pain for links. color: "#333", }, }, branch: { lineWidth: 2, spacing: 18, label: { // Due to some alignment issues, this won't work. // We'll just misuse tags to display branches instead. display: false, font: smallFont, }, }, tag: { font: smallFont, }, // Sadly three colours for all the branches isn't enough, since // the defaults look very nice. colors: [ "#979797", "#008fb5", "#f1c109", // Defaults "#e78e49", "#c524d3", "#89e830", "#b72222", "#69e5d1", "#344d34", ], }), author: "Unknown Author", commitMessage: "Unknown commit message", }); // Use a custom system for laying out branches, so they can reuse // the space left by a previous one. // Copied and modified from GitgraphCore.withPosition: // https://github.com/nicoespeon/gitgraph.js/blob/9239bab87f6f/packages/gitgraph-core/src/gitgraph.ts gitgraph._graph.withPosition = function (rows, branchesOrder, commit) { const row = rows.getRowOf(commit.hash); const maxRow = rows.getMaxRow(); const order = commit.btCommit.column; return commit.setPosition({ x: this.initCommitOffsetX + this.template.branch.spacing * order, y: this.initCommitOffsetY + this.template.commit.spacing * (maxRow - row), }); } gitgraph._graph.computeCommitMessagesX = function (branchesPaths) { // const numberOfColumns = Array.from(branchesPaths).length; const numberOfColumns = this.btNumColumns; return numberOfColumns * this.template.branch.spacing; } let byHash = {}; // When a commit is forked, one commit needs to 'own' the old // branch to keep the most important commits in a single // straight line. // This function recursively plans for that. function planCommit(commit) { if (commit.entered) return; commit.entered = true; if (commit.Parents.length === 0) return; for (let parentHash of commit.Parents) { planCommit(byHash[parentHash]); } let firstParent = byHash[commit.Parents[0]]; // 'commits' are actually just branches, so we can't re-use // one if it's already in use by something else. if (!firstParent.branchReservedFor) { firstParent.branchReservedFor = commit; } } function renderCommit(commit) { let visualBase if (commit.Parents.length === 0) { visualBase = gitgraph.branch("initial-branch"); } else { let firstParent = byHash[commit.Parents[0]]; if (firstParent.branchReservedFor === commit) { visualBase = firstParent.visual; } else { visualBase = firstParent.visual.branch("br-" + commit.Hash); } } let additionalParents = commit.Parents.slice(1); // Dig around and use the private API, since there's no way to handle // octopus merges though the API. visualBase._commitWithParents({ subject: commit.Subject, hash: commit.Hash, renderMessage: commit.HasBuilds ? createCommitLink : undefined, }, additionalParents); gitgraph._graph.commits.at(-1).btCommit = commit; // We'll use this in withPosition visualBase._onGraphUpdate(); // Maybe we can defer this for better performance? commit.visual = visualBase; // Use tags to represent branches, since they fit in the compact line spacing. for (let branch of commit.Branches) { commit.visual.tag(branch); } } function createCommitLink(commit) { const url = siteUrlPrefix + "/view_build?commit=" + commit.hash; // Derived from createText in svg-elements.ts const text = document.createElementNS("http://www.w3.org/2000/svg", "text"); text.setAttribute("alignment-baseline", "central"); text.setAttribute("dominant-baseline", "central"); // Copy exactly what the default renderer does text.textContent = commit.message; text.setAttribute("y", commit.style.dot.size); text.setAttribute("fill", "#33c"); text.setAttribute("style", `font: ${commit.style.message.font}`); text.setAttribute("text-decoration", "underline"); const link = document.createElementNS("http://www.w3.org/2000/svg", "a"); link.setAttributeNS("http://www.w3.org/1999/xlink", "href", url); link.appendChild(text); return link; } let branches = {}; for (let commit of commitData.Commits) { // We can only really do one branch like this if (commit.Branches != null) { let name = commit.Branches[0]; branches[name] = commit; } // Go's arrays can be null, set them here to avoid having to check later. if (commit.Parents == null) commit.Parents = []; if (commit.Branches == null) commit.Branches = []; commit.children = []; byHash[commit.Hash] = commit; } // Build the commit children lists for (let commit of commitData.Commits) { for (let parentHash of commit.Parents) { byHash[parentHash].children.push(commit); } } // Figure out where all the forks are going to be. for (let name of commitData.PriorityBranches) { planCommit(branches[name]); } for (let name in branches) { planCommit(branches[name]); } // Sort the commits into a linear history let placed = new Set(); let ordered = []; let nextCommits = new Set(); for (let commit of commitData.Commits) { if (commit.children.length === 0) nextCommits.add(commit); } // nextCommits.add(byHash["1bb6b0d9bfd528e2b6a92464646b319baa854987"]); while (nextCommits.size !== 0) { let newest = null; for (let available of nextCommits) { // If this is the first commit, select it if (newest == null) { newest = available; continue; } // If this commit is newer than the previous newest one, use it let newestWhen = newest.AuthorTS; let availWhen = available.AuthorTS; if (newestWhen < availWhen) { newest = available; continue; } // If it has the same date (likely because it was rebased between // branches), sort by the commit IDs so the ordering is stable and // doesn't shuffle around between refreshes. if (newestWhen === availWhen && newest.Hash.localeCompare(available.Hash, 'en') > 0) { newest = available; } } if (newest === null) { panic(fmt.Sprintf("Found null newest commit, with the remaining commits: %v", nextCommits)); } nextCommits.delete(newest); placed.add(newest); ordered.push(newest); newest.position = ordered.length; for (let parent of newest.Parents) { let commit = byHash[parent] if (commit == null) { panic(fmt.Sprintf("Invalid null commit for ID '%s'", parent)); } // Check this commit hasn't already been placed if (placed.has(commit)) { continue; } // Check that all of its children have been placed let allPlaced = true; for (let child of commit.children) { if (!placed.has(child)) { allPlaced = false; break; } } if (!allPlaced) continue; nextCommits.add(commit); } } // We have to go oldest-to-newest ordered.reverse(); // console.log(ordered); // Choose which columns all the commits go in // We'll do this here rather than letting gitgraph do it for // us, as we know when the branches end and can reuse the // column for another branch, while it places every branch // in a new column. // Note we can get a case like this: // | * c // | * | b // *-┴-┘ a // If we used the obvious algorithm for laying out the columns // (each commit that's branching finds the first column that's // not currently occupied) then b and c would be laid out in // the same column. Thus we have to keep a list of the commits // in each column, and iterate over them until we go to before // the 'a' commit. This is done with the commitsPerColumn map, // which lets us only iterate over the commits on that specific // column. let usedColumns = new Set(); let commitsPerColumn = {}; let maxColumn = 0; for (let commit of ordered) { // If this commit shares it's parent's branch, then it // also shares the column. let sameAsParent = false; let column = null; let firstParent = null; if (commit.Parents.length !== 0) { firstParent = byHash[commit.Parents[0]]; if (firstParent.branchReservedFor === commit) { column = firstParent.column; sameAsParent = true; } } // Otherwise we need a new column - pick one now. if (!sameAsParent) { for (let i = 0; ; i++) { // Check if a future commit will use this column if (usedColumns.has(i)) continue; // Check if this column has been used in the past // between us and our first parent - if so avoid // it, since our line would overlap with another // branch. let commits = commitsPerColumn[i]; if (firstParent && commits) { // Note that lower numbers are newer, so if the latest // commit in that column is older than our parent commit, it // can't be blocking that column. if (commits.at(-1).position < firstParent.position) { continue; } } column = i; usedColumns.add(i); break; } } commit.column = column; if (column in commitsPerColumn) { commitsPerColumn[column].push(commit); } else { commitsPerColumn[column] = [commit]; } if (column > maxColumn) { maxColumn = column; } // If this commit doesn't have any children that continue this // branch, free up it's column. if (!commit.branchReservedFor) { usedColumns.delete(column); } } gitgraph._graph.btNumColumns = maxColumn + 1; // Columns are zero-indexed // Render the commits for (let commit of ordered) { renderCommit(commit); } // renderCommit(byHash["8e840b5be5989bfe7a603c8c5e365b67f041b9d4"]); // Simulate git commands with Gitgraph API. // const master = gitgraph.branch("master"); // master.commit("Initial commit"); // // const develop = master.branch("develop"); // develop.commit("Add TypeScript"); // // const aFeature = develop.branch("a-feature"); // aFeature // .commit("Make it work") // .commit("Make it right") // .commit("Make it fast"); // // develop.merge(aFeature); // develop.commit("Prepare v1"); // // master.merge(develop).tag("v1.0.0");