Skip to content

Commit 45cdfcb

Browse files
AtkinsSJKernelDeimos
authored andcommitted
feat(git): Display ref names in git log and git show
1 parent 351d3f5 commit 45cdfcb

File tree

2 files changed

+129
-9
lines changed

2 files changed

+129
-9
lines changed

packages/git/src/format.js

+36-9
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919
import { shorten_hash } from './git-helpers.js';
2020
import chalk from 'chalk';
21+
import { get_matching_refs } from './refs.js';
2122

2223
export const commit_formatting_options = {
2324
'abbrev-commit': {
@@ -68,9 +69,35 @@ export const process_commit_formatting_options = (options) => {
6869
* @param short_hashes Whwther to shorten the hash
6970
* @returns {String}
7071
*/
71-
export const format_oid = async (git_context, oid, { short_hashes = false } = {}) => {
72-
// TODO: List refs at this commit, after the hash
73-
return short_hashes ? shorten_hash(git_context, oid) : oid;
72+
export const format_commit_oid = async (git_context, oid, { short_hashes = false } = {}) => {
73+
const hash = short_hashes ? await shorten_hash(git_context, oid) : oid;
74+
75+
const refs = await get_matching_refs(git_context, oid);
76+
if (refs.length === 0)
77+
return hash;
78+
79+
let s = `${hash} (`;
80+
s += refs.map(ref => {
81+
// Different kinds of ref are styled differently, but all are in bold:
82+
// HEAD and local branches are cyan
83+
if (ref === 'HEAD') {
84+
// TODO: If HEAD points to another ref, that should be shown here as `HEAD -> other`
85+
return chalk.bold.cyan(ref);
86+
}
87+
if (ref.startsWith('refs/heads/'))
88+
return chalk.bold.cyanBright(ref.slice('refs/heads/'.length));
89+
// Tags are `tag: foo` in yellow
90+
if (ref.startsWith('refs/tags/'))
91+
return chalk.bold.yellowBright(`tag: ${ref.slice('refs/tags/'.length)}`);
92+
// Remote branches are red
93+
if (ref.startsWith('refs/remotes/'))
94+
return chalk.bold.red(ref.slice('refs/remotes/'.length));
95+
// Assuming there's anything else, we'll just bold it.
96+
return chalk.bold(ref);
97+
}).join(', ');
98+
s += ')';
99+
100+
return s;
74101
}
75102

76103
/**
@@ -124,18 +151,18 @@ export const format_commit = async (git_context, commit, oid, options = {}) => {
124151
switch (options.format || 'medium') {
125152
// TODO: Other formats
126153
case 'oneline':
127-
return `${chalk.yellow(await format_oid(git_context, oid, options))} ${title_line()}`;
154+
return `${chalk.yellow(await format_commit_oid(git_context, oid, options))} ${title_line()}`;
128155
case 'short': {
129156
let s = '';
130-
s += chalk.yellow(`commit ${await format_oid(git_context, oid, options)}\n`);
157+
s += chalk.yellow(`commit ${await format_commit_oid(git_context, oid, options)}\n`);
131158
s += `Author: ${format_person(commit.author)}\n`;
132159
s += '\n';
133160
s += indent(title_line());
134161
return s;
135162
}
136163
case 'medium': {
137164
let s = '';
138-
s += chalk.yellow(`commit ${await format_oid(git_context, oid, options)}\n`);
165+
s += chalk.yellow(`commit ${await format_commit_oid(git_context, oid, options)}\n`);
139166
s += `Author: ${format_person(commit.author)}\n`;
140167
s += `Date: ${format_date(commit.author)}\n`;
141168
s += '\n';
@@ -144,7 +171,7 @@ export const format_commit = async (git_context, commit, oid, options = {}) => {
144171
}
145172
case 'full': {
146173
let s = '';
147-
s += chalk.yellow(`commit ${await format_oid(git_context, oid, options)}\n`);
174+
s += chalk.yellow(`commit ${await format_commit_oid(git_context, oid, options)}\n`);
148175
s += `Author: ${format_person(commit.author)}\n`;
149176
s += `Commit: ${format_person(commit.committer)}\n`;
150177
s += '\n';
@@ -153,7 +180,7 @@ export const format_commit = async (git_context, commit, oid, options = {}) => {
153180
}
154181
case 'fuller': {
155182
let s = '';
156-
s += chalk.yellow(`commit ${await format_oid(git_context, oid, options)}\n`);
183+
s += chalk.yellow(`commit ${await format_commit_oid(git_context, oid, options)}\n`);
157184
s += `Author: ${format_person(commit.author)}\n`;
158185
s += `AuthorDate: ${format_date(commit.author)}\n`;
159186
s += `Commit: ${format_person(commit.committer)}\n`;
@@ -164,7 +191,7 @@ export const format_commit = async (git_context, commit, oid, options = {}) => {
164191
}
165192
case 'raw': {
166193
let s = '';
167-
s += chalk.yellow(`commit ${oid}\n`);
194+
s += chalk.yellow(`commit ${await format_commit_oid(git_context, oid, options)}\n`);
168195
s += `tree ${commit.tree}\n`;
169196
if (commit.parent[0])
170197
s += `parent ${commit.parent[0]}\n`;

packages/git/src/refs.js

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright (C) 2024 Puter Technologies Inc.
3+
*
4+
* This file is part of Puter's Git client.
5+
*
6+
* Puter's Git client is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Affero General Public License as published
8+
* by the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Affero General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
20+
// Map of hash -> array of full reference names
21+
import git from 'isomorphic-git';
22+
23+
const hash_to_refs = new Map();
24+
const add_hash = (hash, ref) => {
25+
const existing_array = hash_to_refs.get(hash);
26+
if (existing_array) {
27+
existing_array.push(ref);
28+
} else {
29+
hash_to_refs.set(hash, [ref]);
30+
}
31+
}
32+
33+
// Avoid loading everything multiple times
34+
let mark_cache_loaded;
35+
let started_loading = false;
36+
const cache_loaded = new Promise(resolve => { mark_cache_loaded = resolve });
37+
38+
/**
39+
* Reverse search from a commit hash to the refs that point to it.
40+
* The first time this is called, we retrieve all the references and cache them, meaning that
41+
* later calls are much faster, but won't reflect changes.
42+
* @param git_context {{ fs, dir, gitdir, cache }} as taken by most isomorphic-git methods.
43+
* @param commit_oid
44+
* @returns {Promise<[string]>} An array of full references, eg `HEAD`, `refs/heads/main`, `refs/tags/foo`, or `refs/remotes/origin/main`
45+
*/
46+
export const get_matching_refs = async (git_context, commit_oid) => {
47+
if (started_loading) {
48+
// If someone else started loading the cache, just wait for it to be ready
49+
await cache_loaded;
50+
} else {
51+
// Otherwise, we have to load it!
52+
started_loading = true;
53+
54+
// HEAD
55+
add_hash(await git.resolveRef({ ...git_context, ref: 'HEAD' }), 'HEAD');
56+
57+
// Branches
58+
const branch_names = await git.listBranches(git_context);
59+
for (const branch of branch_names) {
60+
const ref = `refs/heads/${branch}`;
61+
add_hash(await git.resolveRef({ ...git_context, ref}), ref);
62+
}
63+
64+
// Tags
65+
const tags = await git.listTags(git_context);
66+
for (const tag of tags)
67+
add_hash(await git.resolveRef({ ...git_context, ref: tag }), `refs/tags/${tag}`);
68+
69+
// Remote branches
70+
const remotes = await git.listRemotes(git_context);
71+
for (const { remote } of remotes) {
72+
const remote_branches = await git.listBranches({ ...git_context, remote });
73+
for (const branch of remote_branches) {
74+
const ref = `refs/remotes/${remote}/${branch}`;
75+
add_hash(await git.resolveRef({ ...git_context, ref }), ref);
76+
}
77+
}
78+
79+
if (window.DEBUG) {
80+
console.groupCollapsed('Collected refs');
81+
for (const [ hash, ref_list ] of hash_to_refs) {
82+
console.groupCollapsed(hash);
83+
for (const ref of ref_list)
84+
console.log(ref);
85+
console.groupEnd();
86+
}
87+
console.groupEnd();
88+
}
89+
mark_cache_loaded();
90+
}
91+
92+
return hash_to_refs.get(commit_oid) ?? [];
93+
}

0 commit comments

Comments
 (0)