Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 37 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,17 @@ deno task install
semver <command>

Commands:
semver get Get the version
semver set <value> Set the version
semver inc Increment the version
semver parse [value] Parse the version and print
semver cmp <v1> <v2> Compare v1 to v2 and return -1/0/1
semver gt <v1> <v2> Return 0 if v1 is greater than v2, else 1
semver gte <v1> <v2> Return 0 if v1 is greater than or equal to v2, else 1
semver lt <v1> <v2> Return 0 if v1 is less than v2, else 1
semver lte <v1> <v2> Return 0 if v1 is less than or equal to v2, else 1
semver eq <v1> <v2> Return 0 if v1 is equal to v2, else 1
semver get Get the version
semver set <value> Set the version
semver inc Increment the version
semver parse [value] Parse the version and print
semver cmp <v1> <v2> Compare v1 to v2 and return -1/0/1
semver gt <v1> <v2> Return 0 if v1 is greater than v2, else 1
semver gte <v1> <v2> Return 0 if v1 is greater than or equal to v2, else 1
semver lt <v1> <v2> Return 0 if v1 is less than v2, else 1
semver lte <v1> <v2> Return 0 if v1 is less than or equal to v2, else 1
semver eq <v1> <v2> Return 0 if v1 is equal to v2, else 1
semver sort [versions..] Sort semantic versions

Options:
--help Show help [boolean]
Expand All @@ -74,6 +75,10 @@ command will create the `VERSION` file if it doesn't already exist.
The `parse` command accepts a version string as input and parses and prints that
version as output if it is valid.

The `sort` command accepts one or more version strings and outputs them in
sorted order (descending by default, one version per line). Use the `-a` flag
for ascending order, or read versions from stdin using `--`.

#### examples

```sh
Expand All @@ -97,6 +102,28 @@ semver get # 1.2.3
semver parse 1.0.0 # {"major":1,"minor":1,"patch":0,"prerelease":[],"build":[]}
```

```sh
# sort versions in descending order (default)
semver sort 2.0.0 1.0.0 3.0.0
# 3.0.0
# 2.0.0
# 1.0.0
```

```sh
# sort versions in ascending order
semver sort -a 2.0.0 1.0.0 3.0.0
# 1.0.0
# 2.0.0
# 3.0.0
```

```sh
# sort versions from stdin
cat versions.txt | semver sort --
# (sorted output)
```

### Incrementing

When calling the command `inc` the `VERSION` file will be updated based on the
Expand Down
19 changes: 18 additions & 1 deletion deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
lte,
parse,
set,
sort,
} from "./src/commands/mod.ts";
import { getContext } from "./src/context.ts";
import { ApplicationError } from "./src/errors/application.error.ts";
Expand All @@ -34,6 +35,7 @@ try {
.command(lt)
.command(lte)
.command(eq)
.command(sort)
.strictOptions()
.strictCommands()
.demandCommand(1)
Expand Down
1 change: 1 addition & 0 deletions src/commands/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from "./gte.ts";
export * from "./lt.ts";
export * from "./lte.ts";
export * from "./eq.ts";
export * from "./sort.ts";
151 changes: 151 additions & 0 deletions src/commands/sort.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { describe, it } from "testing/bdd";
import { assertSpyCall, assertSpyCalls, stub } from "testing/mock";
import { Arguments } from "yargs";
import { sort } from "./sort.ts";
import { testContext } from "../util/testContext.ts";
import { IContext } from "../context.ts";

describe("sort", () => {
const ctx = testContext({
consoleLog: () => stub(console, "log"),
});

it("SORT00 - sorts versions in descending order by default", async () => {
await sort.handler(
{
_: [],
versions: ["2.0.0", "1.0.0", "3.0.0"],
asc: false,
desc: false,
} as unknown as Arguments & IContext,
);
assertSpyCall(ctx.consoleLog, 0, {
args: ["3.0.0"],
});
assertSpyCall(ctx.consoleLog, 1, {
args: ["2.0.0"],
});
assertSpyCall(ctx.consoleLog, 2, {
args: ["1.0.0"],
});
assertSpyCalls(ctx.consoleLog, 3);
});

it("SORT01 - sorts versions in ascending order with -a flag", async () => {
await sort.handler(
{
_: [],
versions: ["2.0.0", "1.0.0", "3.0.0"],
asc: true,
desc: false,
} as unknown as Arguments & IContext,
);
assertSpyCall(ctx.consoleLog, 0, {
args: ["1.0.0"],
});
assertSpyCall(ctx.consoleLog, 1, {
args: ["2.0.0"],
});
assertSpyCall(ctx.consoleLog, 2, {
args: ["3.0.0"],
});
assertSpyCalls(ctx.consoleLog, 3);
});

it("SORT02 - sorts versions in descending order with -d flag", async () => {
await sort.handler(
{
_: [],
versions: ["1.0.0", "3.0.0", "2.0.0"],
asc: false,
desc: true,
} as unknown as Arguments & IContext,
);
assertSpyCall(ctx.consoleLog, 0, {
args: ["3.0.0"],
});
assertSpyCall(ctx.consoleLog, 1, {
args: ["2.0.0"],
});
assertSpyCall(ctx.consoleLog, 2, {
args: ["1.0.0"],
});
assertSpyCalls(ctx.consoleLog, 3);
});

it("SORT03 - handles prerelease versions correctly", async () => {
await sort.handler(
{
_: [],
versions: ["1.0.0", "1.0.0-alpha", "1.0.0-beta"],
asc: true,
desc: false,
} as unknown as Arguments & IContext,
);
assertSpyCall(ctx.consoleLog, 0, {
args: ["1.0.0-alpha"],
});
assertSpyCall(ctx.consoleLog, 1, {
args: ["1.0.0-beta"],
});
assertSpyCall(ctx.consoleLog, 2, {
args: ["1.0.0"],
});
assertSpyCalls(ctx.consoleLog, 3);
});

it("SORT04 - handles build metadata correctly", async () => {
await sort.handler(
{
_: [],
versions: ["1.0.0+build1", "1.0.0+build2", "1.0.0"],
asc: true,
desc: false,
} as unknown as Arguments & IContext,
);
// Build metadata should not affect sort order
assertSpyCalls(ctx.consoleLog, 3);
Comment thread
justinmchase marked this conversation as resolved.
Outdated
});

it("SORT05 - sorts complex semver versions", async () => {
await sort.handler(
{
_: [],
versions: ["1.0.0", "2.1.0", "2.0.0", "1.1.0", "1.0.1"],
asc: true,
desc: false,
} as unknown as Arguments & IContext,
);
assertSpyCall(ctx.consoleLog, 0, {
args: ["1.0.0"],
});
assertSpyCall(ctx.consoleLog, 1, {
args: ["1.0.1"],
});
assertSpyCall(ctx.consoleLog, 2, {
args: ["1.1.0"],
});
assertSpyCall(ctx.consoleLog, 3, {
args: ["2.0.0"],
});
assertSpyCall(ctx.consoleLog, 4, {
args: ["2.1.0"],
});
assertSpyCalls(ctx.consoleLog, 5);
});

it("SORT06 - handles single version", async () => {
await sort.handler(
{
_: [],
versions: ["1.0.0"],
asc: false,
desc: false,
} as unknown as Arguments & IContext,
);
assertSpyCall(ctx.consoleLog, 0, {
args: ["1.0.0"],
});
assertSpyCalls(ctx.consoleLog, 1);
});
});
91 changes: 91 additions & 0 deletions src/commands/sort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Arguments, YargsInstance } from "yargs";
import { compare, parse } from "semver";
import { InvalidVersionError } from "../errors/mod.ts";
import { IContext } from "../context.ts";

export const sort = {
command: "sort [versions..]",
describe: "Sort semantic versions",
builder(yargs: YargsInstance) {
return yargs
.positional("versions", {
describe: "Versions to sort",
type: "string",
array: true,
})
.option("asc", {
alias: "a",
type: "boolean",
description: "Sort in ascending order",
default: false,
})
.option("desc", {
alias: "d",
type: "boolean",
description: "Sort in descending order (default)",
default: false,
});
},
async handler(args: Arguments & IContext) {
const { versions, asc, desc } = args;
let versionList: string[] = [];

// Check if we should read from stdin
// The main.ts filters out "--" from args, so if versions is undefined or empty,
// we check if "--" was in the original Deno.args
const hasStdinFlag = Deno.args.includes("--");

if (hasStdinFlag || !versions || versions.length === 0) {
// Read from stdin
const decoder = new TextDecoder();
const data = await Deno.stdin.readable;
const reader = data.getReader();
let buffer = "";

try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
}
} finally {
reader.releaseLock();
}

versionList = buffer
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0);
} else if (Array.isArray(versions)) {
versionList = versions;
}
Comment thread
justinmchase marked this conversation as resolved.

if (versionList.length === 0) {
throw new Error("No versions provided");
}
Comment thread
justinmchase marked this conversation as resolved.

// Parse and validate all versions
const parsedVersions = versionList.map((v) => {
const parsed = parse(v);
if (!parsed) {
throw new InvalidVersionError(v);
}
return { original: v, parsed };
});

// Sort versions using semver compare
parsedVersions.sort((a, b) => compare(a.parsed, b.parsed));

// Determine sort order
// Default is descending unless -a/--asc is explicitly set
const isDescending = desc || (!asc && !desc);

// Reverse if descending
if (isDescending) {
parsedVersions.reverse();
}

// Output sorted versions, one per line
parsedVersions.forEach((v) => console.log(v.original));
},
};
Loading