• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

discoveryjs / scan-git / 11603961725

31 Oct 2024 12:38AM UTC coverage: 87.595% (+0.3%) from 87.257%
11603961725

push

github

lahmatiy
Add `repo.currentBranch()` method

344 of 387 branches covered (88.89%)

Branch coverage included in aggregate %.

29 of 33 new or added lines in 1 file covered. (87.88%)

5 existing lines in 1 file now uncovered.

2304 of 2636 relevant lines covered (87.41%)

263.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.39
/src/resolve-ref.ts
1
import { promises as fsPromises, existsSync } from 'fs';
1✔
2
import { join as pathJoin, basename, sep as pathSep } from 'path';
1✔
3
import { scanFs } from '@discoveryjs/scan-fs';
1✔
4

1✔
5
type Ref = {
1✔
6
    name: string;
1✔
7
    oid: string;
1✔
8
};
1✔
9
type RefSourceInfo = {
1✔
10
    ref: string | null;
1✔
11
    oid: string | null;
1✔
12
};
1✔
13
type LooseRefFile = {
1✔
14
    path: string;
1✔
15
    content: string | null;
1✔
16
};
1✔
17
type RefResolver = {
1✔
18
    remotes: string[];
1✔
19
    names: string[];
1✔
20
    exists(ref: string): boolean;
1✔
21
    resolveOid(ref: string): Promise<string>;
1✔
22
    resolve(ref: string): Promise<RefSourceInfo>;
1✔
23
};
1✔
24
type RefInfo = {
1✔
25
    path: string;
1✔
26
    name: string;
1✔
27
    symbolic: boolean;
1✔
28
    scope: string;
1✔
29
    namespace: string;
1✔
30
    category: string;
1✔
31
    remote: string | null;
1✔
32
    ref: string | null;
1✔
33
    oid: string | null;
1✔
34
};
1✔
35

1✔
36
// NOTICE: Don't forget to update README.md when change the values
1✔
37
const symbolicRefs = new Set(['HEAD', 'FETCH_HEAD', 'CHERRY_PICK_HEAD', 'MERGE_HEAD', 'ORIG_HEAD']);
1✔
38

1✔
39
// https://git-scm.com/docs/git-rev-parse.html#_specifying_revisions
1✔
40
const refpaths = (ref: string) => [
1✔
41
    `refs/${ref}`,
93✔
42
    `refs/tags/${ref}`,
93✔
43
    `refs/heads/${ref}`,
93✔
44
    `refs/remotes/${ref}`,
93✔
45
    `refs/remotes/${ref}/HEAD`
93✔
46
];
93✔
47

1✔
48
function isOid(value: unknown) {
1✔
49
    return typeof value === 'string' && value.length === 40 && /^[0-9a-f]{40}$/.test(value);
224✔
50
}
224✔
51

1✔
52
export async function createRefIndex(gitdir: string) {
1✔
53
    const refResolver = await createRefResolver(gitdir);
19✔
54

19✔
55
    // expand a ref into a full form
19✔
56
    const expandRef = (ref: string) => {
19✔
57
        if (refResolver.exists(ref)) {
159✔
58
            return ref;
66✔
59
        }
66✔
60

93✔
61
        // Look in all the proper paths, in this order
93✔
62
        for (const candidateRef of refpaths(ref)) {
93✔
63
            if (refResolver.exists(candidateRef)) {
311✔
64
                return candidateRef;
56✔
65
            }
56✔
66
        }
311✔
67

37✔
68
        // Nothing found
37✔
69
        return null;
37✔
70
    };
37✔
71
    const resolveRef = async (ref: string): Promise<string> => {
19✔
72
        // Is it a complete and valid SHA?
69✔
73
        if (isOid(ref)) {
69✔
74
            return ref;
18✔
75
        }
18✔
76

51✔
77
        const expandedRef = expandRef(ref);
51✔
78

51✔
79
        if (expandedRef === null) {
51✔
80
            throw new Error(`Reference "${ref}" is not found`);
16✔
81
        }
16✔
82

35✔
83
        return refResolver.resolveOid(expandedRef);
35✔
84
    };
35✔
85
    const describeRef = async (ref: string): Promise<RefInfo> => {
19✔
86
        const expandedRef = expandRef(ref);
14✔
87

14✔
88
        if (expandedRef === null) {
14✔
89
            throw new Error(`Reference "${ref}" is not found`);
1✔
90
        }
1✔
91

13✔
92
        const refInfo = await refResolver.resolve(expandedRef);
13✔
93
        const [, scope, path] = expandedRef.match(/^([^/]+\/[^/]+)\/(.+)$/) || [
13✔
94
            '',
6✔
95
            'refs/heads',
6✔
96
            expandedRef
6✔
97
        ];
6✔
98
        const [namespace, category] = scope.split('/');
14✔
99
        const remoteMatch = scope === 'refs/remotes' ? path.match(/^([^/]+)\/(.+)$/) : null;
14✔
100
        const [remote = null, name] = remoteMatch ? remoteMatch.slice(1) : [null, path];
14✔
101

14✔
102
        return {
14✔
103
            path: expandedRef,
14✔
104
            name,
14✔
105
            symbolic: symbolicRefs.has(name),
14✔
106
            scope,
14✔
107
            namespace,
14✔
108
            category,
14✔
109
            remote,
14✔
110
            ref: refInfo.ref,
14✔
111
            oid: refInfo.oid
14✔
112
        } satisfies RefInfo;
14✔
113
    };
14✔
114

19✔
115
    const listRemotes = () => refResolver.remotes.slice();
19✔
116

19✔
117
    // List all the refs that match the prefix
19✔
118
    const listRefsCache = new Map<string, string[]>();
19✔
119
    const listRefsWithOidCache = new Map<string, Ref[]>();
19✔
120
    const listRefs = async (prefix: string, withOid: boolean) => {
19✔
121
        let cachedRefs = listRefsCache.get(prefix);
27✔
122

27✔
123
        if (cachedRefs === undefined) {
27✔
124
            // all refs filtered by a prefix
24✔
125
            cachedRefs = refResolver.names
24✔
126
                .filter((name) => name.startsWith(prefix))
24✔
127
                .map((name) => name.slice(prefix.length));
24✔
128

24✔
129
            listRefsCache.set(prefix, cachedRefs);
24✔
130
        }
24✔
131

27✔
132
        if (!withOid) {
27✔
133
            return cachedRefs.slice();
24✔
134
        }
24✔
135

3✔
136
        let cachedRefsWithOid = listRefsWithOidCache.get(prefix);
3✔
137

3✔
138
        if (cachedRefsWithOid === undefined) {
3✔
139
            const oids = await Promise.all(
3✔
140
                cachedRefs.map((name) => refResolver.resolveOid(prefix + name))
3✔
141
            );
3✔
142

3✔
143
            cachedRefsWithOid = cachedRefs.map((name, index) => ({
3✔
144
                name,
23✔
145
                oid: oids[index]
23✔
146
            }));
23✔
147

3✔
148
            listRefsWithOidCache.set(prefix, cachedRefsWithOid);
3✔
149
        }
3✔
150

3✔
151
        return cachedRefsWithOid.map((ref) => ({ ...ref }));
3✔
152
    };
3✔
153

19✔
154
    const listRemoteBranches = (remote: string, withOids = false) =>
19✔
155
        listRefs(`refs/remotes/${remote}/`, withOids);
6✔
156
    const listBranches = (withOids = false) => listRefs('refs/heads/', withOids);
19✔
157
    const listTags = (withOids = false) => listRefs('refs/tags/', withOids);
19✔
158
    const readRefContent = async (ref: string) =>
19✔
159
        basename(
1✔
160
            (await fsPromises.readFile(pathJoin(gitdir, ref), 'utf8'))
1✔
161
                .trim()
1✔
162
                .replace(/^ref:\s*/, '')
1✔
163
        );
1✔
164

19✔
165
    const currentBranch = async function () {
19✔
166
        const { ref, oid } = await describeRef('HEAD');
5✔
167
        const name = ref ? (await describeRef(ref)).name : null;
5✔
168

5✔
169
        return {
5✔
170
            name,
5✔
171
            oid
5✔
172
        };
5✔
173
    };
5✔
174

19✔
175
    // inspired by https://usethis.r-lib.org/reference/git-default-branch.html
19✔
176
    const defaultBranch = async function () {
19✔
177
        const branches = (await listBranches()) as string[]; // FIXME: remove string[]
5✔
178

5✔
179
        if (branches.length <= 1) {
5✔
180
            return basename(branches[0]);
3✔
181
        }
3✔
182

2✔
183
        const branchRef =
2✔
184
            expandRef('refs/remotes/upstream/HEAD') ||
2✔
185
            expandRef('refs/remotes/origin/HEAD') ||
2✔
186
            expandRef('refs/heads/main') ||
1!
NEW
187
            expandRef('refs/heads/master');
×
188

5✔
189
        if (branchRef) {
5✔
190
            return branchRef.endsWith('/HEAD') ? readRefContent(branchRef) : basename(branchRef);
2✔
191
        }
2✔
NEW
192

×
NEW
193
        return null;
×
NEW
194
    };
×
195

19✔
196
    return {
19✔
197
        isOid,
19✔
198
        isRefExists: (ref: string) => expandRef(ref) !== null,
19✔
199
        resolveRef,
19✔
200
        expandRef: (ref: string) => (isOid(ref) ? ref : expandRef(ref)),
19✔
201
        describeRef,
19✔
202

19✔
203
        listRemotes,
19✔
204
        listRemoteBranches,
19✔
205
        listBranches,
19✔
206
        listTags,
19✔
207

19✔
208
        defaultBranch,
19✔
209
        currentBranch,
19✔
210

19✔
211
        async stat() {
19✔
212
            const remotes = listRemotes();
6✔
213
            const branchesByRemote = await Promise.all(
6✔
214
                remotes.map((remote) => listRemoteBranches(remote))
6✔
215
            );
6✔
216

6✔
217
            return {
6✔
218
                remotes: remotes.map((remote, idx) => ({
6✔
219
                    remote,
4✔
220
                    branches: branchesByRemote[idx]
4✔
221
                })),
4✔
222
                branches: await listBranches(),
6✔
223
                tags: await listTags()
6✔
224
            };
6✔
225
        }
6✔
226
    };
19✔
227
}
19✔
228

1✔
229
async function resolveRef(
1✔
230
    ref: string,
78✔
231
    resolvedRefs: Map<string, RefSourceInfo>,
78✔
232
    looseRefs: Map<string, LooseRefFile>
78✔
233
): Promise<RefSourceInfo> {
78✔
234
    let resolvedRef = resolvedRefs.get(ref);
78✔
235

78✔
236
    if (resolvedRef === undefined) {
78✔
237
        const looseRef = looseRefs.get(ref);
35✔
238

35✔
239
        if (looseRef !== undefined) {
35✔
240
            if (looseRef.content === null) {
35✔
241
                looseRef.content = (await fsPromises.readFile(looseRef.path, 'utf8')).trim();
35✔
242
            }
35✔
243

35✔
244
            let refValue = looseRef.content;
35✔
245

35✔
246
            while (!isOid(refValue)) {
35✔
247
                // Is it a ref pointer?
17✔
248
                if (refValue.startsWith('ref: ')) {
17✔
249
                    refValue = refValue.replace(/^ref:\s+/, '');
7✔
250
                    continue;
7✔
251
                }
7✔
252

10✔
253
                // Sometimes an additional information is appended such as tags, branch names or comments
10✔
254
                const spaceIndex = refValue.search(/\s/);
10✔
255
                if (spaceIndex !== -1) {
10✔
256
                    refValue = refValue.slice(0, spaceIndex);
3✔
257
                    continue;
3✔
258
                }
3✔
259

7✔
260
                break;
7✔
261
            }
7✔
262

35✔
263
            const oid = isOid(refValue)
35✔
264
                ? refValue
35✔
265
                : (await resolveRef(refValue, resolvedRefs, looseRefs)).oid;
35✔
266

35✔
267
            resolvedRef = {
35✔
268
                ref: refValue !== oid ? refValue : null,
35✔
269
                oid
35✔
270
            };
35✔
271
        } else {
35✔
UNCOV
272
            resolvedRef = { ref: null, oid: null };
×
UNCOV
273
        }
×
274

35✔
275
        resolvedRefs.set(ref, resolvedRef);
35✔
276
    }
35✔
277

78✔
278
    if (resolvedRef.oid !== null) {
78✔
279
        return resolvedRef;
78✔
280
    }
78✔
UNCOV
281

×
UNCOV
282
    throw new Error(`Reference "${ref}" can't be resolved into oid`);
×
UNCOV
283
}
×
284

1✔
285
async function createRefResolver(gitdir: string) {
1✔
286
    const resolvedRefs = new Map<string, RefSourceInfo>();
19✔
287
    const refNames = new Set<string>();
19✔
288
    const remotes = await readRemotes(gitdir);
19✔
289
    const [packedRefs, looseRefs] = await Promise.all([
19✔
290
        readPackedRefs(gitdir),
19✔
291
        readLooseRefs(gitdir, remotes)
19✔
292
    ]);
19✔
293

19✔
294
    for (const ref of looseRefs.keys()) {
19✔
295
        refNames.add(ref);
218✔
296
    }
218✔
297

19✔
298
    for (const ref of packedRefs.keys()) {
19✔
299
        if (!ref.endsWith('^{}') && !refNames.has(ref)) {
46✔
300
            const oid = packedRefs.get(ref);
30✔
301

30✔
302
            if (oid !== undefined) {
30✔
303
                resolvedRefs.set(ref, { ref: null, oid });
30✔
304
            }
30✔
305

30✔
306
            refNames.add(ref);
30✔
307
        }
30✔
308
    }
46✔
309

19✔
310
    return {
19✔
311
        remotes,
19✔
312
        names: [...refNames].sort((a, b) => (a < b ? -1 : 1)),
19✔
313
        exists: (ref: string) => refNames.has(ref),
19✔
314
        resolveOid: async (ref: string) =>
19✔
315
            (await resolveRef(ref, resolvedRefs, looseRefs)).oid as string,
58✔
316
        resolve: (ref: string) => resolveRef(ref, resolvedRefs, looseRefs)
19✔
317
    } satisfies RefResolver;
19✔
318
}
19✔
319

1✔
320
async function readRemotes(gitdir: string) {
1✔
321
    const remotesDir = pathJoin(gitdir, 'refs', 'remotes');
19✔
322
    const remotes = [];
19✔
323

19✔
324
    if (existsSync(remotesDir)) {
19✔
325
        const entries = await fsPromises.readdir(remotesDir, {
9✔
326
            withFileTypes: true
9✔
327
        });
9✔
328

9✔
329
        for (const entry of entries) {
9✔
330
            if (entry.isDirectory()) {
32✔
331
                remotes.push(entry.name);
18✔
332
            }
18✔
333
        }
32✔
334
    }
9✔
335

19✔
336
    return remotes.sort();
19✔
337
}
19✔
338

1✔
339
async function readLooseRefs(gitdir: string, remotes: string[]) {
1✔
340
    const looseRefs = new Map<string, LooseRefFile>();
19✔
341
    const include = [
19✔
342
        'refs/heads',
19✔
343
        'refs/tags',
19✔
344
        ...remotes.map((remote) => `refs/remotes/${remote}`)
19✔
345
    ].filter((path) => existsSync(pathJoin(gitdir, path.replace(/\//g, pathSep))));
19✔
346

19✔
347
    if (include.length) {
19✔
348
        const { files } = await scanFs({
19✔
349
            basedir: gitdir,
19✔
350
            include
19✔
351
        });
19✔
352

19✔
353
        for (const { posixPath, path } of files) {
19✔
354
            looseRefs.set(posixPath, { path: pathJoin(gitdir, path), content: null });
169✔
355
        }
169✔
356
    }
19✔
357

19✔
358
    for (const ref of symbolicRefs) {
19✔
359
        const filename = pathJoin(gitdir, ref);
95✔
360

95✔
361
        if (existsSync(filename)) {
95✔
362
            looseRefs.set(ref, { path: filename, content: null });
49✔
363
        }
49✔
364
    }
95✔
365

19✔
366
    return looseRefs;
19✔
367
}
19✔
368

1✔
369
async function readPackedRefs(gitdir: string) {
1✔
370
    const packedRefsFilename = pathJoin(gitdir, 'packed-refs');
19✔
371
    const packedRefs = new Map<string, string>();
19✔
372

19✔
373
    if (existsSync(packedRefsFilename)) {
19✔
374
        const packedRefsContent = await fsPromises.readFile(packedRefsFilename, 'utf8');
11✔
375
        let ref = null;
11✔
376

11✔
377
        for (const line of packedRefsContent.trim().split(/\r\n?|\n/)) {
11✔
378
            if (line.startsWith('#')) {
57✔
379
                continue;
11✔
380
            }
11✔
381

46✔
382
            if (line.startsWith('^')) {
46✔
383
                // This is a oid for the commit associated with the annotated tag immediately preceding this line.
7✔
384
                // Trim off the '^'
7✔
385
                const oid = line.slice(1);
7✔
386

7✔
387
                // The tagname^{} syntax is based on the output of `git show-ref --tags -d`
7✔
388
                packedRefs.set(ref + '^{}', oid);
7✔
389
            } else {
7✔
390
                // This is an oid followed by the ref name
39✔
391
                const spaceOffset = line.indexOf(' ');
39✔
392
                const oid = line.slice(0, spaceOffset);
39✔
393

39✔
394
                ref = line.slice(spaceOffset + 1);
39✔
395
                packedRefs.set(ref, oid);
39✔
396
            }
39✔
397
        }
57✔
398
    }
11✔
399

19✔
400
    return packedRefs;
19✔
401
}
19✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc