Skip to content

Commit 711dbc0

Browse files
AtkinsSJKernelDeimos
authored andcommitted
feat(git): Understand references like HEAD^ and main~3
There are a lot of ways of specifying a revision, but these are a couple of common ones.
1 parent adcd3db commit 711dbc0

File tree

1 file changed

+124
-4
lines changed

1 file changed

+124
-4
lines changed

packages/git/src/git-helpers.js

+124-4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
*/
1919
import path from 'path-browserify';
2020
import git from 'isomorphic-git';
21+
import { GrammarContext, standard_parsers } from '@heyputer/parsely/exports.js';
22+
import { StringStream } from '@heyputer/parsely/streams.js';
2123

2224
/**
2325
* Attempt to locate the git repository directory.
@@ -156,16 +158,97 @@ export const group_positional_arguments = (arg_tokens) => {
156158
return result;
157159
}
158160

161+
/**
162+
* Parse a ref string such as `HEAD`, `master^^^` or `tags/foo~3` into a usable format.
163+
* @param ref_string
164+
* @returns {{rev: string, suffixes: [{type: string, n: number}]}}
165+
*/
166+
const parse_ref = (ref_string) => {
167+
const grammar_context = new GrammarContext({
168+
...standard_parsers(),
169+
});
170+
171+
// See description at https://git-scm.com/docs/gitrevisions#_specifying_revisions
172+
const parser = grammar_context.define_parser({
173+
// sha-1 and named refs are ambiguous (eg, deadbeef can be either) so we treat them the same
174+
// TODO: This is not a complete list of valid characters.
175+
// See https://git-scm.com/docs/git-check-ref-format#_description
176+
rev: a => a.stringOf(c => /[\w/.-]/.test(c)),
177+
178+
suffix: a => a.firstMatch(
179+
a.symbol('parent'),
180+
a.symbol('ancestor'),
181+
),
182+
parent: a => a.sequence(
183+
a.literal('^'),
184+
a.optional(
185+
a.symbol('number'),
186+
),
187+
),
188+
ancestor: a => a.sequence(
189+
a.literal('~'),
190+
a.optional(
191+
a.symbol('number'),
192+
),
193+
),
194+
195+
number: a => a.stringOf(c => /\d/.test(c)),
196+
197+
ref: a => a.sequence(
198+
a.symbol('rev'),
199+
a.optional(
200+
a.repeat(
201+
a.symbol('suffix')
202+
),
203+
),
204+
),
205+
}, {
206+
parent: it => {
207+
if (it.length === 2)
208+
return { type: 'parent', n: it[1].value };
209+
return { type: 'parent', n: 1 };
210+
},
211+
ancestor: it => {
212+
if (it.length === 2)
213+
return { type: 'ancestor', n: it[1].value };
214+
return { type: 'ancestor', n: 1 };
215+
},
216+
217+
number: n => parseInt(n, 10),
218+
219+
ref: it => {
220+
const rev = it[0].value;
221+
const suffixes = it[1]?.value?.map(s => s.value);
222+
return { rev, suffixes }
223+
}
224+
});
225+
226+
const stream = new StringStream(ref_string);
227+
const result = parser(stream, 'ref', { must_consume_all_input: true });
228+
return result.value;
229+
}
230+
159231
/**
160232
* Take some kind of reference, and resolve it to a full oid if possible.
161233
* @param git_context Object of common parameters to isomorphic-git methods
162234
* @param ref Reference to resolve
163235
* @returns {Promise<string>} Full oid, or a thrown Error
164236
*/
165237
export const resolve_to_oid = async (git_context, ref) => {
238+
239+
let parsed_ref;
240+
try {
241+
parsed_ref = parse_ref(ref);
242+
} catch (e) {
243+
throw new Error(`Unable to resolve reference '${ref}'`);
244+
}
245+
246+
const revision = parsed_ref.rev;
247+
const suffixes = parsed_ref.suffixes;
248+
166249
const [ resolved_oid, expanded_oid ] = await Promise.allSettled([
167-
git.resolveRef({ ...git_context, ref }),
168-
git.expandOid({ ...git_context, oid: ref }),
250+
git.resolveRef({ ...git_context, ref: revision }),
251+
git.expandOid({ ...git_context, oid: revision }),
169252
]);
170253
let oid;
171254
if (resolved_oid.status === 'fulfilled') {
@@ -175,8 +258,45 @@ export const resolve_to_oid = async (git_context, ref) => {
175258
} else {
176259
throw new Error(`Unable to resolve reference '${ref}'`);
177260
}
178-
// TODO: Advanced revision selection, see https://git-scm.com/book/en/v2/Git-Tools-Revision-Selection
179-
// and https://git-scm.com/docs/gitrevisions
261+
262+
if (suffixes?.length) {
263+
for (const suffix of suffixes) {
264+
let commit;
265+
try {
266+
commit = await git.readCommit({ ...git_context, oid });
267+
} catch (e) {
268+
throw new Error(`bad revision '${ref}'`);
269+
}
270+
271+
switch (suffix.type) {
272+
case 'ancestor': {
273+
for (let i = 0; i < suffix.n; ++i) {
274+
oid = commit.commit.parent[0];
275+
try {
276+
commit = await git.readCommit({ ...git_context, oid });
277+
} catch (e) {
278+
throw new Error(`bad revision '${ref}'`);
279+
}
280+
}
281+
break;
282+
}
283+
case 'parent': {
284+
// "As a special rule, <rev>^0 means the commit itself and is used when <rev> is the object name of
285+
// a tag object that refers to a commit object."
286+
if (suffix.n === 0)
287+
continue;
288+
289+
oid = commit.commit.parent[suffix.n - 1];
290+
if (!oid)
291+
throw new Error(`bad revision '${ref}'`);
292+
break;
293+
}
294+
default:
295+
throw new Error(`Unable to resolve reference '${ref}' (unimplemented suffix '${suffix.type}')`);
296+
}
297+
}
298+
}
299+
180300
return oid;
181301
}
182302

0 commit comments

Comments
 (0)