18
18
*/
19
19
import path from 'path-browserify' ;
20
20
import git from 'isomorphic-git' ;
21
+ import { GrammarContext , standard_parsers } from '@heyputer/parsely/exports.js' ;
22
+ import { StringStream } from '@heyputer/parsely/streams.js' ;
21
23
22
24
/**
23
25
* Attempt to locate the git repository directory.
@@ -156,16 +158,97 @@ export const group_positional_arguments = (arg_tokens) => {
156
158
return result ;
157
159
}
158
160
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
+
159
231
/**
160
232
* Take some kind of reference, and resolve it to a full oid if possible.
161
233
* @param git_context Object of common parameters to isomorphic-git methods
162
234
* @param ref Reference to resolve
163
235
* @returns {Promise<string> } Full oid, or a thrown Error
164
236
*/
165
237
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
+
166
249
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 } ) ,
169
252
] ) ;
170
253
let oid ;
171
254
if ( resolved_oid . status === 'fulfilled' ) {
@@ -175,8 +258,45 @@ export const resolve_to_oid = async (git_context, ref) => {
175
258
} else {
176
259
throw new Error ( `Unable to resolve reference '${ ref } '` ) ;
177
260
}
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
+
180
300
return oid ;
181
301
}
182
302
0 commit comments