Skip to content

Commit 7f70bfa

Browse files
authored
Merge pull request #61 from Araxeus/commit-hash-versioning
2 parents 28b3b69 + e27c70f commit 7f70bfa

File tree

8 files changed

+207
-49
lines changed

8 files changed

+207
-49
lines changed

README.md

+59
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ But that's not all - Vendorfiles is not limited to managing text files - it can
1414

1515
- [Installation](#installation)
1616
- [Configuration](#configuration)
17+
- [Versioning Dependencies](#versioning-dependencies)
1718
- [GitHub Releases](#github-releases)
19+
- [Default Configuration](#default-configuration)
1820
- [Commands](#commands)
1921
- [Sync](#sync)
2022
- [Update](#update)
@@ -125,6 +127,41 @@ To rename or move files, you can specify an object with the source file as the k
125127
}
126128
```
127129

130+
### Versioning Dependencies
131+
132+
This project uses GitHub releases to determine the version of a dependency. When a new release is made on GitHub, the version of the dependency in this project is updated accordingly, and the files are based on the tag of that release.
133+
134+
However, there is an optional `hashVersionFile` key for each dependency that allows for a different versioning strategy. If `hashVersionFile` is specified, the version is based on the latest commit hash of the file specified by hashVersionFile.
135+
136+
The `hashVersionFile` key can be either:
137+
138+
- A string: In this case, it should be the path to the file in the dependency repository. The version of the dependency will be the latest commit hash of this file.
139+
140+
- A boolean: If `hashVersionFile` is set to true, the path of the first file provided in the file list for that dependency will be used. The version of the dependency will be the latest commit hash of this file.
141+
142+
This versioning strategy allows for more granular control over the version of a dependency, as it can be updated whenever a specific file in the dependency repository changes.
143+
144+
```json
145+
{
146+
"vendorDependencies": {
147+
"Cooltipz": {
148+
"repository": "https://github.com/jackdomleo7/Cooltipz.css",
149+
"version": "f6ec482ea395cead4fd849c05df6edd8da284a52",
150+
"hashVersionFile": "package.json",
151+
"files": ["cooltipz.min.css", "package.json"],
152+
},
153+
"Coloris": {
154+
"repository": "https://github.com/mdbassit/Coloris",
155+
"version": "v0.17.1",
156+
"hashVersionFile": true,
157+
"files": ["dist/coloris.min.js"],
158+
}
159+
}
160+
}
161+
```
162+
163+
> in this example, the version of Cooltipz will be the latest commit hash of the `package.json` file, <br> and the version of Coloris will be the latest commit hash of the `dist/coloris.min.js` file.
164+
128165
### GitHub Releases
129166

130167
You can download release assets by using the `{release}/` placeholder in the file path.
@@ -171,6 +208,28 @@ To extract files from a compressed release archive, you can define an object tha
171208
}
172209
```
173210

211+
## Default Configuration
212+
213+
For shared options across dependencies, use a `default` object at the same level as `vendorConfig` and `vendorDependencies`. Here's an example:
214+
215+
```yml
216+
vendorConfig:
217+
vendorFolder: .
218+
default:
219+
vendorFolder: "{vendorFolder}"
220+
repository: https://github.com/nushell/nu_scripts
221+
hashVersionFile: true
222+
vendorDependencies:
223+
nu-winget-completions:
224+
files: custom-completions/winget/winget-completions.nu
225+
version: 912bea4588ba089aebe956349488e7f78e56061c
226+
nu-cargo-completions:
227+
files: custom-completions/cargo/cargo-completions.nu
228+
version: afde2592a6254be7c14ccac520cb608bd1adbaf9
229+
```
230+
231+
In this example, the `default` object specifies the `vendorFolder`, `repository`, and `hashVersionFile` options. These options will be applied to all dependencies listed under `vendorDependencies`, unless they are overridden in the individual dependency configuration.
232+
174233
## Commands
175234

176235
```text

lib/commands.ts

+8-12
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
flatFiles,
2626
getDependencyFolder,
2727
getFilesFromLockfile,
28+
getNewVersion,
2829
green,
2930
info,
3031
ownerAndNameFromRepoUrl,
@@ -206,12 +207,9 @@ export async function install({
206207
}
207208

208209
if (!newVersion) {
209-
const latestRelease = await github.getLatestRelease(repo);
210-
newVersion = latestRelease.tag_name as string;
210+
newVersion = await getNewVersion(dependency, repo, showOutdatedOnly);
211211
}
212212

213-
assert(!!newVersion, `Could not find a version for ${dependency.name}`);
214-
215213
const needUpdate =
216214
force ||
217215
(await checkIfNeedsUpdate({
@@ -312,7 +310,9 @@ export async function install({
312310
error(
313311
`${err.toString()}:\nCould not download file "${
314312
typeof file === 'string' ? file : file[0]
315-
}" from ${dependency.repository}`,
313+
}" from ${
314+
dependency.repository
315+
} with version ${ref}`,
316316
);
317317
}
318318
});
@@ -409,8 +409,8 @@ export async function install({
409409
await writeLockfile(
410410
dependency.name,
411411
{
412-
version: newVersion,
413412
repository: dependency.repository,
413+
version: newVersion,
414414
files: dependency.files,
415415
},
416416
lockfilePath,
@@ -419,12 +419,8 @@ export async function install({
419419
const oldVersion = dependency.version;
420420

421421
if (newVersion !== oldVersion) {
422-
configFile.vendorDependencies[dependency.name] = {
423-
version: newVersion,
424-
repository: dependency.repository,
425-
files: dependency.files,
426-
vendorFolder: dependency.vendorFolder,
427-
} as VendorDependency;
422+
configFile.vendorDependencies[dependency.name].version = newVersion;
423+
428424
await writeConfig({
429425
configFile,
430426
configFileSettings,

lib/config.ts

+13
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,19 @@ export async function getConfig(): Promise<VendorsOptions> {
124124
`Invalid vendorConfig key in ${configFile.path}`,
125125
);
126126

127+
const defaultOptions = configFile.data.default || {};
128+
129+
for (const depName of Object.keys(dependencies)) {
130+
const files = dependencies[depName].files;
131+
if (typeof files === 'string') {
132+
dependencies[depName].files = [files];
133+
}
134+
for (const defaultKey of Object.keys(defaultOptions)) {
135+
// @ts-expect-error expression of type 'string' can't be used to index type 'VendorDependency'
136+
dependencies[depName][defaultKey] ??= defaultOptions[defaultKey];
137+
}
138+
}
139+
127140
res = {
128141
dependencies,
129142
config,

lib/github.ts

+23-2
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,35 @@ export async function getLatestRelease({ owner, name: repo }: Repository) {
7070
return res.data;
7171
}
7272

73+
export async function getFileCommitSha({
74+
repo,
75+
path,
76+
}: {
77+
repo: Repository;
78+
path: string;
79+
}) {
80+
const commit = await octokit().repos.listCommits({
81+
owner: repo.owner,
82+
repo: repo.name,
83+
path,
84+
per_page: 1,
85+
});
86+
if (!commit.data?.[0]?.sha) {
87+
error(`No commits found for ${repo.owner}/${repo.name}: ${path}`);
88+
}
89+
return commit.data[0].sha;
90+
}
91+
7392
export async function getFile({
7493
repo,
7594
path,
7695
ref,
7796
}: {
7897
repo: Repository;
7998
path: string;
80-
ref: string;
99+
ref: string | undefined;
81100
}) {
101+
ref = ref || undefined;
82102
const requestOptions = octokit().repos.getContent.endpoint({
83103
owner: repo.owner,
84104
repo: repo.name,
@@ -94,7 +114,7 @@ export async function getFile({
94114
const req = await fetch(requestOptions.url, requestOptions);
95115

96116
if (!(req.ok && req.body)) {
97-
throw 'Request failed';
117+
throw `Request failed with status ${req.status}`;
98118
}
99119

100120
return req.body;
@@ -223,6 +243,7 @@ export async function login(token?: string) {
223243
export default {
224244
login,
225245
getFile,
246+
getFileCommitSha,
226247
getLatestRelease,
227248
downloadReleaseFile,
228249
findRepoUrl,

lib/types.d.ts

+10
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export type VendorsOptions = {
88
export type ConfigFile = {
99
vendorConfig: VendorConfig;
1010
vendorDependencies: VendorDependencies;
11+
default?: DefaultOptions;
1112
[key: string]: unknown;
1213
};
1314

@@ -32,10 +33,19 @@ export type FileInputOutput = {
3233

3334
export type FilesArray = (string | FileInputOutput)[];
3435

36+
export type DefaultOptions = {
37+
repository?: string;
38+
files?: FilesArray;
39+
hashVersionFile?: string | boolean;
40+
vendorFolder?: string;
41+
version?: string;
42+
};
43+
3544
export type VendorDependency = {
3645
repository: string;
3746
files: FilesArray;
3847
version?: string;
48+
hashVersionFile?: string | boolean;
3949
name?: string;
4050
vendorFolder?: string;
4151
};

lib/utils.ts

+54-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { finished } from 'node:stream/promises';
2525
import parseJson from 'parse-json';
2626

2727
import { getConfig, getRunOptions } from './config.js';
28+
import github from './github.js';
2829

2930
export function assert(condition: boolean, message: string): asserts condition {
3031
if (!condition) {
@@ -96,16 +97,16 @@ export function replaceVersion(path: string, version: string) {
9697
export async function writeLockfile(
9798
name: string,
9899
data: {
99-
version: string;
100100
repository: string;
101+
version: string;
101102
files: FilesArray;
102103
},
103104
filepath: string,
104105
): Promise<void> {
105106
let lockfile: Lockfile;
106107
const vendorLock: VendorLock = {
107-
version: data.version,
108108
repository: data.repository,
109+
version: data.version,
109110
files: configFilesToVendorlockFiles(data.files, data.version),
110111
};
111112

@@ -119,6 +120,57 @@ export async function writeLockfile(
119120
await writeFile(filepath, JSON.stringify(lockfile, null, 2));
120121
}
121122

123+
export async function getNewVersion(
124+
dependency: VendorDependency,
125+
repo: Repository,
126+
showOutdatedOnly?: boolean,
127+
): Promise<string> {
128+
let newVersion: string;
129+
130+
if (dependency.hashVersionFile) {
131+
let hashVersionFile = dependency.hashVersionFile;
132+
if (hashVersionFile === true) {
133+
if (typeof dependency.files[0] === 'string') {
134+
hashVersionFile = dependency.files[0];
135+
} else if (typeof dependency.files[0] === 'object') {
136+
hashVersionFile = Object.keys(dependency.files[0])[0];
137+
} else {
138+
error(
139+
`files[0] is invalid for hashVersionFile, must be a string or an object - got ${typeof dependency
140+
.files[0]}`,
141+
);
142+
}
143+
}
144+
if (typeof hashVersionFile === 'string') {
145+
const fileCommitSha = await github
146+
.getFileCommitSha({
147+
repo,
148+
path: hashVersionFile,
149+
})
150+
.catch((err) => {
151+
error(
152+
`Error while getting commit sha for ${hashVersionFile}:\n${err}`,
153+
);
154+
});
155+
newVersion = fileCommitSha;
156+
} else {
157+
error('hashVersionFile is invalid, must be a string or true');
158+
}
159+
} else {
160+
try {
161+
const latestRelease = await github.getLatestRelease(repo);
162+
newVersion = latestRelease.tag_name as string;
163+
} catch {
164+
if (showOutdatedOnly) {
165+
error(`Could not find a version for ${dependency.name}`);
166+
}
167+
newVersion = '';
168+
}
169+
}
170+
171+
return newVersion;
172+
}
173+
122174
export async function checkIfNeedsUpdate({
123175
lockfilePath,
124176
name,

package.json

+5-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "vendorfiles",
33
"author": "Araxeus",
44
"description": "A CLI tool to manage vendor files",
5-
"version": "1.1.6",
5+
"version": "1.2.0",
66
"type": "module",
77
"license": "MIT",
88
"repository": "https://github.com/Araxeus/vendorfiles",
@@ -41,7 +41,7 @@
4141
"publish": "yarn check && yarn npm publish"
4242
},
4343
"dependencies": {
44-
"@commander-js/extra-typings": "^12.0.1",
44+
"@commander-js/extra-typings": "^12.1.0",
4545
"@ltd/j-toml": "^1.38.0",
4646
"@octokit/auth-oauth-device": "^7.1.1",
4747
"@octokit/rest": "^20.1.1",
@@ -52,17 +52,17 @@
5252
"make-fetch-happen": "^13.0.1",
5353
"open": "^10.1.0",
5454
"parse-json": "^8.1.0",
55-
"unarchive": "^1.1.1",
55+
"unarchive": "^1.1.2",
5656
"yaml": "^2.4.2"
5757
},
5858
"devDependencies": {
5959
"@biomejs/biome": "^1.7.3",
6060
"@types/make-fetch-happen": "^10.0.4",
61-
"@types/node": "^20.12.12",
61+
"@types/node": "^20.12.13",
6262
"@types/parse-json": "^4.0.2",
6363
"cpy-cli": "^5.0.0",
6464
"del-cli": "^5.1.0",
65-
"type-fest": "^4.18.2",
65+
"type-fest": "^4.18.3",
6666
"typescript": "=5.0.4"
6767
},
6868
"vendorDependencies": {

0 commit comments

Comments
 (0)