Author:
Hash:
Timestamp:
+134 -58 +/-6 browse
Kevin Schoon [me@kevinschoon.com]
33cab614cdef0909208e1bf5144150eccab473b2
Thu, 04 Dec 2025 17:03:21 +0000 (3 days ago)
| 1 | diff --git a/content/project-atom-feed.md b/content/project-atom-feed.md |
| 2 | index 3816681..d1ffc3c 100644 |
| 3 | --- a/content/project-atom-feed.md |
| 4 | +++ b/content/project-atom-feed.md |
| 5 | @@ -4,17 +4,17 @@ title: Atom Feed |
| 6 | |
| 7 | # Atom Feeds |
| 8 | |
| 9 | - Your forge should expose an [Atom](https://www.rfc-editor.org/rfc/rfc4287) |
| 10 | + A forge SHOULD expose an [Atom](https://www.rfc-editor.org/rfc/rfc4287) |
| 11 | feed in order for other peers to subscribe to interesting code projects that |
| 12 | - are developed on your forge. The server should expose projects ordered by those |
| 13 | - which have been recently updated. THe heuristic you use to determine what has |
| 14 | - been updated depends on your forge. A common way to do this may be to simply |
| 15 | - look at your VCS's concept of a commit and order updates with that. |
| 16 | + are developed on your forge. The server SHOULD expose projects ordered by those |
| 17 | + which have been recently updated. The heuristic used to determine what has |
| 18 | + been updated depends on the implementor forge. A common way to do this may be |
| 19 | + to simply look at your VCS's concept of a commit and order updates with that. |
| 20 | |
| 21 | ## Determine if a Host Supports Forge Feed |
| 22 | |
| 23 | - In order to participate in ForgeFeed your forge MUST present an HTML link |
| 24 | - element such as below at the root domain of your forge. For example, |
| 25 | + In order to participate as an Atom enabled ForgeFeed the forge MUST present an |
| 26 | + HTML link element such as below at the root domain of your forge. For example, |
| 27 | `code.example.org` MUST have a link element present in it's html header: |
| 28 | |
| 29 | ```html |
| 30 | @@ -73,10 +73,11 @@ Below is an example Atom feed with an optional repository section (described bel |
| 31 | |
| 32 | ### ForgeFeed Extension |
| 33 | |
| 34 | - In order to facilitate discovery by external indexes it is highly recommended |
| 35 | - that your server implement the webfinger [project specification](/webfinger-project) |
| 36 | - so that repository indexes may populate their state with rich information |
| 37 | - about code repositories hosted on your server. |
| 38 | + In order to facilitate discovery by external indexes the server SHOULD implement |
| 39 | + implement the webfinger [project specification](/webfinger-project) as well as |
| 40 | + the [repository specification](/webfinger-repository) so that repository indexes |
| 41 | + may populate their state with rich information about code repositories hosted |
| 42 | + on your server. |
| 43 | |
| 44 | The extension currently supports only a single field called `project`. |
| 45 | |
| 46 | @@ -94,7 +95,7 @@ of the server which is providing the feed. |
| 47 | Forge-feed enabled Atom feeds have no support for sharing private projects |
| 48 | and any project that is not shared publicly on the internet must be hidden |
| 49 | from the Atom activity feed stream. If your forge provides the ability to change |
| 50 | - a project from public to private it must be understood that clients may |
| 51 | + a project from public to private it should be understood that clients may |
| 52 | already have cached versions of your project data. |
| 53 | |
| 54 | ### Recommendations for Enumerating Project Events |
| 55 | diff --git a/content/webfinger-project.md b/content/webfinger-project.md |
| 56 | index 2f7dc4c..6eba516 100644 |
| 57 | --- a/content/webfinger-project.md |
| 58 | +++ b/content/webfinger-project.md |
| 59 | @@ -2,18 +2,26 @@ |
| 60 | title: Project Discovery via WebFinger |
| 61 | --- |
| 62 | |
| 63 | - A project refers to a software project which may contain multiple repositories |
| 64 | - as well as other resources such as mailing lists, bug trackers, links to |
| 65 | - chatrooms, etc. NOTE that some forges do not make a distinction between a |
| 66 | - project and a repository. If your forge does not then you MAY choose to expose |
| 67 | - repositories as a project with a single repository resource link. |
| 68 | + A project refers to a peice of software which may be spread across multiple |
| 69 | + code [repositories](/webfinger-repository). For example a consider a fictious |
| 70 | + programming language which may be composed of several components such as a |
| 71 | + compiler, a standard library, example codebases, a marketing website, etc. |
| 72 | + When the architecture of such a project spans multiple repositories then it |
| 73 | + is likely they should be represented as a project. |
| 74 | + |
| 75 | + Sometimes however the scope of a project is much smaller such that it is |
| 76 | + entirely contained within one single repository, alternatively a large project |
| 77 | + might choose to version it's code all in one logical repository. If your forge |
| 78 | + supports it you MAY present a single repository as both an identifiable project |
| 79 | + and also a repository. |
| 80 | |
| 81 | # Project URI |
| 82 | |
| 83 | A project URI identifies a software project and optionally the host that is |
| 84 | - resides on. This value is similar to |
| 85 | - [RFC7565](https://datatracker.ietf.org/doc/html/rfc7565). The slug and hostname |
| 86 | - part MUST match the URI path specification as defined in |
| 87 | + resides on. The value serves to contain enough information such that a client |
| 88 | + with a simple URL parser may easily parse the identifier. A project URI MUST |
| 89 | + be a valid [RFC7565](https://datatracker.ietf.org/doc/html/rfc7565) URI. The |
| 90 | + slug and hostname parts MUST match the URI path specification as defined in |
| 91 | [RFC3986-3.3](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) while the |
| 92 | hostname, if specified, must match |
| 93 | [RFC3986-3.2.2](https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2). |
| 94 | @@ -26,7 +34,8 @@ hostname, if specified, must match |
| 95 | |
| 96 | ### Slug |
| 97 | |
| 98 | - The Slug represents a unique string that identifies a project at a particular code forge. |
| 99 | + The Slug represents a unique string that identifies a project at a particular |
| 100 | + code forge. |
| 101 | |
| 102 | ### Hostname |
| 103 | |
| 104 | @@ -38,25 +47,26 @@ https://example.org/.well-known/webfinger?resource=project://spartacus |
| 105 | https://example.org/.well-known/webfinger?resource=project://spartacus@example.org |
| 106 | ``` |
| 107 | |
| 108 | - TODO: Make this an actual spec, for now, some Python: |
| 109 | + ## Parsing |
| 110 | + |
| 111 | + An example parser written in the Python programming language. Any programming |
| 112 | + language which implements a valid RFC7565 parser should be sufficent for |
| 113 | + reading a project URI. |
| 114 | |
| 115 | ```python |
| 116 | - from urllib.parse import urlparse, quote_plus |
| 117 | + from urllib.parse import urlparse |
| 118 | |
| 119 | - def from_string(text): |
| 120 | + def parse_project(text): |
| 121 | url = urlparse(text) |
| 122 | + if url.scheme != "project": # schema must be repository:// |
| 123 | + raise Exception("Wrong scheme, should be project://") |
| 124 | if not url.path: |
| 125 | - return (None, None) |
| 126 | + raise Exception("Missing slug part") |
| 127 | split = url.path.split("@", 1) |
| 128 | if len(split) == 2: |
| 129 | - return (split[0], split[1]) |
| 130 | + domain = urlparse(f"ignore://{split[1]}") |
| 131 | + return (split[0], domain.netloc) |
| 132 | return (split[0], None) |
| 133 | - |
| 134 | - def to_string(slug, domain=None): |
| 135 | - if domain: |
| 136 | - return quote_plus(f"project://{slug}@{domain}") |
| 137 | - else: |
| 138 | - return quote_plus(f"project://{slug}") |
| 139 | ``` |
| 140 | |
| 141 | |
| 142 | @@ -90,6 +100,9 @@ an avatar for use in other applications. |
| 143 | |
| 144 | #### `http://feed-forge.org/rel/chatroom` |
| 145 | |
| 146 | + A chatroom refers to an interactive chat environment for real time |
| 147 | + collaboration among project contributors. |
| 148 | + |
| 149 | ```json |
| 150 | { |
| 151 | "rel": "http://webfinger.net/rel/chatroom", |
| 152 | @@ -180,7 +193,7 @@ Reference to a VCS managed code repository. |
| 153 | The following [property identifiers](https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.3) are available |
| 154 | for use within a project resource. |
| 155 | |
| 156 | - ```txt |
| 157 | + ```text |
| 158 | http://forge-feed.org/ns/label |
| 159 | http://forge-feed.org/ns/repository-uri |
| 160 | http://forge-feed.org/ns/vcs-type |
| 161 | @@ -218,7 +231,7 @@ Identifies VCS types, valid strings are: |
| 162 | ## Example Multi-Repository Query |
| 163 | |
| 164 | A [WebFinger](https://webfinger.net/spec/) query may be used to identify |
| 165 | - detailed information about a public repository at a particular forge. Here is |
| 166 | + detailed information about a public project at a particular forge. Here is |
| 167 | an example response about a fictitious project which has two code repositories |
| 168 | associated with it as well as chat links, and a bug tracking system. |
| 169 | |
| 170 | diff --git a/content/webfinger-repository.md b/content/webfinger-repository.md |
| 171 | index a7181dd..f1cc33e 100644 |
| 172 | --- a/content/webfinger-repository.md |
| 173 | +++ b/content/webfinger-repository.md |
| 174 | @@ -2,18 +2,19 @@ |
| 175 | title: Repository Discovery via WebFinger |
| 176 | --- |
| 177 | |
| 178 | - A repository refers to a version control managed source code which may be |
| 179 | - browsed over HTTP or downloaded with specific tooling. |
| 180 | + A repository refers to a version control managed source code which MAY be |
| 181 | + browsable over HTTP or accessed with specific tooling. |
| 182 | |
| 183 | ## Repository URI |
| 184 | |
| 185 | - A repository URI identifies a code repository and optionally the host that is |
| 186 | - resides on. This value is similar to |
| 187 | - [RFC7565](https://datatracker.ietf.org/doc/html/rfc7565). The slug and hostname |
| 188 | - part MUST match the URI path specification as defined in |
| 189 | + A repository URI identifies a VCS resource and optionally the host that is |
| 190 | + resides on. The value serves to contain enough information such that a client |
| 191 | + with a simple URL parser may easily parse the identifier. A repository URI MUST |
| 192 | + be a valid [RFC7565](https://datatracker.ietf.org/doc/html/rfc7565) URI. The |
| 193 | + slug and hostname parts MUST match the URI path specification as defined in |
| 194 | [RFC3986-3.3](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) while the |
| 195 | hostname, if specified, must match |
| 196 | - [RFC3986-3.2.2](https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2) |
| 197 | + [RFC3986-3.2.2](https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2). |
| 198 | |
| 199 | repository-uri = prefix slug hostname |
| 200 | |
| 201 | @@ -37,25 +38,26 @@ https://example.org/.well-known/webfinger?resource=repository://spartacus/game |
| 202 | https://example.org/.well-known/webfinger?resource=repository://spartacus/game@example.org |
| 203 | ``` |
| 204 | |
| 205 | - TODO: Make this an actual spec, for now, some Python: |
| 206 | + ## Parsing |
| 207 | + |
| 208 | + An example parser written in the Python programming language. Any programming |
| 209 | + language which implements a valid RFC7565 parser should be sufficent for |
| 210 | + reading a project URI. |
| 211 | |
| 212 | ```python |
| 213 | - from urllib.parse import urlparse, quote_plus |
| 214 | + from urllib.parse import urlparse |
| 215 | |
| 216 | def from_string(text): |
| 217 | url = urlparse(text) |
| 218 | + if url.scheme != "repository": # schema must be repository:// |
| 219 | + raise Exception("Wrong scheme, should be repository://") |
| 220 | if not url.path: |
| 221 | - return (None, None) |
| 222 | + raise Exception("Missing slug part") |
| 223 | split = url.path.split("@", 1) |
| 224 | if len(split) == 2: |
| 225 | - return (split[0], split[1]) |
| 226 | + domain = urlparse(f"ignore://{split[1]}") |
| 227 | + return (split[0], domain.netloc) |
| 228 | return (split[0], None) |
| 229 | - |
| 230 | - def to_string(slug, domain=None): |
| 231 | - if domain: |
| 232 | - return quote_plus(f"repository://{slug}@{domain}") |
| 233 | - else: |
| 234 | - return quote_plus(f"repository://{slug}") |
| 235 | ``` |
| 236 | |
| 237 | |
| 238 | @@ -64,7 +66,7 @@ def to_string(slug, domain=None): |
| 239 | The following [Relation Types](https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.1) are |
| 240 | available for use within a repository resource. |
| 241 | |
| 242 | - ``` |
| 243 | + ```text |
| 244 | http://forge-feed.org/rel/avatar |
| 245 | http://forge-feed.org/rel/clone |
| 246 | http://forge-feed.org/rel/description |
| 247 | @@ -155,9 +157,9 @@ Links to issue tracking systems. |
| 248 | ### Property Identifiers |
| 249 | |
| 250 | The following [property identifiers](https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.3) are available |
| 251 | - for use within a project resource. |
| 252 | + for use within a repository resource. |
| 253 | |
| 254 | - ``` |
| 255 | + ```text |
| 256 | http://forge-feed.org/ns/label |
| 257 | http://forge-feed.org/ns/project-uri |
| 258 | http://forge-feed.org/ns/spdx-identifier |
| 259 | @@ -185,7 +187,7 @@ Identifies VCS types, valid strings are: |
| 260 | svn (Apache Subversion) subversion.apache.org |
| 261 | |
| 262 | |
| 263 | - ## WebFinger Query |
| 264 | + ## Example Repository WebFinger Query |
| 265 | |
| 266 | A [WebFinger](https://webfinger.net/spec/) query may be used to identify |
| 267 | detailed information about a public repository at a particular forge. Here is |
| 268 | diff --git a/parser.py b/parser.py |
| 269 | new file mode 100644 |
| 270 | index 0000000..15f52a3 |
| 271 | --- /dev/null |
| 272 | +++ b/parser.py |
| 273 | @@ -0,0 +1,28 @@ |
| 274 | + from urllib.parse import urlparse, quote_plus |
| 275 | + |
| 276 | + |
| 277 | + def from_string(text): |
| 278 | + url = urlparse(text) |
| 279 | + if not url.path: |
| 280 | + return (None, None) |
| 281 | + split = url.path.split("@", 1) |
| 282 | + if len(split) == 2: |
| 283 | + return (split[0], split[1]) |
| 284 | + return (split[0], None) |
| 285 | + |
| 286 | + |
| 287 | + def to_string(slug, domain=None): |
| 288 | + if domain: |
| 289 | + return quote_plus(f"repository:{slug}@{domain}") |
| 290 | + else: |
| 291 | + return quote_plus(f"repository:{slug}") |
| 292 | + |
| 293 | + |
| 294 | + print(from_string("repository://fuu/bar@example.org")) |
| 295 | + print(from_string("repository://fuu/bar/baz/qux@example.org")) |
| 296 | + print(from_string("repository://fuu/bar/baz/qux")) |
| 297 | + print(from_string("repository://~hello/world")) |
| 298 | + print(from_string("repository://~hello/world@example.org")) |
| 299 | + print(from_string("repository://~hello/world@example.org@aaaa")) |
| 300 | + print(to_string("fuu/bar/baz", None)) |
| 301 | + print(to_string("fuu/bar/baz", "example.org")) |
| 302 | diff --git a/project-uri.py b/project-uri.py |
| 303 | new file mode 100644 |
| 304 | index 0000000..4e4c9ba |
| 305 | --- /dev/null |
| 306 | +++ b/project-uri.py |
| 307 | @@ -0,0 +1,30 @@ |
| 308 | + from urllib.parse import urlparse, quote_plus |
| 309 | + |
| 310 | + |
| 311 | + def from_string(text): |
| 312 | + url = urlparse(text) |
| 313 | + if url.scheme != "project": # schema must be repository:// |
| 314 | + raise Exception("Wrong scheme, should be project://") |
| 315 | + if not url.path: |
| 316 | + raise Exception("Missing slug part") |
| 317 | + split = url.path.split("@", 1) |
| 318 | + if len(split) == 2: |
| 319 | + domain = urlparse(f"ignore://{split[1]}") |
| 320 | + return (split[0], domain.netloc) |
| 321 | + return (split[0], None) |
| 322 | + |
| 323 | + |
| 324 | + def to_string(slug, domain=None): |
| 325 | + if domain: |
| 326 | + return quote_plus(f"project:{slug}@{domain}") |
| 327 | + else: |
| 328 | + return quote_plus(f"project:{slug}") |
| 329 | + |
| 330 | + |
| 331 | + print(from_string("project://fuu/bar@example.org")) |
| 332 | + print(from_string("project://fuu/bar/baz/qux@example.org")) |
| 333 | + print(from_string("project://fuu/bar/baz/qux")) |
| 334 | + print(from_string("project://~hello/world")) |
| 335 | + print(from_string("project://~hello/world@example.org")) |
| 336 | + print(to_string("fuu/bar/baz", None)) |
| 337 | + print(to_string("fuu/bar/baz", "example.org")) |
| 338 | diff --git a/repository-uri.py b/repository-uri.py |
| 339 | index 15f52a3..3264e18 100644 |
| 340 | --- a/repository-uri.py |
| 341 | +++ b/repository-uri.py |
| 342 | @@ -3,11 +3,14 @@ from urllib.parse import urlparse, quote_plus |
| 343 | |
| 344 | def from_string(text): |
| 345 | url = urlparse(text) |
| 346 | + if url.scheme != "repository": # schema must be repository:// |
| 347 | + raise Exception("Wrong scheme, should be repository://") |
| 348 | if not url.path: |
| 349 | - return (None, None) |
| 350 | + raise Exception("Missing slug part") |
| 351 | split = url.path.split("@", 1) |
| 352 | if len(split) == 2: |
| 353 | - return (split[0], split[1]) |
| 354 | + domain = urlparse(f"ignore://{split[1]}") |
| 355 | + return (split[0], domain.netloc) |
| 356 | return (split[0], None) |
| 357 | |
| 358 | |
| 359 | @@ -23,6 +26,5 @@ print(from_string("repository://fuu/bar/baz/qux@example.org")) |
| 360 | print(from_string("repository://fuu/bar/baz/qux")) |
| 361 | print(from_string("repository://~hello/world")) |
| 362 | print(from_string("repository://~hello/world@example.org")) |
| 363 | - print(from_string("repository://~hello/world@example.org@aaaa")) |
| 364 | print(to_string("fuu/bar/baz", None)) |
| 365 | print(to_string("fuu/bar/baz", "example.org")) |