From 45cdfcb5bfa66937b33054a127e0b17001f3faa4 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Wed, 26 Jun 2024 17:09:15 +0100 Subject: [PATCH] feat(git): Display ref names in `git log` and `git show` --- packages/git/src/format.js | 45 ++++++++++++++---- packages/git/src/refs.js | 93 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 packages/git/src/refs.js diff --git a/packages/git/src/format.js b/packages/git/src/format.js index 84f0acb1..fe9a00b7 100644 --- a/packages/git/src/format.js +++ b/packages/git/src/format.js @@ -18,6 +18,7 @@ */ import { shorten_hash } from './git-helpers.js'; import chalk from 'chalk'; +import { get_matching_refs } from './refs.js'; export const commit_formatting_options = { 'abbrev-commit': { @@ -68,9 +69,35 @@ export const process_commit_formatting_options = (options) => { * @param short_hashes Whwther to shorten the hash * @returns {String} */ -export const format_oid = async (git_context, oid, { short_hashes = false } = {}) => { - // TODO: List refs at this commit, after the hash - return short_hashes ? shorten_hash(git_context, oid) : oid; +export const format_commit_oid = async (git_context, oid, { short_hashes = false } = {}) => { + const hash = short_hashes ? await shorten_hash(git_context, oid) : oid; + + const refs = await get_matching_refs(git_context, oid); + if (refs.length === 0) + return hash; + + let s = `${hash} (`; + s += refs.map(ref => { + // Different kinds of ref are styled differently, but all are in bold: + // HEAD and local branches are cyan + if (ref === 'HEAD') { + // TODO: If HEAD points to another ref, that should be shown here as `HEAD -> other` + return chalk.bold.cyan(ref); + } + if (ref.startsWith('refs/heads/')) + return chalk.bold.cyanBright(ref.slice('refs/heads/'.length)); + // Tags are `tag: foo` in yellow + if (ref.startsWith('refs/tags/')) + return chalk.bold.yellowBright(`tag: ${ref.slice('refs/tags/'.length)}`); + // Remote branches are red + if (ref.startsWith('refs/remotes/')) + return chalk.bold.red(ref.slice('refs/remotes/'.length)); + // Assuming there's anything else, we'll just bold it. + return chalk.bold(ref); + }).join(', '); + s += ')'; + + return s; } /** @@ -124,10 +151,10 @@ export const format_commit = async (git_context, commit, oid, options = {}) => { switch (options.format || 'medium') { // TODO: Other formats case 'oneline': - return `${chalk.yellow(await format_oid(git_context, oid, options))} ${title_line()}`; + return `${chalk.yellow(await format_commit_oid(git_context, oid, options))} ${title_line()}`; case 'short': { let s = ''; - s += chalk.yellow(`commit ${await format_oid(git_context, oid, options)}\n`); + s += chalk.yellow(`commit ${await format_commit_oid(git_context, oid, options)}\n`); s += `Author: ${format_person(commit.author)}\n`; s += '\n'; s += indent(title_line()); @@ -135,7 +162,7 @@ export const format_commit = async (git_context, commit, oid, options = {}) => { } case 'medium': { let s = ''; - s += chalk.yellow(`commit ${await format_oid(git_context, oid, options)}\n`); + s += chalk.yellow(`commit ${await format_commit_oid(git_context, oid, options)}\n`); s += `Author: ${format_person(commit.author)}\n`; s += `Date: ${format_date(commit.author)}\n`; s += '\n'; @@ -144,7 +171,7 @@ export const format_commit = async (git_context, commit, oid, options = {}) => { } case 'full': { let s = ''; - s += chalk.yellow(`commit ${await format_oid(git_context, oid, options)}\n`); + s += chalk.yellow(`commit ${await format_commit_oid(git_context, oid, options)}\n`); s += `Author: ${format_person(commit.author)}\n`; s += `Commit: ${format_person(commit.committer)}\n`; s += '\n'; @@ -153,7 +180,7 @@ export const format_commit = async (git_context, commit, oid, options = {}) => { } case 'fuller': { let s = ''; - s += chalk.yellow(`commit ${await format_oid(git_context, oid, options)}\n`); + s += chalk.yellow(`commit ${await format_commit_oid(git_context, oid, options)}\n`); s += `Author: ${format_person(commit.author)}\n`; s += `AuthorDate: ${format_date(commit.author)}\n`; s += `Commit: ${format_person(commit.committer)}\n`; @@ -164,7 +191,7 @@ export const format_commit = async (git_context, commit, oid, options = {}) => { } case 'raw': { let s = ''; - s += chalk.yellow(`commit ${oid}\n`); + s += chalk.yellow(`commit ${await format_commit_oid(git_context, oid, options)}\n`); s += `tree ${commit.tree}\n`; if (commit.parent[0]) s += `parent ${commit.parent[0]}\n`; diff --git a/packages/git/src/refs.js b/packages/git/src/refs.js new file mode 100644 index 00000000..c03d125b --- /dev/null +++ b/packages/git/src/refs.js @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Puter's Git client. + * + * Puter's Git client is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +// Map of hash -> array of full reference names +import git from 'isomorphic-git'; + +const hash_to_refs = new Map(); +const add_hash = (hash, ref) => { + const existing_array = hash_to_refs.get(hash); + if (existing_array) { + existing_array.push(ref); + } else { + hash_to_refs.set(hash, [ref]); + } +} + +// Avoid loading everything multiple times +let mark_cache_loaded; +let started_loading = false; +const cache_loaded = new Promise(resolve => { mark_cache_loaded = resolve }); + +/** + * Reverse search from a commit hash to the refs that point to it. + * The first time this is called, we retrieve all the references and cache them, meaning that + * later calls are much faster, but won't reflect changes. + * @param git_context {{ fs, dir, gitdir, cache }} as taken by most isomorphic-git methods. + * @param commit_oid + * @returns {Promise<[string]>} An array of full references, eg `HEAD`, `refs/heads/main`, `refs/tags/foo`, or `refs/remotes/origin/main` + */ +export const get_matching_refs = async (git_context, commit_oid) => { + if (started_loading) { + // If someone else started loading the cache, just wait for it to be ready + await cache_loaded; + } else { + // Otherwise, we have to load it! + started_loading = true; + + // HEAD + add_hash(await git.resolveRef({ ...git_context, ref: 'HEAD' }), 'HEAD'); + + // Branches + const branch_names = await git.listBranches(git_context); + for (const branch of branch_names) { + const ref = `refs/heads/${branch}`; + add_hash(await git.resolveRef({ ...git_context, ref}), ref); + } + + // Tags + const tags = await git.listTags(git_context); + for (const tag of tags) + add_hash(await git.resolveRef({ ...git_context, ref: tag }), `refs/tags/${tag}`); + + // Remote branches + const remotes = await git.listRemotes(git_context); + for (const { remote } of remotes) { + const remote_branches = await git.listBranches({ ...git_context, remote }); + for (const branch of remote_branches) { + const ref = `refs/remotes/${remote}/${branch}`; + add_hash(await git.resolveRef({ ...git_context, ref }), ref); + } + } + + if (window.DEBUG) { + console.groupCollapsed('Collected refs'); + for (const [ hash, ref_list ] of hash_to_refs) { + console.groupCollapsed(hash); + for (const ref of ref_list) + console.log(ref); + console.groupEnd(); + } + console.groupEnd(); + } + mark_cache_loaded(); + } + + return hash_to_refs.get(commit_oid) ?? []; +}