Commit

Author:

Hash:

Timestamp:

+307 -0 +/-2 browse

Kevin Schoon [me@kevinschoon.com]

6a7cd23c228707b4a20e74512659eac050775b08

Wed, 04 Jun 2025 22:32:34 +0000 (3 days ago)

init
1diff --git a/README.md b/README.md
2new file mode 100644
3index 0000000..af7bf76
4--- /dev/null
5+++ b/README.md
6 @@ -0,0 +1,282 @@
7+ # ForgeFeed
8+
9+ ForgeFeed is a small collection of standards which when implemented allow your
10+ [forge](https://en.wikipedia.org/wiki/Forge_(software)) to better federate with
11+ others across the internet.
12+
13+ *NOTE: This specification is a W.I.P and should not yet be implemented
14+ anywhere.*
15+
16+ ## Repository URI
17+
18+ A repository URI provides a unique identifier for referencing a repository
19+ that is present in a given code forge. This value is similar to
20+ [RFC7565](https://datatracker.ietf.org/doc/html/rfc7565).
21+
22+ repository-uri = "repository:"<slug>[@hostname]
23+
24+ If the hostname part is missing then the address of the server receiving the
25+ query is assumed.
26+
27+ TODO: Make this an actual spec, for now, some Python:
28+
29+ ```python
30+ from urllib.parse import urlparse, quote_plus
31+
32+ def from_string(text):
33+ url = urlparse(text)
34+ if not url.path:
35+ return (None, None)
36+ split = url.path.split("@", 1)
37+ if len(split) == 2:
38+ return (split[0], split[1])
39+ return (split[0], None)
40+
41+ def to_string(slug, domain=None):
42+ if domain:
43+ return quote_plus(f"repository:{slug}@{domain}")
44+ else:
45+ return quote_plus(f"repository:{slug}")
46+ ```
47+
48+
49+ ## WebFinger Query
50+
51+ A [WebFinger](https://webfinger.net/spec/) query may be used to identify
52+ detailed information about a public repository at a particular forge. Here is
53+ an example response about a ficticious repository:
54+
55+ `GET https://example.org/.well-known/webfinger?resource=repository:example/spartacus`
56+
57+ ```json
58+ {
59+ "subject": "repository:example/spartacus",
60+ "aliases": [
61+ "https://example.org/example/spartacus"
62+ ],
63+ "properties":
64+ {
65+ "http://forge-feed.org/rel/vcs-type": "git"
66+ },
67+ "links": [
68+ {
69+ "rel": "http://webfinger.net/rel/avatar",
70+ "href": "https://example.org/stylized-logo.png"
71+ },
72+ {
73+ "rel": "http://forge-feed.org/rel/description",
74+ "titles": {
75+ "en-us": "A Text Adventure Written in FORTRAN 77",
76+ "es": "Una Aventura de Texto Escrita en FORTRAN 77"
77+ }
78+ },
79+ {
80+ "rel": "http://forge-feed.org/rel/license",
81+ "href": "https://example.com/example/spartacus/tree/LICENSE",
82+ "properties": {
83+ "spdx-identifier": "GPL-2.0-or-later"
84+ }
85+ },
86+ {
87+ "rel": "http://forge-feed.org/rel/chatroom",
88+ "href": "ircs://irc.libera.chat/#spartacus-game"
89+ },
90+ {
91+ "rel": "http://forge-feed.org/rel/label",
92+ "properties": {
93+ "label": "fortran"
94+ }
95+ },
96+ {
97+ "rel": "http://forge-feed.org/rel/label",
98+ "properties": {
99+ "label": "text-adventure"
100+ }
101+ }
102+ ]
103+ }
104+ ```
105+
106+ ### Properties
107+
108+ #### http://feed-forge.org/ns/vcs-type
109+
110+ bzr (GNU Bazaar) bazaar.canonical.com
111+ darcs (Darcs) darcs.net
112+ fossil (Fossil) fossil-scm.org
113+ git (Git) git-scm.com
114+ hg (Mercurial) mercurial-scm.org
115+ pijul (Pijul) pijul.org
116+ svn (Apache Subversion) subversion.apache.org
117+
118+ ### Link Extension Types
119+
120+ #### Homepage
121+
122+ Link to an HTTP representation of the project codebase
123+
124+ {
125+ "rel": "http://feed-forge.org/rel/homepage",
126+ "href": "https://example.org/example/spartacus"
127+ }
128+
129+ #### Description
130+
131+ A short text representation of the repository.
132+
133+ {
134+ "rel": "http://example.org/rel/description",
135+ "titles": {
136+ "en-us": "A Text Adventure Written in FORTRAN 77",
137+ "es": "Una Aventura de Texto Escrita en FORTRAN 77"
138+ }
139+ }
140+
141+ #### License
142+
143+ A license SPDX identifier and link to the license's full text.
144+
145+ {
146+ "rel": "http://feed-forge.org/rel/license",
147+ "href": "https://example.com/example/spartacus/tree/LICENSE",
148+ "properties": {
149+ "spdx-identifier": "GPL-2.0-or-later"
150+ }
151+ }
152+
153+ #### Chat Links
154+
155+ Links to chatrooms: IRC, Matrix, XMPP, etc.
156+
157+ {
158+ "rel": "http://feed-forge.org/rel/chatroom",
159+ "href": "ircs://irc.libera.chat/#spartacus-game"
160+ },
161+
162+ #### Mailing Lists
163+
164+ Links to associated mailing lists, forms, etc.
165+
166+
167+ #### Free-Form Tags
168+
169+ Arbitrary, free-form tags.
170+
171+ {
172+ "rel": "http://example.org/rel/label",
173+ "properties": {
174+ "label": "fortran"
175+ }
176+ },
177+ {
178+ "rel": "http://example.org/rel/label",
179+ "properties": {
180+ "label": "text-adventure"
181+ }
182+ }
183+
184+ # RSS Feeds
185+
186+ Your forge needs to expose an RSS feed in order for other peers to determine
187+ what code repositories exist in your server. Your server should expose
188+ repositories ordered by recent updates. The heuristic you use to determine
189+ "recently updated" depends on your forge. The simplest way to determine recent
190+ activity is likely by your VCS's concept of a "commit".
191+
192+ ## Determine if a Host Supports Forge Feed
193+
194+ Forges participating in the ForgeFeed protocol MUST respond to an HTTP
195+ GET request such as below:
196+
197+ GET https://example.org/.well-known/forge-feed/firehose.xml
198+
199+ Responses
200+
201+ ### HTTP 200
202+
203+ This indicates the feed exists and the server will return the feed XML content directly. TODO: Is this valid for .well-known?
204+
205+ ### HTTP 301
206+
207+ The server MAY respond with a 301 indicating a different location however the resource MUST reside on the server which the well-known request was made against.
208+
209+ ### HTTP 501
210+
211+ This indicates the server no longer wishes to be indexed and that the operator of a forge index should cease crawling operations.
212+
213+ ### An example Feed
214+
215+ Below is an example RSS feed with an optional repository section (described below).
216+
217+ ```xml
218+ <?xml version="1.0" encoding="utf-8"?>
219+ <rss version="2.0">
220+ <channel>
221+ <title>Firehose</title>
222+ <link>https://code.example.org/browse</link>
223+ <description>Recent Activity @ code.example.org</description>
224+ <lastBuildDate>Tue, 03 Jun 2025 14:18:48 +0000</lastBuildDate>
225+ <ttl>60</ttl>
226+ <item>
227+ <title>Spartacus</title>
228+ <description>A Text Adventure Written in FORTRAN 77<description/>
229+ <author>admin@example.org (Administrator)</author>
230+ <!-- Link to an HTTP representation of the repository -->
231+ <link>https://example.org/example/spartacus</link>
232+ <!-- Guid may be the repository name at a particular commit id-->
233+ <guid>https://example.org/example/spartacus@f28ce68f16e338355e55372bc788e8ea5feda470</guid>
234+ <pubDate>Sun, 25 May 2025 18:23:03 +0200</pubDate>
235+ <!-- Optional enclosure with an image reference if your forge supports this -->
236+ <enclosure url="https://example.org/example/spartacus/raw/main/logo.png" length="12216320" type="image/png" />
237+ <!-- Although highly recommended, the fields below are optional -->
238+ <repository>
239+ <!-- repository URI described above -->
240+ <webfinger>repository:example/spartacus</webfinger>
241+ </repository>
242+ </item>
243+ </channel>
244+ </rss>
245+ ```
246+
247+ ### Repository Extension
248+
249+ In order to facilitate discovery by external indexes it is highly recommended
250+ that your server implement the webfinger repository specification described
251+ above so that repository indexes may populate their state with rich information
252+ about code repositories hosted on your server.
253+
254+ <repository>
255+ <webfinger>repository:example/spartacus@example.org</webfinger>
256+ </repository>
257+
258+ If the host section of the URI is included that is MUST match domain name
259+ of the server which is providing the feed.
260+
261+ ### Recommendations for Enumerating Repository Events
262+
263+ #### Specify a Maximum Timeframe
264+
265+ Your activity feed should not include repository events that are older than 1
266+ week.
267+
268+ #### Event "Clamping"
269+
270+ It may be undesirable to enumerate repository events items with the simple
271+ heuristic of
272+
273+ end = time.now()
274+ start = end - 6h
275+
276+ Because the oldest repository events will fall out of the time window on
277+ subsequent queries by RSS readers. Instead events can be "clamped" within a
278+ certain time period. For example you may choose to publish four buckets of
279+ updates per day:
280+
281+ 00:00:00 06:00:00
282+ 06:00:00 12:00:00
283+ 12:00:00 18:00:00
284+ 18:00:00 00:00:00
285+
286+ # Forge Discovery Index
287+
288+ TODO
289 diff --git a/repository-uri.py b/repository-uri.py
290new file mode 100644
291index 0000000..dd4a254
292--- /dev/null
293+++ b/repository-uri.py
294 @@ -0,0 +1,25 @@
295+ from urllib.parse import urlparse
296+ from urllib.parse import quote_plus
297+
298+ def from_string(text):
299+ url = urlparse(text)
300+ if not url.path:
301+ return (None, None)
302+ split = url.path.split("@", 1)
303+ if len(split) == 2:
304+ return (split[0], split[1])
305+ return (split[0], None)
306+
307+ def to_string(slug, domain=None):
308+ if domain:
309+ return quote_plus(f"repository:{slug}@{domain}")
310+ else:
311+ return quote_plus(f"repository:{slug}")
312+
313+
314+ print(from_string("repository:fuu/bar@example.org"))
315+ print(from_string("repository:fuu/bar/baz/qux@example.org"))
316+ print(from_string("repository:fuu/bar/baz/qux"))
317+ print(to_string("fuu/bar/baz", None))
318+ print(to_string("fuu/bar/baz", "example.org"))
319+