@foss.global/codefeed 5.1.0 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +21 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.giteasourceinventory.d.ts +35 -9
- package/dist_ts/classes.giteasourceinventory.js +284 -26
- package/dist_ts/classes.giteasourcetransfer.d.ts +114 -6
- package/dist_ts/classes.giteasourcetransfer.js +326 -9
- package/dist_ts/forgeinventory.client.d.ts +22 -4
- package/dist_ts/forgeinventory.client.js +32 -13
- package/dist_ts/giteasourceinventory.packages.d.ts +46 -0
- package/dist_ts/giteasourceinventory.packages.js +139 -0
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/plugins.d.ts +2 -2
- package/dist_ts/plugins.js +3 -1
- package/package.json +3 -3
- package/readme.md +28 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.giteasourceinventory.ts +293 -24
- package/ts/classes.giteasourcetransfer.ts +395 -8
- package/ts/forgeinventory.client.ts +63 -12
- package/ts/giteasourceinventory.packages.ts +160 -0
- package/ts/index.ts +1 -0
- package/ts/plugins.ts +2 -2
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import * as validation from './forgeinventory.validation.js';
|
|
3
|
+
|
|
4
|
+
type TMeasure = plugins.fossInterfaces.data.TForgeSourceMeasure;
|
|
5
|
+
type IPackageRef = plugins.fossInterfaces.data.IForgeSourcePackageRef;
|
|
6
|
+
type IPackageRecord = plugins.fossInterfaces.data.IForgeSourcePackageInventory;
|
|
7
|
+
type IPackageContent = plugins.fossInterfaces.data.IForgeSourcePackageContent;
|
|
8
|
+
type IPackageContentSelection = plugins.fossInterfaces.data.IForgeSourcePackageContentSelection;
|
|
9
|
+
type IPackageContentGroup = plugins.fossInterfaces.data.IForgeSourcePackageContentGroup;
|
|
10
|
+
type IPackageContentTotals = plugins.fossInterfaces.data.IForgeSourcePackageContentTotals;
|
|
11
|
+
|
|
12
|
+
/** Media types Gitea serves an index or a manifest list as; an index candidate must be one of them. */
|
|
13
|
+
export const containerIndexMediaTypes: readonly string[] = Object.freeze([
|
|
14
|
+
'application/vnd.oci.image.index.v1+json',
|
|
15
|
+
'application/vnd.docker.distribution.manifest.list.v2+json',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
/** Every manifest media type Gitea serves: images, indexes and manifest lists, OCI and Docker. */
|
|
19
|
+
export const containerManifestMediaTypes: readonly string[] = Object.freeze([
|
|
20
|
+
...containerIndexMediaTypes,
|
|
21
|
+
'application/vnd.oci.image.manifest.v1+json',
|
|
22
|
+
'application/vnd.docker.distribution.manifest.v2+json',
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
/** A package type as Gitea names it; it names reason paths, so it must be one of Gitea's plain identifiers. */
|
|
26
|
+
export const packageTypePattern = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
27
|
+
|
|
28
|
+
/** Gitea's npm name rule (`modules/packages/npm/creator.go`): `name` or `@scope/name`, URL-safe as it stands. */
|
|
29
|
+
const npmNamePattern = /^(?:@[a-z0-9-][a-z0-9-._]*\/)?[a-z0-9-][a-z0-9-._]*$/;
|
|
30
|
+
/** Gitea's Cargo crate name rule (`modules/packages/cargo/parser.go`). */
|
|
31
|
+
const cargoNamePattern = /^[A-Za-z][A-Za-z0-9-_]{0,63}$/;
|
|
32
|
+
/** Gitea's Swift scope and name rules (`routers/api/packages/swift/swift.go`); it stores the package as `scope.name`. */
|
|
33
|
+
const swiftPackagePattern = /^([A-Za-z0-9][A-Za-z0-9-]{0,38})\.([A-Za-z0-9][A-Za-z0-9-_]{0,99})$/;
|
|
34
|
+
|
|
35
|
+
const compareText = (aArg: string, bArg: string): number => (aArg < bArg ? -1 : aArg > bArg ? 1 : 0);
|
|
36
|
+
|
|
37
|
+
/** The key of a package within one instance: owner, type and name, which Gitea keeps unique (the name without case). */
|
|
38
|
+
export const packageKey = (refArg: IPackageRef): string => `${refArg.owner}/${refArg.type}/${refArg.name}`;
|
|
39
|
+
|
|
40
|
+
/** Orders packages by owner, type and name in code-unit order. */
|
|
41
|
+
export const comparePackages = (aArg: IPackageRef, bArg: IPackageRef): number =>
|
|
42
|
+
compareText(aArg.owner, bArg.owner) || compareText(aArg.type, bArg.type) || compareText(aArg.name, bArg.name);
|
|
43
|
+
|
|
44
|
+
/** The path segment(s) of an npm package in Gitea's npm registry: the name unencoded, as Gitea's npm routes match it. */
|
|
45
|
+
export const npmPackagePath = (nameArg: string): string => {
|
|
46
|
+
if (!npmNamePattern.test(nameArg) || nameArg.length > 214) throw new validation.ForgeInventoryError('invalid-npm-name');
|
|
47
|
+
return nameArg;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The path of a crate's entry in the Cargo sparse index, lowercased, per the Cargo registry index layout. */
|
|
51
|
+
export const cargoIndexPath = (nameArg: string): string => {
|
|
52
|
+
if (!cargoNamePattern.test(nameArg)) throw new validation.ForgeInventoryError('invalid-cargo-name');
|
|
53
|
+
const name = nameArg.toLowerCase();
|
|
54
|
+
if (name.length === 1) return `1/${name}`;
|
|
55
|
+
if (name.length === 2) return `2/${name}`;
|
|
56
|
+
if (name.length === 3) return `3/${name[0]}/${name}`;
|
|
57
|
+
return `${name.slice(0, 2)}/${name.slice(2, 4)}/${name}`;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** The scope and name of a Swift package stored as `scope.name`. */
|
|
61
|
+
export const swiftPackageParts = (nameArg: string): { scope: string; name: string } => {
|
|
62
|
+
const match = swiftPackagePattern.exec(nameArg);
|
|
63
|
+
if (!match) throw new validation.ForgeInventoryError('invalid-swift-name');
|
|
64
|
+
return { scope: match[1], name: match[2] };
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The digest of the manifest a container version holds: `sha256:` and the
|
|
69
|
+
* SHA-256 of the blob holding the version's `manifest.json`; null for a
|
|
70
|
+
* version without one.
|
|
71
|
+
*/
|
|
72
|
+
export const containerManifestDigest = (packageArg: IPackageRecord, versionIdArg: string): string | null => {
|
|
73
|
+
const blob = packageArg.blobs.find((blobArg) => blobArg.files.some((fileArg) => fileArg.versionId === versionIdArg && fileArg.name === 'manifest.json'));
|
|
74
|
+
return blob ? `sha256:${blob.sha256}` : null;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The version ids of a package's tagged-and-referenced selection: for a
|
|
79
|
+
* container package the tagged versions and the versions holding a manifest
|
|
80
|
+
* a tagged index references; every version of any other type.
|
|
81
|
+
*/
|
|
82
|
+
export const taggedAndReferencedVersionIds = (packageArg: IPackageRecord): Set<string> => {
|
|
83
|
+
if (packageArg.type !== 'container') return new Set(packageArg.versions.map((versionArg) => versionArg.id));
|
|
84
|
+
const tagged = packageArg.versions.filter((versionArg) => !versionArg.version.startsWith('sha256:'));
|
|
85
|
+
const referenced = new Set(tagged.flatMap((versionArg) => versionArg.references ?? []));
|
|
86
|
+
const selected = new Set(tagged.map((versionArg) => versionArg.id));
|
|
87
|
+
for (const version of packageArg.versions) {
|
|
88
|
+
const digest = containerManifestDigest(packageArg, version.id);
|
|
89
|
+
if (digest !== null && referenced.has(digest)) selected.add(version.id);
|
|
90
|
+
}
|
|
91
|
+
return selected;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
interface IContentSum {
|
|
95
|
+
blobs: Map<string, bigint>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const contentMeasure = (sumArg: IContentSum, missingArg: number): IPackageContent => {
|
|
99
|
+
const blobs = BigInt(sumArg.blobs.size);
|
|
100
|
+
const bytes = [...sumArg.blobs.values()].reduce((totalArg, sizeArg) => totalArg + sizeArg, 0n);
|
|
101
|
+
if (missingArg === 0) return { blobs: { status: 'complete', value: blobs.toString() }, bytes: { status: 'complete', value: bytes.toString() } };
|
|
102
|
+
const reason = `Package units not measured: ${missingArg}.`;
|
|
103
|
+
const partial = (valueArg: bigint): TMeasure => ({ status: 'partial', observedValue: valueArg.toString(), reason });
|
|
104
|
+
return { blobs: partial(blobs), bytes: partial(bytes) };
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** Distinct content of a set of package records over both selections; `missingArg` packages of the set were not read. */
|
|
108
|
+
const selection = (packagesArg: readonly IPackageRecord[], missingArg: number): IPackageContentSelection => {
|
|
109
|
+
const all: IContentSum = { blobs: new Map() };
|
|
110
|
+
const tagged: IContentSum = { blobs: new Map() };
|
|
111
|
+
for (const record of packagesArg) {
|
|
112
|
+
const selected = taggedAndReferencedVersionIds(record);
|
|
113
|
+
for (const blob of record.blobs) {
|
|
114
|
+
const size = BigInt(blob.size);
|
|
115
|
+
all.blobs.set(blob.sha256, size);
|
|
116
|
+
if (blob.files.some((fileArg) => selected.has(fileArg.versionId))) tagged.blobs.set(blob.sha256, size);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { allVersions: contentMeasure(all, missingArg), taggedAndReferenced: contentMeasure(tagged, missingArg) };
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/** A group of packages with its content by type; `refsArg` are the group's discovered packages. */
|
|
123
|
+
const group = (refsArg: readonly IPackageRef[], recordsArg: ReadonlyMap<string, IPackageRecord>): IPackageContentGroup => {
|
|
124
|
+
const read = (refs: readonly IPackageRef[]) => refs.flatMap((refArg) => {
|
|
125
|
+
const record = recordsArg.get(packageKey(refArg));
|
|
126
|
+
return record ? [record] : [];
|
|
127
|
+
});
|
|
128
|
+
const missing = (refs: readonly IPackageRef[]) => refs.filter((refArg) => !recordsArg.has(packageKey(refArg))).length;
|
|
129
|
+
const types = [...new Set(refsArg.map((refArg) => refArg.type))].sort(compareText);
|
|
130
|
+
return {
|
|
131
|
+
...selection(read(refsArg), missing(refsArg)),
|
|
132
|
+
types: types.map((typeArg) => {
|
|
133
|
+
const refs = refsArg.filter((refArg) => refArg.type === typeArg);
|
|
134
|
+
return { type: typeArg, ...selection(read(refs), missing(refs)) };
|
|
135
|
+
}),
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The distinct content of an inventory's packages, `totals.packageContent`:
|
|
141
|
+
* a pure function of the discovered packages and the records their units
|
|
142
|
+
* returned. Every SHA-256 counts once per scope, globally and per owner, each
|
|
143
|
+
* by type, over all versions and over the tagged versions with the manifests
|
|
144
|
+
* their indexes reference. A measure is complete only when every discovered
|
|
145
|
+
* package of its scope has a record; otherwise it is partial with the sum of
|
|
146
|
+
* the records read. Records of packages discovery does not list are ignored.
|
|
147
|
+
*/
|
|
148
|
+
export const composeGiteaPackageContent = (
|
|
149
|
+
discoveredArg: readonly IPackageRef[],
|
|
150
|
+
packagesArg: readonly IPackageRecord[],
|
|
151
|
+
): IPackageContentTotals => {
|
|
152
|
+
const discovered = new Set(discoveredArg.map(packageKey));
|
|
153
|
+
const records = new Map(packagesArg.filter((recordArg) => discovered.has(packageKey(recordArg))).map((recordArg) => [packageKey(recordArg), recordArg]));
|
|
154
|
+
const refs = [...new Map(discoveredArg.map((refArg) => [packageKey(refArg), { owner: refArg.owner, type: refArg.type, name: refArg.name }])).values()];
|
|
155
|
+
const owners = [...new Set(refs.map((refArg) => refArg.owner))].sort(compareText);
|
|
156
|
+
return {
|
|
157
|
+
global: group(refs, records),
|
|
158
|
+
owners: owners.map((ownerArg) => ({ owner: ownerArg, ...group(refs.filter((refArg) => refArg.owner === ownerArg), records) })),
|
|
159
|
+
};
|
|
160
|
+
};
|
package/ts/index.ts
CHANGED
|
@@ -491,3 +491,4 @@ export * from './classes.giteasourceinventory.js';
|
|
|
491
491
|
export * from './classes.giteasourcetransfer.js';
|
|
492
492
|
export * from './classes.lfsbatchclient.js';
|
|
493
493
|
export { GitScratchBudget, mirrorScratchBytes } from './giteasourceinventory.git.js';
|
|
494
|
+
export { composeGiteaPackageContent } from './giteasourceinventory.packages.js';
|
package/ts/plugins.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as fossInterfaces from '@foss.global/interfaces';
|
|
2
2
|
|
|
3
3
|
import * as crypto from 'node:crypto';
|
|
4
4
|
import * as fs from 'node:fs/promises';
|
|
@@ -20,7 +20,7 @@ export {
|
|
|
20
20
|
import * as lik from '@push.rocks/lik';
|
|
21
21
|
import * as smarttime from '@push.rocks/smarttime';
|
|
22
22
|
|
|
23
|
-
export
|
|
23
|
+
export { fossInterfaces };
|
|
24
24
|
export type TNpmRegistry = import('@push.rocks/smartnpm').NpmRegistry;
|
|
25
25
|
export type TGitManager = import('@foss.global/gitmanager').GitManager;
|
|
26
26
|
export type TGitManagerCommitSummary = import('@foss.global/gitmanager').IGitManagerCommitSummary;
|