Commit

Author:

Hash:

Timestamp:

+38 -6 +/-3 browse

Kevin Schoon [me@kevinschoon.com]

c6b20ec7fe389afc3392ed633358131123bb9c0e

Mon, 01 Dec 2025 23:14:00 +0000 (1 month ago)

update repository query per forge-feed spec further
1diff --git a/ayllu/src/license.rs b/ayllu/src/license.rs
2index 34a6444..31e6736 100644
3--- a/ayllu/src/license.rs
4+++ b/ayllu/src/license.rs
5 @@ -14,6 +14,7 @@ static LICENSE_PATHS: &[&str] = &["LICENSE", "COPYING", "LICENSE.md"];
6
7 // TODO: super naive license detection
8 // see https://github.com/OpenSourceOrg/licenses
9+ // TODO: https://crates.io/crates/spdx/0.12.0
10 pub fn detect(repository: &Wrapper, commit: Option<String>) -> Result<Option<String>, Error> {
11 for path in LICENSE_PATHS {
12 let path = Path::new(path);
13 diff --git a/ayllu/src/web2/routes/finger.rs b/ayllu/src/web2/routes/finger.rs
14index 56317d0..bcd29b6 100644
15--- a/ayllu/src/web2/routes/finger.rs
16+++ b/ayllu/src/web2/routes/finger.rs
17 @@ -8,23 +8,26 @@ use webfinger_rs::{Link, WebFingerRequest, WebFingerResponse};
18
19 use crate::{
20 config::{Collection, Config},
21+ license,
22 web2::error::Error,
23 };
24 use ayllu_git::Scanner;
25
26 const FORGE_FEED_REL_AVATAR: &str = "http://forge-feed.org/rel/avatar";
27- // const FORGE_FEED_REL_TICKETING: &str = "http://forge-feed.org/rel/ticketing-system";
28+ const FORGE_FEED_REL_TICKETING: &str = "http://forge-feed.org/rel/ticketing-system";
29 const FORGE_FEED_REL_REPOSITORY: &str = "http://forge-feed.org/rel/repository";
30 // const FORGE_FEED_REL_REPOSITORY_URI: &str = "http://forge-feed.org/rel/repository-uri";
31 const FORGE_FEED_REL_MAILING_LIST: &str = "http://forge-feed.org/rel/mailing-list";
32 const FORGE_FEED_REL_PROJECT: &str = "http://forge-feed.org/rel/project";
33 const FORGE_FEED_REL_HOMEPAGE: &str = "http://forge-feed.org/rel/homepage";
34 const FORGE_FEED_REL_DESCRIPTION: &str = "http://forge-feed.org/rel/description";
35- // const FORGE_FEED_REL_LICENSE: &str = "http://forge-feed.org/rel/license";
36+ const FORGE_FEED_REL_LICENSE: &str = "http://forge-feed.org/rel/license";
37 const FORGE_FEED_REL_VCS_CLONE_LINK: &str = "http://forge-feed.org/rel/clone";
38- // const FORGE_FEED_REL_LABEL: &str = "http://forge-feed.org/rel/label";
39- // const FORGE_FEED_NS_LABEL: &str = "http://forge-feed.org/ns/label";
40+ // const FORGE_FEED_REL_CHAT_LINK: &str = "http://forge-feed.org/rel/chatroom";
41+ const FORGE_FEED_REL_LABEL: &str = "http://forge-feed.org/rel/label";
42+ const FORGE_FEED_NS_LABEL: &str = "http://forge-feed.org/ns/label";
43 const FORGE_FEED_NS_VCS_TYPE: &str = "http://forge-feed.org/ns/vcs-type";
44+ const FORGE_FEED_NS_SPDX_IDENTIFIER: &str = "http://feed-forge.org/ns/spdx-identifier";
45
46 const SCHEME_ACCT: &str = "acct";
47 const SCHEME_PROJECT: &str = "project";
48 @@ -78,7 +81,6 @@ impl Resolver {
49 let collection_name = resource.host().ok_or(Error::Message(format!(
50 "Missing collection name: {resource:?}"
51 )))?;
52- println!("Path: {}", resource.path());
53 if resource.path() != "/" {
54 return Err(Error::Message(format!(
55 "Collection format is project://<name>, got: {resource}"
56 @@ -108,7 +110,6 @@ impl Resolver {
57 let collection_name = resource.host().ok_or(Error::Message(format!(
58 "Missing collection name: {resource:?}"
59 )))?;
60-
61 let repository_name = resource.path().trim_start_matches("/");
62 if repository_name.is_empty() {
63 return Err(Error::Message(format!(
64 @@ -204,6 +205,7 @@ impl Resolver {
65 Resource::Repository((collection, repo_path)) => {
66 let collection_link = self.origin.join(&collection.name).unwrap();
67 let repository = ayllu_git::Wrapper::new(repo_path.as_path())?;
68+ let spdx_identifier = license::detect(&repository, None)?;
69 let config = repository.config()?;
70 let mut http_clone_link = self.origin.clone();
71 http_clone_link.set_path(&repository.name());
72 @@ -225,6 +227,22 @@ impl Resolver {
73 )]))
74 .build(),
75 ];
76+ if let Some(spdx_identifier) = spdx_identifier {
77+ links.push(
78+ Link::builder(FORGE_FEED_REL_LICENSE)
79+ .properties(HashMap::from_iter(vec![(
80+ FORGE_FEED_NS_SPDX_IDENTIFIER.to_string(),
81+ Some(spdx_identifier),
82+ )]))
83+ .build(),
84+ )
85+ }
86+ if let Some(ticketing_systems) = config.ticketing_systems {
87+ ticketing_systems.iter().for_each(|system| {
88+ let item = Link::builder(FORGE_FEED_REL_TICKETING).href(system).build();
89+ links.push(item);
90+ });
91+ }
92 if let Some(mailing_lists) = config.mail {
93 mailing_lists.iter().for_each(|mailing_list| {
94 links.push(
95 @@ -234,6 +252,15 @@ impl Resolver {
96 );
97 });
98 }
99+ if let Some(labels) = config.labels {
100+ labels.iter().for_each(|label| {
101+ links.push(
102+ Link::builder(FORGE_FEED_REL_LABEL)
103+ .property(FORGE_FEED_NS_LABEL, label.to_string())
104+ .build(),
105+ );
106+ });
107+ }
108 Ok(WebFingerResponse {
109 subject: resource.to_string(),
110 aliases: None,
111 diff --git a/crates/git/src/config.rs b/crates/git/src/config.rs
112index 6376734..d2a5fdd 100644
113--- a/crates/git/src/config.rs
114+++ b/crates/git/src/config.rs
115 @@ -60,6 +60,8 @@ impl Remotes {
116 pub struct Config {
117 pub description: Option<String>,
118 pub homepage: Option<String>,
119+ pub labels: Option<Vec<String>>,
120+ pub ticketing_systems: Option<Vec<String>>,
121 pub chat: Option<Vec<ChatLink>>,
122 pub mail: Option<Vec<Email>>,
123 pub hidden: Option<bool>,
124 @@ -169,6 +171,8 @@ pub(crate) fn read(git_config: &GitConfig) -> Result<Config, Error> {
125 let is_mirror = remotes.has_mirror();
126 Ok(Config {
127 description: string(git_config, "ayllu.description"),
128+ ticketing_systems: strings(git_config, "ayllu.tickets").unwrap(),
129+ labels: strings(git_config, "ayllu.label").unwrap(),
130 homepage: string(git_config, "ayllu.homepage"),
131 default_branch: string(git_config, "ayllu.default-branch"),
132 chat,