-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add [WingetVersion] Badge #10245
Merged
Merged
add [WingetVersion] Badge #10245
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
7a22dbc
feat: add winget version badge
anatawa12 f71b6b8
chore: accept dotted path instead of slashed
anatawa12 d387631
test: add test for winget-version
anatawa12 d9963b5
fix: remove debug code
anatawa12 6b70732
chore: use winget-specific version compare algorithm
anatawa12 439c5ef
fix: support latest and unknown
anatawa12 120d3ce
fix(winget/version): trailing '.0' handling is incorrect
anatawa12 439a7e0
fix(winget/version): latest returns last newest version instead of th…
anatawa12 aabbc38
fix(winget/version): confusing subpackage and version name
anatawa12 2cbd02e
fix(winget/version): example for latest is incorrect
anatawa12 c9e7d3a
add a couple of extra test cases for latest()
chris48s File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
/** | ||
* Comparing versions with winget's version comparator. | ||
* | ||
* See https://github.com/microsoft/winget-cli/blob/ae566c7bf21cfcc75be7ec30e4036a30eede8396/src/AppInstallerSharedLib/Versions.cpp for original implementation. | ||
* | ||
* @module | ||
*/ | ||
|
||
/** | ||
* Compares two strings representing version numbers lexicographically and returns an integer value. | ||
* | ||
* @param {string} v1 - The first version to compare | ||
* @param {string} v2 - The second version to compare | ||
* @returns {number} -1 if v1 is smaller than v2, 1 if v1 is larger than v2, 0 if v1 and v2 are equal | ||
* @example | ||
* compareVersion('1.2.3', '1.2.4') // returns -1 because numeric part of first version is smaller than the numeric part of second version. | ||
*/ | ||
function compareVersion(v1, v2) { | ||
// https://github.com/microsoft/winget-cli/blob/ae566c7bf21cfcc75be7ec30e4036a30eede8396/src/AppInstallerSharedLib/Versions.cpp#L109-L173 | ||
// This implementation does not parse s_Approximate_Greater_Than | ||
// and s_Approximate_Less_Than since they won't appear in directory name (package version parsed by shields.io) | ||
const v1Trimmed = trimPrefix(v1) | ||
const v2Trimmed = trimPrefix(v2) | ||
|
||
const v1Latest = v1Trimmed.trim().toLowerCase() === 'latest' | ||
const v2Latest = v2Trimmed.trim().toLowerCase() === 'latest' | ||
|
||
if (v1Latest && v2Latest) { | ||
return 0 | ||
} else if (v1Latest) { | ||
return 1 | ||
} else if (v2Latest) { | ||
return -1 | ||
} | ||
|
||
const v1Unknown = v1Trimmed.trim().toLowerCase() === 'unknown' | ||
const v2Unknown = v2Trimmed.trim().toLowerCase() === 'unknown' | ||
|
||
if (v1Unknown && v2Unknown) { | ||
return 0 | ||
} else if (v1Unknown) { | ||
return -1 | ||
} else if (v2Unknown) { | ||
return 1 | ||
} | ||
|
||
const parts1 = v1Trimmed.split('.') | ||
const parts2 = v2Trimmed.split('.') | ||
|
||
trimLastZeros(parts1) | ||
trimLastZeros(parts2) | ||
|
||
for (let i = 0; i < Math.min(parts1.length, parts2.length); i++) { | ||
const part1 = parts1[i] | ||
const part2 = parts2[i] | ||
|
||
const compare = compareVersionPart(part1, part2) | ||
if (compare !== 0) { | ||
return compare | ||
} | ||
} | ||
|
||
if (parts1.length === parts2.length) { | ||
return 0 | ||
} | ||
|
||
if (parts1.length > parts2.length) { | ||
return 1 | ||
} else if (parts1.length < parts2.length) { | ||
return -1 | ||
} | ||
|
||
return 0 | ||
} | ||
|
||
/** | ||
* Removes all leading non-digit characters from a version number string | ||
* if there is a digit before the split character, or no split characters exist. | ||
* | ||
* @param {string} version The version number string to trim | ||
* @returns {string} The version number string with all leading non-digit characters removed | ||
*/ | ||
function trimPrefix(version) { | ||
// https://github.com/microsoft/winget-cli/blob/ae566c7bf21cfcc75be7ec30e4036a30eede8396/src/AppInstallerSharedLib/Versions.cpp#L66 | ||
// If there is a digit before the split character, or no split characters exist, trim off all leading non-digit characters | ||
|
||
const digitPos = version.match(/(\d.*)/) | ||
const splitPos = version.match(/\./) | ||
if (digitPos && (splitPos == null || digitPos.index < splitPos.index)) { | ||
// there is digit before the split character so strip off all leading non-digit characters | ||
return version.slice(digitPos.index) | ||
} | ||
return version | ||
} | ||
|
||
/** | ||
* Removes all trailing zeros from a version number part array. | ||
* | ||
* @param {string[]} parts - parts | ||
*/ | ||
function trimLastZeros(parts) { | ||
while (parts.length > 1 && parts[parts.length - 1].trim() === '0') { | ||
parts.pop() | ||
} | ||
} | ||
|
||
/** | ||
* Compares two strings representing version number parts lexicographically and returns an integer value. | ||
* | ||
* @param {string} part1 - The first version part to compare | ||
* @param {string} part2 - The second version part to compare | ||
* @returns {number} -1 if part1 is smaller than part2, 1 if part1 is larger than part2, 0 if part1 and part2 are equal | ||
* @example | ||
* compareVersionPart('3', '4') // returns -1 because numeric part of first part is smaller than the numeric part of second part. | ||
*/ | ||
function compareVersionPart(part1, part2) { | ||
// https://github.com/microsoft/winget-cli/blob/ae566c7bf21cfcc75be7ec30e4036a30eede8396/src/AppInstallerSharedLib/Versions.cpp#L324-L352 | ||
const [, numericString1, other1] = part1.trim().match(/^(\d*)(.*)$/) | ||
const [, numericString2, other2] = part2.trim().match(/^(\d*)(.*)$/) | ||
const numeric1 = parseInt(numericString1 || '0', 10) | ||
const numeric2 = parseInt(numericString2 || '0', 10) | ||
|
||
if (numeric1 < numeric2) { | ||
return -1 | ||
} else if (numeric1 > numeric2) { | ||
return 1 | ||
} | ||
// numeric1 === numeric2 | ||
|
||
const otherFolded1 = (other1 ?? '').toLowerCase() | ||
const otherFolded2 = (other2 ?? '').toLowerCase() | ||
|
||
if (otherFolded1.length !== 0 && otherFolded2.length === 0) { | ||
return -1 | ||
} else if (otherFolded1.length === 0 && otherFolded2.length !== 0) { | ||
return 1 | ||
} | ||
|
||
if (otherFolded1 < otherFolded2) { | ||
return -1 | ||
} else if (otherFolded1 > otherFolded2) { | ||
return 1 | ||
} | ||
|
||
return 0 | ||
} | ||
|
||
/** | ||
* Finds the largest version number lexicographically from an array of strings representing version numbers and returns it as a string. | ||
* | ||
* @param {string[]} versions - The array of version numbers to compare | ||
* @returns {string|undefined} The largest version number as a string, or undefined if the array is empty | ||
* @example | ||
* latest(['1.2.3', '1.2.4', '1.3', '2.0']) // returns '2.0' because it is the largest version number. | ||
* latest(['1.2.3', '1.2.4', '1.3-alpha', '2.0-beta']) // returns '2.0-beta'. there is no special handling for pre-release versions. | ||
*/ | ||
function latest(versions) { | ||
const len = versions.length | ||
if (len === 0) { | ||
return | ||
} | ||
|
||
let version = versions[0] | ||
for (let i = 1; i < len; i++) { | ||
if (compareVersion(version, versions[i]) <= 0) { | ||
version = versions[i] | ||
} | ||
} | ||
return version | ||
} | ||
|
||
export { latest, compareVersion } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { test, given } from 'sazerac' | ||
import { compareVersion, latest } from './version.js' | ||
|
||
describe('Winget Version helpers', function () { | ||
test(compareVersion, () => { | ||
// basic compare | ||
// https://github.com/microsoft/winget-cli/blob/43425fe97d237e03026fca4530dbc422ab445595/src/AppInstallerCLITests/Versions.cpp#L147 | ||
given('1', '2').expect(-1) | ||
given('1.0.0', '2.0.0').expect(-1) | ||
given('0.0.1', '0.0.2').expect(-1) | ||
given('0.0.1-alpha', '0.0.2-alpha').expect(-1) | ||
given('0.0.1-beta', '0.0.2-alpha').expect(-1) | ||
given('0.0.1-beta', '0.0.2-alpha').expect(-1) | ||
given('13.9.8', '14.1').expect(-1) | ||
|
||
given('1.0', '1.0.0').expect(0) | ||
|
||
// Ensure whitespace doesn't affect equality | ||
given('1.0', '1.0 ').expect(0) | ||
given('1.0', '1. 0').expect(0) | ||
|
||
// Ensure versions with preambles are sorted correctly | ||
given('1.0', 'Version 1.0').expect(0) | ||
given('foo1', 'bar1').expect(0) | ||
given('v0.0.1', '0.0.2').expect(-1) | ||
given('v0.0.1', 'v0.0.2').expect(-1) | ||
given('1.a2', '1.b1').expect(-1) | ||
given('alpha', 'beta').expect(-1) | ||
|
||
// latest | ||
// https://github.com/microsoft/winget-cli/blob/43425fe97d237e03026fca4530dbc422ab445595/src/AppInstallerCLITests/Versions.cpp#L217 | ||
given('1.0', 'latest').expect(-1) | ||
given('100', 'latest').expect(-1) | ||
given('943849587389754876.1', 'latest').expect(-1) | ||
given('latest', 'LATEST').expect(0) | ||
|
||
// unknown | ||
// https://github.com/microsoft/winget-cli/blob/43425fe97d237e03026fca4530dbc422ab445595/src/AppInstallerCLITests/Versions.cpp#L231 | ||
given('unknown', '1.0').expect(-1) | ||
given('unknown', '1.fork').expect(-1) | ||
given('unknown', 'UNKNOWN').expect(0) | ||
|
||
// porting failure tests | ||
// https://github.com/badges/shields/pull/10245#discussion_r1817931237 | ||
// trailing .0 and .0-beta | ||
given('1.6.0', '1.6.0-beta.98').expect(-1) | ||
}) | ||
|
||
test(latest, () => { | ||
given(['1.2.3', '1.2.4', '2.0', '1.3.9.1']).expect('2.0') | ||
given(['1.2.3', '1.2.4', '2.0-beta', '1.3-alpha']).expect('2.0-beta') | ||
|
||
// compareVersion('3.1.1.0', '3.1.1') == 0, so It's free to choose any of them. | ||
// I don't know why but it looks winget registry uses last newest version. | ||
given(['3.1.1.0', '3.1.1']).expect('3.1.1') | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import Joi from 'joi' | ||
import gql from 'graphql-tag' | ||
import { renderVersionBadge } from '../version.js' | ||
import { InvalidParameter, pathParam } from '../index.js' | ||
import { GithubAuthV4Service } from '../github/github-auth-service.js' | ||
import { transformErrors } from '../github/github-helpers.js' | ||
import { latest } from './version.js' | ||
|
||
const schema = Joi.object({ | ||
data: Joi.object({ | ||
repository: Joi.object({ | ||
object: Joi.object({ | ||
entries: Joi.array().items( | ||
Joi.object({ | ||
type: Joi.string().required(), | ||
name: Joi.string().required(), | ||
object: Joi.object({ | ||
entries: Joi.array().items( | ||
Joi.object({ | ||
type: Joi.string().required(), | ||
name: Joi.string().required(), | ||
}), | ||
), | ||
}).required(), | ||
}), | ||
), | ||
}) | ||
.allow(null) | ||
.required(), | ||
}).required(), | ||
}).required(), | ||
}).required() | ||
chris48s marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
export default class WingetVersion extends GithubAuthV4Service { | ||
static category = 'version' | ||
|
||
static route = { | ||
base: 'winget/v', | ||
pattern: ':name', | ||
} | ||
|
||
static openApi = { | ||
'/winget/v/{name}': { | ||
get: { | ||
summary: 'WinGet Package Version', | ||
description: 'WinGet Community Repository', | ||
parameters: [ | ||
pathParam({ | ||
name: 'name', | ||
example: 'Microsoft.WSL', | ||
}), | ||
], | ||
}, | ||
}, | ||
} | ||
|
||
static defaultBadgeData = { | ||
label: 'winget', | ||
} | ||
|
||
async fetch({ name }) { | ||
const nameFirstLower = name[0].toLowerCase() | ||
const nameSlashed = name.replaceAll('.', '/') | ||
const path = `manifests/${nameFirstLower}/${nameSlashed}` | ||
const expression = `HEAD:${path}` | ||
return this._requestGraphql({ | ||
query: gql` | ||
query RepoFiles($expression: String!) { | ||
repository(owner: "microsoft", name: "winget-pkgs") { | ||
object(expression: $expression) { | ||
... on Tree { | ||
entries { | ||
type | ||
name | ||
object { | ||
... on Tree { | ||
entries { | ||
type | ||
name | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
`, | ||
variables: { expression }, | ||
schema, | ||
transformErrors, | ||
}) | ||
} | ||
|
||
async handle({ name }) { | ||
const json = await this.fetch({ name }) | ||
if (json.data.repository.object?.entries == null) { | ||
throw new InvalidParameter({ | ||
prettyMessage: 'package not found', | ||
}) | ||
} | ||
const entries = json.data.repository.object.entries | ||
const directories = entries.filter(entry => entry.type === 'tree') | ||
const versionDirs = directories.filter(dir => | ||
dir.object.entries.some( | ||
file => file.type === 'blob' && file.name === `${name}.yaml`, | ||
), | ||
) | ||
const versions = versionDirs.map(dir => dir.name) | ||
const version = latest(versions) | ||
|
||
if (version == null) { | ||
throw new InvalidParameter({ | ||
prettyMessage: 'no versions found', | ||
}) | ||
} | ||
|
||
return renderVersionBadge({ version }) | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll be honest: I don't look forward to the first time someone raises an issue saying "winget version badge shows me version X but
winget show
says latest is version Y" but I think the key is having really solid tests in place to make sure we're not introducing regressions if we need to change this. Thanks for adding all these and annotating/explaining where these all comes from 👍