flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/apps/api/src/renamed.rs

119 lines3,989 bytesCodeBlame
1//! Old addresses: a workspace's slug after a rename, and a repository's
2//! path after a transfer.
3//!
4//! Old workspace slugs, after a rename. An operation that names a
5//! repository (`owner/name`) or a workspace by a slug the workspace has
6//! since been renamed from is run again under the slug it has now, so
7//! scripts and agents written against the old address keep working while
8//! it redirects.
9
10use g1t_contracts::identity::SlugArgs;
11use serde_json::Value;
12use worker::Result;
13
14use crate::operations::Services;
15
16/// The workspace slug an operation's input names: the owner in `repo`, or
17/// `workspace`.
18pub fn named_slug(input: &Value) -> Option<&str> {
19 input["repo"]
20 .as_str()
21 .and_then(|repo| repo.split_once('/'))
22 .map(|(owner, _)| owner)
23 .or_else(|| input["workspace"].as_str())
24 .filter(|slug| !slug.is_empty())
25}
26
27/// `input` with `old` replaced by `new` wherever it names the workspace.
28pub fn rewrite(input: &Value, old: &str, new: &str) -> Value {
29 let mut input = input.clone();
30 if let Some(repo) = input["repo"].as_str()
31 && let Some((owner, name)) = repo.split_once('/')
32 && owner.eq_ignore_ascii_case(old)
33 {
34 input["repo"] = Value::String(format!("{new}/{name}"));
35 }
36 if input["workspace"]
37 .as_str()
38 .is_some_and(|workspace| workspace.eq_ignore_ascii_case(old))
39 {
40 input["workspace"] = Value::String(new.to_owned());
41 }
42 input
43}
44
45/// The input under the workspace's current slug, if it names an old one.
46pub async fn retarget(services: &Services, input: &Value) -> Result<Option<Value>> {
47 let Some(old) = named_slug(input) else {
48 return Ok(None);
49 };
50 let current: Option<String> = g1t_kit::call(
51 &services.identity,
52 "resolve_slug",
53 &SlugArgs {
54 slug: old.to_owned(),
55 },
56 )
57 .await?;
58 Ok(current.map(|new| rewrite(input, old, &new)))
59}
60
61/// The input under a transferred repository's path now, if `repo` names
62/// the path it left.
63pub async fn transferred(services: &Services, input: &Value) -> Result<Option<Value>> {
64 let Some((namespace, name)) = input["repo"].as_str().and_then(|repo| repo.split_once('/')) else {
65 return Ok(None);
66 };
67 let now: Option<g1t_contracts::repos::RepoPath> = g1t_kit::call(
68 &services.repos,
69 "resolve_path",
70 &g1t_contracts::repos::ResolvePathArgs {
71 path: g1t_contracts::repos::RepoPath {
72 namespace: namespace.to_owned(),
73 name: name.to_owned(),
74 },
75 },
76 )
77 .await?;
78 Ok(now.map(|path| moved_to(input, &format!("{}/{}", path.namespace, path.name))))
79}
80
81/// `input` with `repo` replaced by `path`.
82pub fn moved_to(input: &Value, path: &str) -> Value {
83 let mut input = input.clone();
84 input["repo"] = Value::String(path.to_owned());
85 input
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use serde_json::json;
92
93 #[test]
94 fn finds_the_slug_an_operation_names() {
95 assert_eq!(named_slug(&json!({ "repo": "acme/rocket" })), Some("acme"));
96 assert_eq!(named_slug(&json!({ "workspace": "acme" })), Some("acme"));
97 assert_eq!(named_slug(&json!({ "id": "x" })), None);
98 }
99
100 #[test]
101 fn a_transferred_repository_is_named_by_its_new_path() {
102 let input = json!({ "repo": "syntaqx/g1t", "number": 4 });
103 assert_eq!(
104 moved_to(&input, "flagon-io/g1t"),
105 json!({ "repo": "flagon-io/g1t", "number": 4 })
106 );
107 }
108
109 #[test]
110 fn rewrites_only_the_old_slug() {
111 let input = json!({ "repo": "Acme/rocket", "workspace": "acme", "title": "acme/x" });
112 assert_eq!(
113 rewrite(&input, "acme", "acme-inc"),
114 json!({ "repo": "acme-inc/rocket", "workspace": "acme-inc", "title": "acme/x" })
115 );
116 let other = json!({ "repo": "globex/rocket" });
117 assert_eq!(rewrite(&other, "acme", "acme-inc"), other);
118 }
119}