Skip to content

Commit

Permalink
Add repo.describeRef(ref) method
Browse files Browse the repository at this point in the history
  • Loading branch information
lahmatiy committed Oct 30, 2024
1 parent 2321fa5 commit 32d176e
Show file tree
Hide file tree
Showing 4 changed files with 232 additions and 45 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## next

- Added `repo.describeRef(ref)` method, which returns an information object about the reference

## 0.1.3 (2023-10-13)

- Fixed size computation of on-disk reverse index (`.rev` files)
Expand Down
55 changes: 50 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ const commits = await repo.log({ ref: 'my-branch', depth: 10 });
console.log(commits);

await repo.dispose();
});
```

#### createGitReader(gitdir, options?)
Expand Down Expand Up @@ -58,6 +57,15 @@ The algorithm to identify a default branch name:
- `main`
- `master`

#### repo.isRefExists(ref)

Checks if a `ref` exists.

```js
const isValidRef = repo.isRefExists('main');
// true
```

#### repo.expandRef(ref)

Expands a `ref` into a full form, e.g. `'main'` -> `'refs/heads/main'`.
Expand All @@ -78,13 +86,50 @@ const oid = repo.resolveRef('main');
// '8bb6e23769902199e39ab70f2441841712cbdd62'
```

#### repo.isRefExists(ref)
#### repo.describeRef(ref)

Checks if a `ref` exists.
Returns an info object for provided `ref`.

```js
const isValidRef = repo.isRefExists('main');
// true
const info = repo.describeRef('HEAD');
// {
// path: 'HEAD',
// name: 'HEAD',
// symbolic: true,
// ref: 'refs/heads/test',
// oid: '2dbee47a8d4f8d39e1168fad951b703ee05614d6'
// }
```

```js
const info = repo.describeRef('main');
// {
// path: 'refs/heads/main',
// name: 'main',
// symbolic: false,
// scope: 'refs/heads',
// namespace: 'refs',
// category: 'heads',
// remote: null,
// ref: null,
// oid: '7b84f676f2fbea2a3c6d83924fa63059c7bdfbe2'
// }
```

```js
const info = repo.describeRef('origin/HEAD');
// {
// path: 'refs/remotes/origin/HEAD',
// name: 'HEAD',
// symbolic: false,
// scope: 'refs/remotes',
// namespace: 'refs',
// category: 'remotes',
// remote: 'origin',
// ref: 'refs/remotes/origin/main',
// oid: '7b84f676f2fbea2a3c6d83924fa63059c7bdfbe2'
// }
```
```
#### repo.listRemotes()
Expand Down
146 changes: 106 additions & 40 deletions src/resolve-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,36 @@ import { promises as fsPromises, existsSync } from 'fs';
import { join as pathJoin, basename, sep as pathSep } from 'path';
import { scanFs } from '@discoveryjs/scan-fs';

type Ref = { name: string; oid: string };
type LooseRefFile = { path: string; content: string | null };
type Ref = {
name: string;
oid: string;
};
type RefSourceInfo = {
ref: string | null;
oid: string | null;
};
type LooseRefFile = {
path: string;
content: string | null;
};
type RefResolver = {
remotes: string[];
names: string[];
exists(ref: string): boolean;
resolveOid(ref: string): Promise<string>;
resolve(ref: string): Promise<RefSourceInfo>;
};
type RefInfo = {
path: string;
name: string;
symbolic: boolean;
scope: string;
namespace: string;
category: string;
remote: string | null;
ref: string | null;
oid: string | null;
};

// NOTICE: Don't forget to update README.md when change the values
const symbolicRefs = new Set(['HEAD', 'FETCH_HEAD', 'CHERRY_PICK_HEAD', 'MERGE_HEAD', 'ORIG_HEAD']);
Expand All @@ -18,7 +46,7 @@ const refpaths = (ref: string) => [
];

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

export async function createRefIndex(gitdir: string) {
Expand All @@ -40,19 +68,49 @@ export async function createRefIndex(gitdir: string) {
// Nothing found
return null;
};
const resolveRef = async (ref: string) => {
const resolveRef = async (ref: string): Promise<string> => {
// Is it a complete and valid SHA?
if (isOid(ref)) {
return ref;
}

const expandedRef = await expandRef(ref);
const expandedRef = expandRef(ref);

if (expandedRef === null) {
throw new Error(`Reference "${ref}" is not found`);
}

return refResolver.resolve(expandedRef);
return refResolver.resolveOid(expandedRef);
};
const describeRef = async (ref: string): Promise<RefInfo> => {
const expandedRef = expandRef(ref);

if (expandedRef === null) {
throw new Error(`Reference "${ref}" is not found`);
}

const refInfo = await refResolver.resolve(expandedRef);

const [, scope, path] = expandedRef.match(/^([^/]+\/[^/]+)\/(.+)$/) || [
'',
'refs/heads',
expandedRef
];
const [namespace, category] = scope.split('/');
const remoteMatch = scope === 'refs/remotes' ? path.match(/^([^/]+)\/(.+)$/) : null;
const [remote = null, name] = remoteMatch ? remoteMatch.slice(1) : [null, path];

return {
path: expandedRef,
name,
symbolic: symbolicRefs.has(name),
scope,
namespace,
category,
remote,
ref: refInfo.ref,
oid: refInfo.oid
} satisfies RefInfo;
};

const listRemotes = () => refResolver.remotes.slice();
Expand All @@ -79,14 +137,14 @@ export async function createRefIndex(gitdir: string) {
let cachedRefsWithOid = listRefsWithOidCache.get(prefix);

if (cachedRefsWithOid === undefined) {
cachedRefsWithOid = [];
const oids = await Promise.all(
cachedRefs.map((name) => refResolver.resolveOid(prefix + name))
);

for (const name of cachedRefs) {
cachedRefsWithOid.push({
name,
oid: await refResolver.resolve(prefix + name)
});
}
cachedRefsWithOid = cachedRefs.map((name, index) => ({
name,
oid: oids[index]
}));

listRefsWithOidCache.set(prefix, cachedRefsWithOid);
}
Expand All @@ -106,13 +164,10 @@ export async function createRefIndex(gitdir: string) {
);

return {
isRefExists: (ref: string) => expandRef(ref) !== null,
resolveRef,
expandRef(ref: string) {
return isOid(ref) ? ref : expandRef(ref);
},
async isRefExists(ref: string) {
return (await expandRef(ref)) !== null;
},
expandRef: (ref: string) => (isOid(ref) ? ref : expandRef(ref)),
describeRef,

listRemotes,
listRemoteBranches,
Expand Down Expand Up @@ -162,53 +217,62 @@ export async function createRefIndex(gitdir: string) {

async function resolveRef(
ref: string,
resolvedRefs: Map<string, string | null>,
resolvedRefs: Map<string, RefSourceInfo>,
looseRefs: Map<string, LooseRefFile>
): Promise<string> {
const resolvedRef = resolvedRefs.get(ref);

if (resolvedRef !== null) {
if (resolvedRef !== undefined) {
return resolvedRef;
}
): Promise<RefSourceInfo> {
let resolvedRef = resolvedRefs.get(ref);

if (resolvedRef === undefined) {
const looseRef = looseRefs.get(ref);

if (looseRef !== undefined) {
if (looseRef.content === null) {
looseRef.content = (await fsPromises.readFile(looseRef.path, 'utf8')).trim();
}

let value = looseRef.content;
let refValue = looseRef.content;

while (!isOid(value)) {
while (!isOid(refValue)) {
// Is it a ref pointer?
if (value.startsWith('ref: ')) {
value = value.slice(5); // 'ref: '.length == 5
if (refValue.startsWith('ref: ')) {
refValue = refValue.replace(/^ref:\s+/, '');
continue;
}

// Sometimes an additional information is appended such as tags, branch names or comments
if (/\s/.test(value)) {
value = value.split(/\s+/)[0];
const spaceIndex = refValue.search(/\s/);
if (spaceIndex !== -1) {
refValue = refValue.slice(0, spaceIndex);
continue;
}

value = await resolveRef(value, resolvedRefs, looseRefs);
break;
}

resolvedRefs.set(ref, value);
return value;
const oid = isOid(refValue)
? refValue
: (await resolveRef(refValue, resolvedRefs, looseRefs)).oid;

resolvedRef = {
ref: refValue !== oid ? refValue : null,
oid
};
} else {
resolvedRef = { ref: null, oid: null };
}

resolvedRefs.set(ref, resolvedRef);
}

if (resolvedRef.oid !== null) {
return resolvedRef;
}

resolvedRefs.set(ref, null);
throw new Error(`Reference "${ref}" can't be resolved into oid`);
}

async function createRefResolver(gitdir: string) {
const resolvedRefs = new Map<string, string | null>();
const resolvedRefs = new Map<string, RefSourceInfo>();
const refNames = new Set<string>();
const remotes = await readRemotes(gitdir);
const [packedRefs, looseRefs] = await Promise.all([
Expand All @@ -225,7 +289,7 @@ async function createRefResolver(gitdir: string) {
const oid = packedRefs.get(ref);

if (oid !== undefined) {
resolvedRefs.set(ref, oid);
resolvedRefs.set(ref, { ref: null, oid });
}

refNames.add(ref);
Expand All @@ -236,8 +300,10 @@ async function createRefResolver(gitdir: string) {
remotes,
names: [...refNames].sort((a, b) => (a < b ? -1 : 1)),
exists: (ref: string) => refNames.has(ref),
resolveOid: async (ref: string) =>
(await resolveRef(ref, resolvedRefs, looseRefs)).oid as string,
resolve: (ref: string) => resolveRef(ref, resolvedRefs, looseRefs)
};
} satisfies RefResolver;
}

async function readRemotes(gitdir: string) {
Expand Down
Loading

0 comments on commit 32d176e

Please sign in to comment.