pre-release filtering
This commit is contained in:
parent
a9a0e7e9f3
commit
6ad55474fa
6 changed files with 583 additions and 56 deletions
72
src/main.zig
72
src/main.zig
|
@ -67,6 +67,7 @@ pub const Release = struct {
|
|||
description: []const u8,
|
||||
provider: []const u8,
|
||||
is_tag: bool = false,
|
||||
is_prerelease: bool = false, // Track if this is a prerelease/draft
|
||||
|
||||
pub fn deinit(self: Release, allocator: Allocator) void {
|
||||
allocator.free(self.repo_name);
|
||||
|
@ -205,8 +206,25 @@ pub fn main() !u8 {
|
|||
// Sort all releases by published date (most recent first)
|
||||
std.mem.sort(Release, all_releases.items, {}, utils.compareReleasesByDate);
|
||||
|
||||
// Filter out prereleases after duplicate detection is complete
|
||||
// Exception: Keep git-style version prereleases (like kraftkit)
|
||||
var filtered_releases = std.ArrayList(Release).init(allocator);
|
||||
defer filtered_releases.deinit();
|
||||
|
||||
for (all_releases.items) |release| {
|
||||
if (!release.is_prerelease) {
|
||||
try filtered_releases.append(release);
|
||||
} else {
|
||||
// Check if this is a git-style version (e.g., v1.2.3-123-g1234567)
|
||||
// These should be included even if marked as prerelease
|
||||
if (isGitStyleVersion(release.tag_name)) {
|
||||
try filtered_releases.append(release);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate Atom feed from filtered releases
|
||||
const atom_content = try atom.generateFeed(allocator, all_releases.items);
|
||||
const atom_content = try atom.generateFeed(allocator, filtered_releases.items);
|
||||
defer allocator.free(atom_content);
|
||||
|
||||
// Write to output file
|
||||
|
@ -218,12 +236,61 @@ pub fn main() !u8 {
|
|||
_ = checkFileSizeAndWarn(atom_content.len);
|
||||
|
||||
// Log to stderr for user feedback
|
||||
printInfo("Total releases in feed: {} of {} total in last {} days\n", .{ all_releases.items.len, original_count, @divTrunc(RELEASE_AGE_LIMIT_SECONDS, std.time.s_per_day) });
|
||||
printInfo("Total releases in feed: {} of {} total in last {} days\n", .{ filtered_releases.items.len, original_count, @divTrunc(RELEASE_AGE_LIMIT_SECONDS, std.time.s_per_day) });
|
||||
printInfo("Updated feed written to: {s}\n", .{output_file});
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Check if a tag contains git-style version pattern like v1.2.3-123-g1234567
|
||||
fn isGitStyleVersion(tag_name: []const u8) bool {
|
||||
// Convert to lowercase for comparison
|
||||
var tag_lower_buf: [256]u8 = undefined;
|
||||
if (tag_name.len >= tag_lower_buf.len) return false;
|
||||
|
||||
const tag_lower = std.ascii.lowerString(tag_lower_buf[0..tag_name.len], tag_name);
|
||||
|
||||
// Look for pattern: -number-g followed by hex characters
|
||||
var i: usize = 0;
|
||||
while (i < tag_lower.len) {
|
||||
if (tag_lower[i] == '-') {
|
||||
// Found a dash, check if followed by digits
|
||||
var j = i + 1;
|
||||
var has_digits = false;
|
||||
|
||||
// Skip digits
|
||||
while (j < tag_lower.len and tag_lower[j] >= '0' and tag_lower[j] <= '9') {
|
||||
has_digits = true;
|
||||
j += 1;
|
||||
}
|
||||
|
||||
// Check if followed by -g and hex characters
|
||||
if (has_digits and j + 2 < tag_lower.len and
|
||||
tag_lower[j] == '-' and tag_lower[j + 1] == 'g')
|
||||
{
|
||||
// Check if followed by hex characters
|
||||
var k = j + 2;
|
||||
var hex_count: usize = 0;
|
||||
while (k < tag_lower.len and
|
||||
((tag_lower[k] >= '0' and tag_lower[k] <= '9') or
|
||||
(tag_lower[k] >= 'a' and tag_lower[k] <= 'f')))
|
||||
{
|
||||
hex_count += 1;
|
||||
k += 1;
|
||||
}
|
||||
|
||||
// If we found hex characters, this looks like git describe format
|
||||
if (hex_count >= 4) { // At least 4 hex chars for a reasonable commit hash
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
fn formatTimestampForDisplay(allocator: Allocator, timestamp: i64) ![]const u8 {
|
||||
if (timestamp == 0) {
|
||||
return try allocator.dupe(u8, "beginning of time");
|
||||
|
@ -496,6 +563,7 @@ test {
|
|||
std.testing.refAllDecls(@import("timestamp_tests.zig"));
|
||||
std.testing.refAllDecls(@import("atom.zig"));
|
||||
std.testing.refAllDecls(@import("utils.zig"));
|
||||
std.testing.refAllDecls(@import("tag_filter.zig"));
|
||||
std.testing.refAllDecls(@import("providers/GitHub.zig"));
|
||||
std.testing.refAllDecls(@import("providers/GitLab.zig"));
|
||||
std.testing.refAllDecls(@import("providers/SourceHut.zig"));
|
||||
|
|
|
@ -4,6 +4,7 @@ const json = std.json;
|
|||
const Allocator = std.mem.Allocator;
|
||||
const ArrayList = std.ArrayList;
|
||||
const utils = @import("../utils.zig");
|
||||
const tag_filter = @import("../tag_filter.zig");
|
||||
|
||||
const Release = @import("../main.zig").Release;
|
||||
const Provider = @import("../Provider.zig");
|
||||
|
@ -224,6 +225,13 @@ fn getRepoReleases(allocator: Allocator, client: *http.Client, token: []const u8
|
|||
const tag_name_value = obj.get("tag_name") orelse continue;
|
||||
if (tag_name_value != .string) continue;
|
||||
|
||||
const tag_name = tag_name_value.string;
|
||||
|
||||
// Skip problematic tags
|
||||
if (tag_filter.shouldSkipTag(allocator, tag_name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const published_at_value = obj.get("published_at") orelse continue;
|
||||
if (published_at_value != .string) continue;
|
||||
|
||||
|
@ -235,7 +243,7 @@ fn getRepoReleases(allocator: Allocator, client: *http.Client, token: []const u8
|
|||
|
||||
const release = Release{
|
||||
.repo_name = try allocator.dupe(u8, repo),
|
||||
.tag_name = try allocator.dupe(u8, tag_name_value.string),
|
||||
.tag_name = try allocator.dupe(u8, tag_name),
|
||||
.published_at = try utils.parseReleaseTimestamp(published_at_value.string),
|
||||
.html_url = try allocator.dupe(u8, html_url_value.string),
|
||||
.description = try allocator.dupe(u8, body_str),
|
||||
|
@ -342,3 +350,26 @@ test "codeberg release parsing with live data snapshot" {
|
|||
);
|
||||
try std.testing.expectEqualStrings("codeberg", releases.items[0].provider);
|
||||
}
|
||||
|
||||
test "Codeberg tag filtering" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
// Test that Codeberg now uses the same filtering as other providers
|
||||
const problematic_tags = [_][]const u8{
|
||||
"nightly", "prerelease", "latest", "edge", "canary", "dev-branch",
|
||||
};
|
||||
|
||||
for (problematic_tags) |tag| {
|
||||
try std.testing.expect(tag_filter.shouldSkipTag(allocator, tag));
|
||||
}
|
||||
|
||||
// Test that valid tags are not filtered
|
||||
const valid_tags = [_][]const u8{
|
||||
"v1.0.0", "v2.1.3-stable",
|
||||
// Note: v1.0.0-alpha.1 is now filtered to avoid duplicates
|
||||
};
|
||||
|
||||
for (valid_tags) |tag| {
|
||||
try std.testing.expect(!tag_filter.shouldSkipTag(allocator, tag));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ const Allocator = std.mem.Allocator;
|
|||
const ArrayList = std.ArrayList;
|
||||
const Thread = std.Thread;
|
||||
const utils = @import("../utils.zig");
|
||||
const tag_filter = @import("../tag_filter.zig");
|
||||
|
||||
const Release = @import("../main.zig").Release;
|
||||
const Provider = @import("../Provider.zig");
|
||||
|
@ -488,6 +489,10 @@ fn getRepoReleases(allocator: Allocator, client: *http.Client, token: []const u8
|
|||
for (array.items) |item| {
|
||||
const obj = item.object;
|
||||
|
||||
// Track prerelease and draft status but don't filter yet
|
||||
const is_prerelease = if (obj.get("prerelease")) |prerelease| prerelease.bool else false;
|
||||
const is_draft = if (obj.get("draft")) |draft| draft.bool else false;
|
||||
|
||||
const body_value = obj.get("body") orelse json.Value{ .string = "" };
|
||||
const body_str = if (body_value == .string) body_value.string else "";
|
||||
|
||||
|
@ -499,6 +504,7 @@ fn getRepoReleases(allocator: Allocator, client: *http.Client, token: []const u8
|
|||
.description = try allocator.dupe(u8, body_str),
|
||||
.provider = try allocator.dupe(u8, "github"),
|
||||
.is_tag = false,
|
||||
.is_prerelease = is_prerelease or is_draft, // Mark as prerelease if either flag is true
|
||||
};
|
||||
|
||||
try releases.append(release);
|
||||
|
@ -507,57 +513,9 @@ fn getRepoReleases(allocator: Allocator, client: *http.Client, token: []const u8
|
|||
return releases;
|
||||
}
|
||||
|
||||
fn shouldSkipTag(allocator: std.mem.Allocator, tag_name: []const u8) bool {
|
||||
// List of common moving tags that should be filtered out
|
||||
const moving_tags = [_][]const u8{
|
||||
// common "latest commit tags"
|
||||
"latest",
|
||||
"tip",
|
||||
"continuous",
|
||||
"head",
|
||||
|
||||
// common branch tags
|
||||
"main",
|
||||
"master",
|
||||
"trunk",
|
||||
"develop",
|
||||
"development",
|
||||
"dev",
|
||||
|
||||
// common fast moving channel names
|
||||
"nightly",
|
||||
"edge",
|
||||
"canary",
|
||||
"alpha",
|
||||
|
||||
// common slower channels, but without version information
|
||||
// they probably are not something we're interested in
|
||||
"beta",
|
||||
"rc",
|
||||
"release",
|
||||
"snapshot",
|
||||
"unstable",
|
||||
"experimental",
|
||||
"prerelease",
|
||||
"preview",
|
||||
};
|
||||
|
||||
// Check if tag name contains common moving patterns
|
||||
const tag_lower = std.ascii.allocLowerString(allocator, tag_name) catch return false;
|
||||
defer allocator.free(tag_lower);
|
||||
|
||||
for (moving_tags) |moving_tag|
|
||||
if (std.mem.eql(u8, tag_lower, moving_tag))
|
||||
return true;
|
||||
|
||||
// Skip pre-release and development tags
|
||||
if (std.mem.startsWith(u8, tag_lower, "pre-") or
|
||||
std.mem.startsWith(u8, tag_lower, "dev-") or
|
||||
std.mem.startsWith(u8, tag_lower, "test-") or
|
||||
std.mem.startsWith(u8, tag_lower, "debug-"))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
/// Wrapper function for backward compatibility and testing
|
||||
pub fn shouldSkipTag(allocator: std.mem.Allocator, tag_name: []const u8) bool {
|
||||
return tag_filter.shouldSkipTag(allocator, tag_name);
|
||||
}
|
||||
|
||||
fn getRepoTags(allocator: Allocator, client: *http.Client, token: []const u8, repo: []const u8) !ArrayList(Release) {
|
||||
|
@ -660,7 +618,7 @@ fn parseGraphQL(allocator: std.mem.Allocator, repo: []const u8, body: []const u8
|
|||
const tag_name = node_obj.get("name").?.string;
|
||||
|
||||
// Skip common moving tags
|
||||
if (shouldSkipTag(allocator, tag_name)) continue;
|
||||
if (tag_filter.shouldSkipTag(allocator, tag_name)) continue;
|
||||
|
||||
const target = node_obj.get("target").?.object;
|
||||
|
||||
|
@ -1012,6 +970,114 @@ test "addNonReleaseTags should not add duplicate tags" {
|
|||
try std.testing.expect(found_unique);
|
||||
}
|
||||
|
||||
test "GitHub prerelease and draft filtering" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
// Test shouldSkipRelease function
|
||||
try std.testing.expect(tag_filter.shouldSkipRelease(true, false)); // prerelease
|
||||
try std.testing.expect(tag_filter.shouldSkipRelease(false, true)); // draft
|
||||
try std.testing.expect(tag_filter.shouldSkipRelease(true, true)); // both
|
||||
try std.testing.expect(!tag_filter.shouldSkipRelease(false, false)); // normal release
|
||||
|
||||
// Mock GitHub API response with prerelease and draft fields
|
||||
const github_api_response =
|
||||
\\[
|
||||
\\ {
|
||||
\\ "tag_name": "v1.0.0",
|
||||
\\ "published_at": "2024-01-01T00:00:00Z",
|
||||
\\ "html_url": "https://github.com/test/repo/releases/tag/v1.0.0",
|
||||
\\ "body": "Stable release",
|
||||
\\ "prerelease": false,
|
||||
\\ "draft": false
|
||||
\\ },
|
||||
\\ {
|
||||
\\ "tag_name": "v1.1.0-alpha",
|
||||
\\ "published_at": "2024-01-02T00:00:00Z",
|
||||
\\ "html_url": "https://github.com/test/repo/releases/tag/v1.1.0-alpha",
|
||||
\\ "body": "Alpha release",
|
||||
\\ "prerelease": true,
|
||||
\\ "draft": false
|
||||
\\ },
|
||||
\\ {
|
||||
\\ "tag_name": "v1.2.0-draft",
|
||||
\\ "published_at": "2024-01-03T00:00:00Z",
|
||||
\\ "html_url": "https://github.com/test/repo/releases/tag/v1.2.0-draft",
|
||||
\\ "body": "Draft release",
|
||||
\\ "prerelease": false,
|
||||
\\ "draft": true
|
||||
\\ }
|
||||
\\]
|
||||
;
|
||||
|
||||
// Parse the JSON to verify structure
|
||||
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, github_api_response, .{});
|
||||
defer parsed.deinit();
|
||||
|
||||
const array = parsed.value.array;
|
||||
try std.testing.expectEqual(@as(usize, 3), array.items.len);
|
||||
|
||||
// Verify the prerelease and draft fields are present and correct
|
||||
const stable_release = array.items[0].object;
|
||||
try std.testing.expectEqual(false, stable_release.get("prerelease").?.bool);
|
||||
try std.testing.expectEqual(false, stable_release.get("draft").?.bool);
|
||||
|
||||
const prerelease_item = array.items[1].object;
|
||||
try std.testing.expectEqual(true, prerelease_item.get("prerelease").?.bool);
|
||||
try std.testing.expectEqual(false, prerelease_item.get("draft").?.bool);
|
||||
|
||||
const draft_item = array.items[2].object;
|
||||
try std.testing.expectEqual(false, draft_item.get("prerelease").?.bool);
|
||||
try std.testing.expectEqual(true, draft_item.get("draft").?.bool);
|
||||
}
|
||||
|
||||
test "shouldSkipTag comprehensive filtering tests" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
// Test exact matches for moving tags
|
||||
const moving_tags = [_][]const u8{
|
||||
"latest", "tip", "continuous", "head", "main", "master", "trunk",
|
||||
"develop", "development", "dev", "nightly", "edge", "canary", "alpha",
|
||||
"beta", "rc", "release", "snapshot", "unstable", "experimental", "prerelease",
|
||||
"preview",
|
||||
};
|
||||
|
||||
for (moving_tags) |tag| {
|
||||
try std.testing.expect(shouldSkipTag(allocator, tag));
|
||||
|
||||
// Test case insensitive matching
|
||||
const upper_tag = try std.ascii.allocUpperString(allocator, tag);
|
||||
defer allocator.free(upper_tag);
|
||||
try std.testing.expect(shouldSkipTag(allocator, upper_tag));
|
||||
}
|
||||
|
||||
// Test prefix patterns
|
||||
const prefix_patterns = [_][]const u8{
|
||||
"pre-release", "pre-1.0.0", "dev-branch", "dev-feature",
|
||||
"test-build", "test-123", "debug-version", "debug-info",
|
||||
};
|
||||
|
||||
for (prefix_patterns) |tag| {
|
||||
try std.testing.expect(shouldSkipTag(allocator, tag));
|
||||
}
|
||||
|
||||
// Test valid version tags that should NOT be filtered
|
||||
const valid_tags = [_][]const u8{
|
||||
"v1.0.0", "v2.1.3", "1.0.0", "2.1.3-stable", "v1.0.0-final",
|
||||
"release-1.0.0", "stable-v1.0.0", "v1.0.0-lts",
|
||||
"2023.1.0",
|
||||
// Note: Semantic versioning prerelease tags are now filtered to avoid duplicates
|
||||
};
|
||||
|
||||
for (valid_tags) |tag| {
|
||||
try std.testing.expect(!shouldSkipTag(allocator, tag));
|
||||
}
|
||||
|
||||
// Test edge cases
|
||||
try std.testing.expect(!shouldSkipTag(allocator, "")); // Empty string
|
||||
try std.testing.expect(!shouldSkipTag(allocator, "v1.0.0+build.1")); // Build metadata
|
||||
try std.testing.expect(!shouldSkipTag(allocator, "nightly-build-v1.0.0")); // Contains "nightly" but has version
|
||||
}
|
||||
|
||||
test "parse tag graphQL output" {
|
||||
const result =
|
||||
\\{"data":{"repository":{"refs":{"pageInfo":{"hasNextPage":false,"endCursor":"MzY"},"nodes":[{"name":"v0.7.9","target":{"committedDate":"2025-07-16T06:14:23Z","message":"chore: bump version to v0.7.9"}},{"name":"v0.7.8","target":{"committedDate":"2025-07-15T23:01:11Z","message":"chore: bump version to v0.7.8"}},{"name":"v0.7.7","target":{"committedDate":"2025-04-16T02:32:43Z","message":"chore: bump version to v0.7.0"}},{"name":"v0.7.6","target":{"committedDate":"2025-04-13T18:00:14Z","message":"chore: bump version to v0.7.6"}},{"name":"v0.7.5","target":{"committedDate":"2025-04-12T20:31:13Z","message":"chore: bump version to v0.7.5"}},{"name":"v0.7.4","target":{"committedDate":"2025-04-06T02:08:45Z","message":"chore: bump version to v0.7.4"}},{"name":"v0.3.6","target":{"committedDate":"2024-12-20T07:25:36Z","message":"chore: bump version to v3.4.6"}},{"name":"v0.1.0","target":{"committedDate":"2024-11-16T23:19:14Z","message":"chore: bump version to v0.1.0"}}]}}}}
|
||||
|
|
|
@ -4,6 +4,7 @@ const json = std.json;
|
|||
const Allocator = std.mem.Allocator;
|
||||
const ArrayList = std.ArrayList;
|
||||
const utils = @import("../utils.zig");
|
||||
const tag_filter = @import("../tag_filter.zig");
|
||||
|
||||
const Release = @import("../main.zig").Release;
|
||||
const Provider = @import("../Provider.zig");
|
||||
|
@ -220,9 +221,16 @@ fn getProjectReleases(allocator: Allocator, client: *http.Client, token: []const
|
|||
const desc_value = obj.get("description") orelse json.Value{ .string = "" };
|
||||
const desc_str = if (desc_value == .string) desc_value.string else "";
|
||||
|
||||
const tag_name = obj.get("tag_name").?.string;
|
||||
|
||||
// Skip problematic tags
|
||||
if (tag_filter.shouldSkipTag(allocator, tag_name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const release = Release{
|
||||
.repo_name = try allocator.dupe(u8, obj.get("name").?.string),
|
||||
.tag_name = try allocator.dupe(u8, obj.get("tag_name").?.string),
|
||||
.tag_name = try allocator.dupe(u8, tag_name),
|
||||
.published_at = try utils.parseReleaseTimestamp(obj.get("created_at").?.string),
|
||||
.html_url = try allocator.dupe(u8, obj.get("_links").?.object.get("self").?.string),
|
||||
.description = try allocator.dupe(u8, desc_str),
|
||||
|
@ -347,3 +355,26 @@ test "gitlab release parsing with live data snapshot" {
|
|||
);
|
||||
try std.testing.expectEqualStrings("gitlab", releases.items[0].provider);
|
||||
}
|
||||
|
||||
test "GitLab tag filtering" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
// Test that GitLab now uses the same filtering as other providers
|
||||
const problematic_tags = [_][]const u8{
|
||||
"nightly", "prerelease", "latest", "edge", "canary", "dev-branch",
|
||||
};
|
||||
|
||||
for (problematic_tags) |tag| {
|
||||
try std.testing.expect(tag_filter.shouldSkipTag(allocator, tag));
|
||||
}
|
||||
|
||||
// Test that valid tags are not filtered
|
||||
const valid_tags = [_][]const u8{
|
||||
"v1.0.0", "v2.1.3-stable",
|
||||
// Note: v1.0.0-alpha.1 is now filtered to avoid duplicates
|
||||
};
|
||||
|
||||
for (valid_tags) |tag| {
|
||||
try std.testing.expect(!tag_filter.shouldSkipTag(allocator, tag));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ const json = std.json;
|
|||
const Allocator = std.mem.Allocator;
|
||||
const ArrayList = std.ArrayList;
|
||||
const utils = @import("../utils.zig");
|
||||
const tag_filter = @import("../tag_filter.zig");
|
||||
|
||||
const Release = @import("../main.zig").Release;
|
||||
const Provider = @import("../Provider.zig");
|
||||
|
@ -128,6 +129,11 @@ fn fetchReleasesMultiRepo(allocator: Allocator, client: *http.Client, token: []c
|
|||
else
|
||||
"1970-01-01T00:00:00Z";
|
||||
|
||||
// Skip problematic tags
|
||||
if (tag_filter.shouldSkipTag(allocator, tag_data.tag_name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Investigate annotated tags as the description here
|
||||
const release = Release{
|
||||
.repo_name = try std.fmt.allocPrint(allocator, "~{s}/{s}", .{ tag_data.username, tag_data.reponame }),
|
||||
|
@ -600,3 +606,26 @@ test "sourcehut provider" {
|
|||
|
||||
try std.testing.expectEqualStrings("sourcehut", sourcehut_provider.getName());
|
||||
}
|
||||
|
||||
test "SourceHut tag filtering" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
// Test that SourceHut now uses the same filtering as other providers
|
||||
const problematic_tags = [_][]const u8{
|
||||
"nightly", "prerelease", "latest", "edge", "canary", "dev-branch",
|
||||
};
|
||||
|
||||
for (problematic_tags) |tag| {
|
||||
try std.testing.expect(tag_filter.shouldSkipTag(allocator, tag));
|
||||
}
|
||||
|
||||
// Test that valid tags are not filtered
|
||||
const valid_tags = [_][]const u8{
|
||||
"v1.0.0", "v2.1.3-stable",
|
||||
// Note: v1.0.0-alpha.1 is now filtered to avoid duplicates
|
||||
};
|
||||
|
||||
for (valid_tags) |tag| {
|
||||
try std.testing.expect(!tag_filter.shouldSkipTag(allocator, tag));
|
||||
}
|
||||
}
|
||||
|
|
302
src/tag_filter.zig
Normal file
302
src/tag_filter.zig
Normal file
|
@ -0,0 +1,302 @@
|
|||
const std = @import("std");
|
||||
|
||||
/// Common tag filtering logic that can be used across all providers
|
||||
pub fn shouldSkipTag(allocator: std.mem.Allocator, tag_name: []const u8) bool {
|
||||
// Check if tag name contains common moving patterns
|
||||
const tag_lower = std.ascii.allocLowerString(allocator, tag_name) catch return false;
|
||||
defer allocator.free(tag_lower);
|
||||
|
||||
// First check if this looks like semantic versioning (v1.2.3-something or 1.2.3-something)
|
||||
// If it does, we should be more careful about filtering
|
||||
const is_semantic_versioning = isSemVer(tag_lower);
|
||||
|
||||
// List of common moving tags that should be filtered out
|
||||
const moving_tags = [_][]const u8{
|
||||
// common "latest commit tags"
|
||||
"latest",
|
||||
"tip",
|
||||
"continuous",
|
||||
"head",
|
||||
|
||||
// common branch tags
|
||||
"main",
|
||||
"master",
|
||||
"trunk",
|
||||
"develop",
|
||||
"development",
|
||||
"dev",
|
||||
|
||||
// common fast moving channel names
|
||||
"nightly",
|
||||
"edge",
|
||||
"canary",
|
||||
|
||||
// common slower channels, but without version information
|
||||
// they probably are not something we're interested in
|
||||
"release",
|
||||
"snapshot",
|
||||
"unstable",
|
||||
"experimental",
|
||||
"prerelease",
|
||||
"preview",
|
||||
};
|
||||
|
||||
// Check for exact matches with moving tags
|
||||
for (moving_tags) |moving_tag|
|
||||
if (std.mem.eql(u8, tag_lower, moving_tag))
|
||||
return true;
|
||||
|
||||
// Only filter standalone alpha/beta/rc if they're NOT part of semantic versioning
|
||||
if (!is_semantic_versioning) {
|
||||
const standalone_prerelease_tags = [_][]const u8{
|
||||
"alpha",
|
||||
"beta",
|
||||
"rc",
|
||||
};
|
||||
|
||||
for (standalone_prerelease_tags) |tag|
|
||||
if (std.mem.eql(u8, tag_lower, tag))
|
||||
return true;
|
||||
} else {
|
||||
// For semantic versioning, be more conservative and filter out prerelease versions
|
||||
// since these are likely to be duplicates of releases that are already filtered
|
||||
// by the releases API prerelease flag
|
||||
if (containsPrereleaseIdentifier(tag_lower)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip pre-release and development tags
|
||||
if (std.mem.startsWith(u8, tag_lower, "pre-") or
|
||||
std.mem.startsWith(u8, tag_lower, "dev-") or
|
||||
std.mem.startsWith(u8, tag_lower, "test-") or
|
||||
std.mem.startsWith(u8, tag_lower, "debug-"))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Check if a tag looks like semantic versioning
|
||||
fn isSemVer(tag_lower: []const u8) bool {
|
||||
// Look for patterns like:
|
||||
// v1.2.3, v1.2.3-alpha.1, 1.2.3, 1.2.3-beta.2, etc.
|
||||
|
||||
var start_idx: usize = 0;
|
||||
|
||||
// Skip optional 'v' prefix
|
||||
if (tag_lower.len > 0 and tag_lower[0] == 'v') {
|
||||
start_idx = 1;
|
||||
}
|
||||
|
||||
if (start_idx >= tag_lower.len) return false;
|
||||
|
||||
// Look for pattern: number.number.number
|
||||
var dot_count: u8 = 0;
|
||||
var has_digit = false;
|
||||
|
||||
for (tag_lower[start_idx..]) |c| {
|
||||
if (c >= '0' and c <= '9') {
|
||||
has_digit = true;
|
||||
} else if (c == '.') {
|
||||
if (!has_digit) return false; // dot without preceding digit
|
||||
dot_count += 1;
|
||||
has_digit = false;
|
||||
if (dot_count > 2) break; // we only care about major.minor.patch
|
||||
} else if (c == '-' or c == '+') {
|
||||
// This could be prerelease or build metadata
|
||||
break;
|
||||
} else {
|
||||
// Invalid character for semver
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Must have at least 2 dots (major.minor.patch) and end with a digit
|
||||
return dot_count >= 2 and has_digit;
|
||||
}
|
||||
|
||||
/// Check if a semantic version contains prerelease identifiers
|
||||
fn containsPrereleaseIdentifier(tag_lower: []const u8) bool {
|
||||
// Look for common prerelease identifiers in semantic versioning
|
||||
const prerelease_identifiers = [_][]const u8{
|
||||
"-alpha",
|
||||
"-beta",
|
||||
"-rc",
|
||||
"-pre",
|
||||
};
|
||||
|
||||
for (prerelease_identifiers) |identifier| {
|
||||
if (std.mem.indexOf(u8, tag_lower, identifier) != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Note: We don't filter git-style version tags like v1.2.3-123-g1234567
|
||||
// These are development versions but may be useful to track
|
||||
// (e.g., kraftkit releases that should be included per user request)
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Check if a release should be filtered based on prerelease/draft status
|
||||
pub fn shouldSkipRelease(is_prerelease: bool, is_draft: bool) bool {
|
||||
return is_prerelease or is_draft;
|
||||
}
|
||||
|
||||
test "shouldSkipTag filters common moving tags" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
// Test exact matches for moving tags
|
||||
const moving_tags = [_][]const u8{
|
||||
"latest",
|
||||
"tip",
|
||||
"continuous",
|
||||
"head",
|
||||
"main",
|
||||
"master",
|
||||
"trunk",
|
||||
"develop",
|
||||
"development",
|
||||
"dev",
|
||||
"nightly",
|
||||
"edge",
|
||||
"canary",
|
||||
"release",
|
||||
"snapshot",
|
||||
"unstable",
|
||||
"experimental",
|
||||
"prerelease",
|
||||
"preview",
|
||||
};
|
||||
|
||||
for (moving_tags) |tag| {
|
||||
const should_skip = shouldSkipTag(allocator, tag);
|
||||
try std.testing.expect(should_skip);
|
||||
|
||||
// Test case insensitive matching
|
||||
const upper_tag = try std.ascii.allocUpperString(allocator, tag);
|
||||
defer allocator.free(upper_tag);
|
||||
const should_skip_upper = shouldSkipTag(allocator, upper_tag);
|
||||
try std.testing.expect(should_skip_upper);
|
||||
}
|
||||
|
||||
// Test standalone alpha/beta/rc (should be filtered)
|
||||
const standalone_prerelease = [_][]const u8{ "alpha", "beta", "rc" };
|
||||
for (standalone_prerelease) |tag| {
|
||||
try std.testing.expect(shouldSkipTag(allocator, tag));
|
||||
}
|
||||
}
|
||||
|
||||
test "shouldSkipTag filters prefix patterns" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const prefix_patterns = [_][]const u8{
|
||||
"pre-release",
|
||||
"pre-1.0.0",
|
||||
"dev-branch",
|
||||
"dev-feature",
|
||||
"test-build",
|
||||
"test-123",
|
||||
"debug-version",
|
||||
"debug-info",
|
||||
};
|
||||
|
||||
for (prefix_patterns) |tag| {
|
||||
const should_skip = shouldSkipTag(allocator, tag);
|
||||
try std.testing.expect(should_skip);
|
||||
}
|
||||
}
|
||||
|
||||
test "shouldSkipTag allows valid version tags" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const valid_tags = [_][]const u8{
|
||||
"v1.0.0",
|
||||
"v2.1.3",
|
||||
"1.0.0",
|
||||
"2.1.3-stable",
|
||||
"v1.0.0-final",
|
||||
"release-1.0.0",
|
||||
"stable-v1.0.0",
|
||||
"v1.0.0-lts",
|
||||
"2023.1.0",
|
||||
// Note: Semantic versioning prerelease tags are now filtered to avoid duplicates
|
||||
// with the releases API, so they're not in this "valid" list anymore
|
||||
};
|
||||
|
||||
for (valid_tags) |tag| {
|
||||
const should_skip = shouldSkipTag(allocator, tag);
|
||||
try std.testing.expect(!should_skip);
|
||||
}
|
||||
}
|
||||
|
||||
test "shouldSkipRelease filters prerelease and draft" {
|
||||
// Test prerelease filtering
|
||||
try std.testing.expect(shouldSkipRelease(true, false));
|
||||
|
||||
// Test draft filtering
|
||||
try std.testing.expect(shouldSkipRelease(false, true));
|
||||
|
||||
// Test both prerelease and draft
|
||||
try std.testing.expect(shouldSkipRelease(true, true));
|
||||
|
||||
// Test normal release
|
||||
try std.testing.expect(!shouldSkipRelease(false, false));
|
||||
}
|
||||
|
||||
test "semantic versioning detection" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
// Test that semantic versioning tags with alpha/beta/rc are now filtered
|
||||
// (to avoid duplicates with releases API)
|
||||
const semver_prerelease_tags = [_][]const u8{
|
||||
"v1.0.0-alpha.1",
|
||||
"v1.0.0-beta.2",
|
||||
"v1.0.0-rc.1",
|
||||
"1.0.0-alpha.1",
|
||||
"2.0.0-beta.1",
|
||||
"3.0.0-rc.1",
|
||||
"v5.5.0-rc1",
|
||||
"v0.5.0-alpha01",
|
||||
"v1.12.0-beta3",
|
||||
"v1.24.0-rc0",
|
||||
};
|
||||
|
||||
for (semver_prerelease_tags) |tag| {
|
||||
const should_skip = shouldSkipTag(allocator, tag);
|
||||
try std.testing.expect(should_skip); // Now these should be filtered
|
||||
}
|
||||
|
||||
// Test that git-style version tags are preserved (per user request for kraftkit)
|
||||
const git_style_tags = [_][]const u8{
|
||||
"v0.11.6-212-g74599361",
|
||||
"v1.2.3-45-g1234567",
|
||||
"v2.0.0-123-gabcdef0",
|
||||
};
|
||||
|
||||
for (git_style_tags) |tag| {
|
||||
const should_skip = shouldSkipTag(allocator, tag);
|
||||
try std.testing.expect(!should_skip); // These should NOT be filtered
|
||||
}
|
||||
|
||||
// Test that regular semantic versioning tags are preserved
|
||||
const regular_semver_tags = [_][]const u8{
|
||||
"v1.0.0",
|
||||
"v2.1.3",
|
||||
"1.0.0",
|
||||
"2.1.3",
|
||||
"v10.20.30",
|
||||
};
|
||||
|
||||
for (regular_semver_tags) |tag| {
|
||||
const should_skip = shouldSkipTag(allocator, tag);
|
||||
try std.testing.expect(!should_skip);
|
||||
}
|
||||
|
||||
// Test that standalone alpha/beta/rc are still filtered
|
||||
const standalone_tags = [_][]const u8{ "alpha", "beta", "rc", "ALPHA", "Beta", "RC" };
|
||||
for (standalone_tags) |tag| {
|
||||
const should_skip = shouldSkipTag(allocator, tag);
|
||||
try std.testing.expect(should_skip);
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue