mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-18 19:30:27 +08:00
Add list_canonical_tags command
- Load the release modal's tags with it - Sort them with the shared sortedTags helper - Test that peers' tags are left out
This commit is contained in:
committed by
Rūdolfs Ošiņš
parent
6f57b811ef
commit
040906ba6d
@@ -1,3 +1,5 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use radicle::git;
|
||||
use radicle::identity::RepoId;
|
||||
use radicle::node::NodeId;
|
||||
@@ -37,6 +39,14 @@ pub async fn list_repo_refs(
|
||||
blocking(ctx, move |ctx| ctx.list_repo_refs(rid)).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_canonical_tags(
|
||||
ctx: tauri::State<'_, AppState>,
|
||||
rid: RepoId,
|
||||
) -> Result<BTreeMap<String, types::repo::Tag>, Error> {
|
||||
blocking(ctx, move |ctx| ctx.list_canonical_tags(rid)).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn repo_by_id(
|
||||
ctx: tauri::State<'_, AppState>,
|
||||
|
||||
@@ -122,6 +122,7 @@ pub fn run() {
|
||||
repo::clean,
|
||||
repo::diff_stats,
|
||||
repo::list_commits,
|
||||
repo::list_canonical_tags,
|
||||
repo::list_repo_commits,
|
||||
repo::list_repo_refs,
|
||||
repo::list_repos,
|
||||
|
||||
@@ -655,6 +655,17 @@ pub trait Repo: Profile {
|
||||
Ok(repo::RepoRefs { canonical, remotes })
|
||||
}
|
||||
|
||||
/// The canonical tags, without the per-peer refs that `list_repo_refs`
|
||||
/// loads and verifies for every remote.
|
||||
fn list_canonical_tags(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
) -> Result<BTreeMap<String, repo::Tag>, Error> {
|
||||
let repo = self.profile().storage.repository(rid)?;
|
||||
|
||||
Ok(canonical_refs(&repo)?.tags)
|
||||
}
|
||||
|
||||
fn repo_by_id(&self, rid: identity::RepoId) -> Result<repo::RepoInfo, Error> {
|
||||
let profile = self.profile();
|
||||
let repo = profile.storage.repository(rid)?;
|
||||
@@ -1309,6 +1320,59 @@ mod test {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_tags_ignore_peer_tags() {
|
||||
use radicle::cob::Title;
|
||||
use radicle::identity::Identity;
|
||||
use radicle::identity::doc::PayloadId;
|
||||
use radicle::storage::{SignRepository, WriteRepository, WriteStorage};
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let profile = test::profile(&tmp.path().join("home"), [0xff; 32]);
|
||||
let signer = SigningKey::from_seed(Seed::new([0xff; 32]));
|
||||
let (rid, _, _, head) =
|
||||
fixtures::project(tmp.path().join("working"), &profile.storage, &signer).unwrap();
|
||||
|
||||
{
|
||||
let repo = profile.storage.repository_mut(rid).unwrap();
|
||||
let mut identity = Identity::load_mut(&repo, &signer).unwrap();
|
||||
let mut doc = identity.doc().clone().edit();
|
||||
doc.payload.insert(
|
||||
PayloadId::canonical_refs(),
|
||||
serde_json::json!({
|
||||
"rules": {
|
||||
"refs/tags/*": { "allow": "delegates", "threshold": 1 },
|
||||
},
|
||||
})
|
||||
.into(),
|
||||
);
|
||||
identity
|
||||
.update(
|
||||
Title::new("Make tags canonical").unwrap(),
|
||||
"",
|
||||
&doc.verified().unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
repo.sign_refs(&signer).unwrap();
|
||||
repo.set_identity_head().unwrap();
|
||||
|
||||
let other = repo.identity_head().unwrap().into();
|
||||
let peer = "z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7";
|
||||
let peer_tags = format!("refs/namespaces/{peer}/refs/tags");
|
||||
for (name, oid) in [
|
||||
("refs/tags/v1".to_owned(), head),
|
||||
(format!("{peer_tags}/v1"), other),
|
||||
(format!("{peer_tags}/v2"), head),
|
||||
] {
|
||||
repo.backend.reference(&name, oid, false, "test").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let tags = AppState { profile }.list_canonical_tags(rid).unwrap();
|
||||
assert_eq!(tags.keys().collect::<Vec<_>>(), vec!["v1"]);
|
||||
assert_eq!(tags["v1"].oid, head);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repo_split_tree_path() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -193,6 +193,7 @@ pub fn router(shared: Shared) -> Router {
|
||||
)
|
||||
.route("/repo_by_id", post(repo_handler))
|
||||
.route("/list_repo_refs", post(list_repo_refs_handler))
|
||||
.route("/list_canonical_tags", post(list_canonical_tags_handler))
|
||||
.route("/identity_by_repo", post(identity_handler))
|
||||
.route("/version", post(version_handler))
|
||||
.route("/git_info", post(git_info_handler))
|
||||
@@ -468,6 +469,15 @@ async fn list_repo_refs_handler(
|
||||
Ok::<_, Error>(Json(refs))
|
||||
}
|
||||
|
||||
async fn list_canonical_tags_handler(
|
||||
Ctx(ctx): Ctx,
|
||||
Json(RepoBody { rid }): Json<RepoBody>,
|
||||
) -> impl IntoResponse {
|
||||
let tags = ctx.list_canonical_tags(rid)?;
|
||||
|
||||
Ok::<_, Error>(Json(tags))
|
||||
}
|
||||
|
||||
async fn git_info_handler() -> impl IntoResponse {
|
||||
Ok::<_, Error>(Json(types::binaries::git_info()))
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import type { Location } from "@bindings/cob/release/Location";
|
||||
import type { ReleaseCounts } from "@bindings/cob/release/ReleaseCounts";
|
||||
import type { ReleaseScope } from "@bindings/cob/release/ReleaseScope";
|
||||
import type { Commit } from "@bindings/repo/Commit";
|
||||
import type { RepoRefs } from "@bindings/repo/RepoRefs";
|
||||
import type { Tag } from "@bindings/repo/Tag";
|
||||
|
||||
// With no scope asked for and no delegate releases, open on the untrusted
|
||||
// scope rather than on an empty list.
|
||||
@@ -154,14 +152,6 @@ export function matchCommits(
|
||||
return matches;
|
||||
}
|
||||
|
||||
// Only canonical tags: a peer's tag of the same name could point a release
|
||||
// at a commit the delegates never tagged.
|
||||
export function canonicalTags(refs: RepoRefs): { name: string; tag: Tag }[] {
|
||||
return Object.entries(refs.canonical.tags)
|
||||
.map(([name, tag]) => ({ name, tag }))
|
||||
.sort((a, b) => b.tag.timestamp - a.tag.timestamp);
|
||||
}
|
||||
|
||||
// A release by a non-delegate is untrusted as a whole. On a delegate's
|
||||
// release, only the artifacts registered by others are.
|
||||
export function releaseWarning(
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import type { ArtifactDigest } from "@bindings/cob/release/ArtifactDigest";
|
||||
import type { Commit } from "@bindings/repo/Commit";
|
||||
import type { RepoInfo } from "@bindings/repo/RepoInfo";
|
||||
import type { RepoRefs } from "@bindings/repo/RepoRefs";
|
||||
import type { Tag } from "@bindings/repo/Tag";
|
||||
|
||||
import { untrack } from "svelte";
|
||||
@@ -14,7 +13,8 @@
|
||||
import { invoke, InvokeError } from "@app/lib/invoke";
|
||||
import { disableHide, enableHide, forceHide } from "@app/lib/modal";
|
||||
import { isOid } from "@app/lib/radUri";
|
||||
import { canonicalTags, matchCommits } from "@app/lib/releases";
|
||||
import { sortedTags } from "@app/lib/refs";
|
||||
import { matchCommits } from "@app/lib/releases";
|
||||
import * as router from "@app/lib/router";
|
||||
import { duplicatePicks } from "@app/lib/stageArtifacts";
|
||||
import { formatBytes, formatOid } from "@app/lib/utils";
|
||||
@@ -102,10 +102,12 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Only canonical tags: a peer's tag of the same name could point a release
|
||||
// at a commit the delegates never tagged.
|
||||
$effect(() => {
|
||||
void invoke<RepoRefs>("list_repo_refs", { rid: repo.rid })
|
||||
.then(refs => {
|
||||
tags = canonicalTags(refs);
|
||||
void invoke<Record<string, Tag>>("list_canonical_tags", { rid: repo.rid })
|
||||
.then(result => {
|
||||
tags = sortedTags(result).map(([name, tag]) => ({ name, tag }));
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
console.error("Could not list tags:", error);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { Artifact } from "@bindings/cob/release/Artifact";
|
||||
import type { ReleaseCounts } from "@bindings/cob/release/ReleaseCounts";
|
||||
import type { Commit } from "@bindings/repo/Commit";
|
||||
import type { RepoRefs } from "@bindings/repo/RepoRefs";
|
||||
import type { Tag } from "@bindings/repo/Tag";
|
||||
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
@@ -11,7 +9,6 @@ import {
|
||||
attestedBy,
|
||||
canAttest,
|
||||
canEditMetadata,
|
||||
canonicalTags,
|
||||
delegatesFirst,
|
||||
displayMetadataValue,
|
||||
locationsByNode,
|
||||
@@ -270,45 +267,6 @@ describe("matchCommits", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("canonicalTags", () => {
|
||||
function tag(timestamp: number, tagOid?: string): Tag {
|
||||
return { oid: "c", timestamp, tagOid };
|
||||
}
|
||||
function refs(
|
||||
canonical: Record<string, Tag>,
|
||||
...remotes: Record<string, Tag>[]
|
||||
): RepoRefs {
|
||||
return {
|
||||
canonical: { branches: {}, tags: canonical },
|
||||
remotes: remotes.map((tags, i) => ({
|
||||
id: `${i}`,
|
||||
delegate: false,
|
||||
branches: {},
|
||||
tags,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
test("lists canonical tags, newest first", () => {
|
||||
expect(
|
||||
canonicalTags(refs({ v1: tag(1), v2: tag(2) })).map(t => t.name),
|
||||
).toEqual(["v2", "v1"]);
|
||||
});
|
||||
|
||||
test("ignores tags only a peer has", () => {
|
||||
expect(canonicalTags(refs({ v1: tag(1) }, { v2: tag(2) }))).toEqual([
|
||||
{ name: "v1", tag: tag(1) },
|
||||
]);
|
||||
});
|
||||
|
||||
test("keeps the canonical tag over a peer's annotated one", () => {
|
||||
const canonical = tag(1);
|
||||
expect(
|
||||
canonicalTags(refs({ v1: canonical }, { v1: tag(2, "evil") }))[0].tag,
|
||||
).toBe(canonical);
|
||||
});
|
||||
});
|
||||
|
||||
describe("releaseWarning", () => {
|
||||
test("warns about a whole release by a non-delegate", () => {
|
||||
expect(releaseWarning(them, delegates, "trusted")).toBe("release");
|
||||
|
||||
Reference in New Issue
Block a user