Commit
+158 -21 +/-10 browse
1 | diff --git a/README.md b/README.md |
2 | index 81951d8..e9106a1 100644 |
3 | --- a/README.md |
4 | +++ b/README.md |
5 | @@ -45,7 +45,7 @@ A general development channel `#ayllu` is available on [libera](ircs://irc.liber |
6 | | ----------------------------- | ------ | ------------------------------------------------------------------ | |
7 | | git-log | ✅ | | |
8 | | git-clone (http & ssh) | ✅ | | |
9 | - | git-notes | TODO | | |
10 | + | git-notes | ✅ | | |
11 | | git-blame | ✅ | | |
12 | | git-lfs | ✅ | | |
13 | | git-verify | ✅ | | |
14 | @@ -64,12 +64,12 @@ A general development channel `#ayllu` is available on [libera](ircs://irc.liber |
15 | | activity tracking | ✅ | | |
16 | | extensible plugin system | ✅ | | |
17 | | WebFinger | ✅ | | |
18 | - | mailing list support | WIP | | |
19 | + | mailing list support | WIP | | |
20 | | xmpp integration | WIP | | |
21 | - | activity pub based federation | TBD | | |
22 | - | continuous integration | TODO | | |
23 | - | graphql api | TODO | | |
24 | - | centralized "hub" | TODO | | |
25 | + | activity pub based federation | TBD | | |
26 | + | continuous integration | TODO | | |
27 | + | programmatic access | TODO | | |
28 | + | centralized "hub" | TODO | | |
29 | |
30 | ## Installation |
31 | |
32 | diff --git a/ayllu/Cargo.toml b/ayllu/Cargo.toml |
33 | index 9c93521..e55335b 100644 |
34 | --- a/ayllu/Cargo.toml |
35 | +++ b/ayllu/Cargo.toml |
36 | @@ -2,6 +2,7 @@ |
37 | name = "ayllu" |
38 | version = "0.2.1" |
39 | edition = "2021" |
40 | + rust-version = "1.70.0" |
41 | |
42 | [[bin]] |
43 | name = "ayllu" |
44 | @@ -13,14 +14,6 @@ ayllu_git = { path = "../crates/git" } |
45 | ayllu_config = { path = "../crates/config" } |
46 | ayllu_database = { path = "../crates/database" } |
47 | |
48 | - # ayllu_config = { workspace = true } |
49 | - # ayllu_database = { workspace = true } |
50 | - # ayllu_git = { workspace = true } |
51 | - # ayllu_rpc = { workspace = true } |
52 | - # ayllu_scheduler = {workspace = true} |
53 | - # ayllu-mail = {workspace = true} |
54 | - # ayllu-build = {workspace = true} |
55 | - # ayllu-xmpp = {workspace = true} |
56 | |
57 | sqlx = { version = "0.8.2", features = [ "runtime-tokio-rustls", "sqlite", "macros", "time" ] } |
58 | git2 = "0.19.0" |
59 | diff --git a/ayllu/src/web2/routes/commit.rs b/ayllu/src/web2/routes/commit.rs |
60 | index 9204fa2..84cfb6c 100644 |
61 | --- a/ayllu/src/web2/routes/commit.rs |
62 | +++ b/ayllu/src/web2/routes/commit.rs |
63 | @@ -25,6 +25,10 @@ pub async fn serve( |
64 | ); |
65 | ctx.insert("commit_hash", &commit_id); |
66 | let commit = repository.commit(Some(commit_id.to_string()))?.unwrap(); |
67 | + if commit.has_note.is_some_and(|has_note| has_note) { |
68 | + let note = repository.read_note(commit.id.as_str())?; |
69 | + ctx.insert("note", ¬e); |
70 | + } |
71 | let (stats, diff) = repository.diff(&commit_id)?; |
72 | ctx.insert("commit", &commit); |
73 | ctx.insert("stats", &stats); |
74 | diff --git a/ayllu/src/web2/routes/refs.rs b/ayllu/src/web2/routes/refs.rs |
75 | index c82710c..b413ce4 100644 |
76 | --- a/ayllu/src/web2/routes/refs.rs |
77 | +++ b/ayllu/src/web2/routes/refs.rs |
78 | @@ -29,6 +29,8 @@ pub async fn refs( |
79 | ctx.insert("branches", &branches); |
80 | let tags = repository.tags()?; |
81 | ctx.insert("tags", &tags); |
82 | + let notes = repository.notes()?; |
83 | + ctx.insert("notes", ¬es); |
84 | let body = templates.render("refs.html", &ctx)?; |
85 | Ok(Html(body)) |
86 | } |
87 | diff --git a/ayllu/themes/default/templates/commit.html b/ayllu/themes/default/templates/commit.html |
88 | index 73f0eb6..e6e5829 100644 |
89 | --- a/ayllu/themes/default/templates/commit.html |
90 | +++ b/ayllu/themes/default/templates/commit.html |
91 | @@ -2,6 +2,7 @@ |
92 | {% import "macros.html" as macros %} |
93 | {% block content %} |
94 | <section class="stretch"> |
95 | + <h5> Commit </h5> |
96 | <section> |
97 | <span><b>Author:</b></span> |
98 | <span class="right"> |
99 | @@ -42,6 +43,23 @@ |
100 | <pre>{{ commit.message }}</pre> |
101 | </div> |
102 | {% endif %} |
103 | + {% if commit.has_note %} |
104 | + <h5>Note</h5> |
105 | + <section> |
106 | + <span><b>Author:</b></span> |
107 | + <span class="right"> |
108 | + <a href="/{{ collection }}/{{ name }}/log?username={{ note.author_name | urlencode }}&email={{ note.author_email | urlencode }}">{{ note.author_name }}</a> |
109 | + [<a href="mailto://{{ note.author_email }}">{{ commit.author_email }}</a>] |
110 | + </span> |
111 | + </section> |
112 | + <section> |
113 | + <span><b>Timestamp:</b></span> |
114 | + <span class="right">{{ note.author_epoch | format_epoch }} ({{ note.author_epoch | friendly_time }})</span> |
115 | + </section> |
116 | + <div class="message"> |
117 | + <pre>{{ note.message }}</pre> |
118 | + </div> |
119 | + {% endif %} |
120 | </section> |
121 | {{ diff.1 | safe }} |
122 | </section> |
123 | diff --git a/ayllu/themes/default/templates/log.html b/ayllu/themes/default/templates/log.html |
124 | index 82e85b5..a9938b2 100644 |
125 | --- a/ayllu/themes/default/templates/log.html |
126 | +++ b/ayllu/themes/default/templates/log.html |
127 | @@ -6,6 +6,7 @@ |
128 | <thead> |
129 | <tr> |
130 | <th>ID</th> |
131 | + <th>Flags</th> |
132 | <th>Age</th> |
133 | <th class="collapse">Author</th> |
134 | <th>Message</th> |
135 | @@ -20,9 +21,13 @@ |
136 | {% else %} |
137 | <span class="negative"> |
138 | {% endif %} |
139 | - <a href="/{{ collection }}/{{ name }}/commit/{{ commit.id }}">{{ commit.id | truncate(length=8, end="") }}</a> |
140 | + <a href="/{{ collection }}/{{ name }}/commit/{{ commit.id }}">{{ commit.id | truncate(length=12, end="") }}</a> |
141 | + </span> |
142 | </td> |
143 | - </span> |
144 | + <td> |
145 | + {% if commit.has_note %} <div data-tooltip="Note">[N]</div>{% endif %} |
146 | + {% if commit.is_extended %} <div data-tooltip="Extended">[E]</div>{% endif %} |
147 | + </td> |
148 | <td>{{ commit.epoch | friendly_time }}</td> |
149 | <td class="collapse"> |
150 | <a href="/{{ collection }}/{{ name }}/log?username={{ commit.author_name | urlencode }}&email={{ commit.author_email | urlencode }}">{{ commit.author_name }}</a> |
151 | diff --git a/ayllu/themes/default/templates/refs.html b/ayllu/themes/default/templates/refs.html |
152 | index 3c6a53f..8553fe8 100644 |
153 | --- a/ayllu/themes/default/templates/refs.html |
154 | +++ b/ayllu/themes/default/templates/refs.html |
155 | @@ -56,5 +56,31 @@ |
156 | {% endfor %} |
157 | </tbody> |
158 | </table> |
159 | + <h4 class="minor-header">Notes</h4> |
160 | + <table> |
161 | + <thead> |
162 | + <tr> |
163 | + <th>Commit</th> |
164 | + <th>Author</th> |
165 | + <th class="collapse">Age</th> |
166 | + <th>Message</th> |
167 | + </tr> |
168 | + </thead> |
169 | + <tbody> |
170 | + {% for note in notes %} |
171 | + <tr> |
172 | + <td> |
173 | + <a href="/{{ collection }}/{{ name }}/commit/{{ note.commit.id }}"> |
174 | + {{ note.commit.id | truncate(length=8, end="") }}</a> |
175 | + </td> |
176 | + <td>{{note.author_name}}</td> |
177 | + <td class="collapse">{{ note.author_epoch | friendly_time }}</td> |
178 | + <td> |
179 | + <pre>{{ note.message | truncate(length=32, end="...") }}</pre> |
180 | + </td> |
181 | + </tr> |
182 | + {% endfor %} |
183 | + </tbody> |
184 | + </table> |
185 | </section> |
186 | {% endblock %} |
187 | diff --git a/ayllu/themes/default/theme.css b/ayllu/themes/default/theme.css |
188 | index e789142..876dbbc 100644 |
189 | --- a/ayllu/themes/default/theme.css |
190 | +++ b/ayllu/themes/default/theme.css |
191 | @@ -479,3 +479,11 @@ thead.collection > tr th { |
192 | thead.collection > tr th > a { |
193 | font-size: larger; |
194 | } |
195 | + |
196 | + [data-tooltip]:hover::after { |
197 | + display: block; |
198 | + position: absolute; |
199 | + content: attr(data-tooltip); |
200 | + border: 1px solid black; |
201 | + padding: .25em; |
202 | + } |
203 | diff --git a/crates/git/src/lite.rs b/crates/git/src/lite.rs |
204 | index 8c8646e..97f1c15 100644 |
205 | --- a/crates/git/src/lite.rs |
206 | +++ b/crates/git/src/lite.rs |
207 | @@ -1,6 +1,6 @@ |
208 | use git2::{ |
209 | Blob as GitBlob, Branch as GitBranch, Commit as GitCommit, DiffStats as GitDiffStats, |
210 | - Tag as GitTag, |
211 | + Note as GitNote, Tag as GitTag, |
212 | }; |
213 | use serde::Serialize; |
214 | |
215 | @@ -41,7 +41,7 @@ pub struct BlameLine { |
216 | pub commit_id: String, |
217 | } |
218 | |
219 | - #[derive(Clone, Serialize)] |
220 | + #[derive(Default, Clone, Debug, Serialize)] |
221 | pub struct Commit { |
222 | pub id: String, |
223 | pub author_name: String, |
224 | @@ -56,6 +56,7 @@ pub struct Commit { |
225 | pub is_extended: bool, |
226 | pub gpg_signature: Option<String>, |
227 | pub is_verified: Option<bool>, |
228 | + pub has_note: Option<bool>, |
229 | } |
230 | |
231 | impl Commit { |
232 | @@ -102,7 +103,7 @@ impl From<GitCommit<'_>> for Commit { |
233 | message, |
234 | is_extended, |
235 | gpg_signature, |
236 | - is_verified: None, |
237 | + ..Default::default() |
238 | } |
239 | } |
240 | } |
241 | @@ -244,3 +245,45 @@ impl From<GitDiffStats> for Stats { |
242 | } |
243 | } |
244 | } |
245 | + |
246 | + #[derive(Serialize, Clone, Debug)] |
247 | + pub struct Note { |
248 | + pub id: String, |
249 | + pub message: String, |
250 | + pub author_name: String, |
251 | + pub author_email: String, |
252 | + pub author_epoch: i64, |
253 | + pub committer_name: String, |
254 | + pub committer_email: String, |
255 | + pub committer_epoch: i64, |
256 | + pub commit: Commit, |
257 | + } |
258 | + |
259 | + impl From<(GitNote<'_>, GitCommit<'_>)> for Note { |
260 | + fn from(value: (GitNote, GitCommit)) -> Self { |
261 | + let (note, commit) = value; |
262 | + let author = note.author(); |
263 | + let committer = note.committer(); |
264 | + Note { |
265 | + id: note.id().to_string(), |
266 | + message: note |
267 | + .message() |
268 | + .map_or(String::default(), |message| message.to_string()), |
269 | + author_name: author |
270 | + .name() |
271 | + .map_or(String::default(), |name| name.to_string()), |
272 | + author_email: author |
273 | + .email() |
274 | + .map_or(String::default(), |name| name.to_string()), |
275 | + author_epoch: author.when().seconds(), |
276 | + committer_name: committer |
277 | + .name() |
278 | + .map_or(String::default(), |name| name.to_string()), |
279 | + committer_email: committer |
280 | + .email() |
281 | + .map_or(String::default(), |email| email.to_string()), |
282 | + committer_epoch: committer.when().seconds(), |
283 | + commit: commit.into(), |
284 | + } |
285 | + } |
286 | + } |
287 | diff --git a/crates/git/src/wrapper.rs b/crates/git/src/wrapper.rs |
288 | index f2f960a..3210239 100644 |
289 | --- a/crates/git/src/wrapper.rs |
290 | +++ b/crates/git/src/wrapper.rs |
291 | @@ -398,7 +398,9 @@ impl Wrapper { |
292 | Some(hash) => { |
293 | let hash = Oid::from_str(hash.as_str())?; |
294 | let commit = self.repository.find_commit(hash)?; |
295 | + let commit_id = commit.id(); |
296 | let mut commit = lite::Commit::from(commit); |
297 | + commit.has_note = Some(self.repository.find_note(None, commit_id).is_ok()); |
298 | if commit.gpg_signature.is_some() { |
299 | commit.is_verified = self.verify(&commit.id)?; |
300 | } |
301 | @@ -411,7 +413,10 @@ impl Wrapper { |
302 | let last_commit = head.peel_to_commit()?; |
303 | match self.repository.find_commit(last_commit.id()) { |
304 | Ok(commit) => { |
305 | + let commit_id = commit.id(); |
306 | let mut commit = lite::Commit::from(commit); |
307 | + commit.has_note = |
308 | + Some(self.repository.find_note(None, commit_id).is_ok()); |
309 | if commit.gpg_signature.is_some() { |
310 | commit.is_verified = self.verify(&commit.id)?; |
311 | } |
312 | @@ -640,6 +645,7 @@ impl Wrapper { |
313 | }; |
314 | |
315 | let mut commit: lite::Commit = commit.into(); |
316 | + commit.has_note = Some(self.repository.find_note(None, id).is_ok()); |
317 | if commit.gpg_signature.is_some() { |
318 | // only try to verify if signature is present |
319 | commit.is_verified = self.verify(&commit.id)?; |
320 | @@ -678,8 +684,9 @@ impl Wrapper { |
321 | Some(_) => {} |
322 | None => { |
323 | hm.insert(commit_id, true); |
324 | - let commit = self.repository.find_commit(commit_id)?; |
325 | - commits.push(commit.into()); |
326 | + let mut commit: lite::Commit = self.repository.find_commit(commit_id)?.into(); |
327 | + commit.has_note = Some(self.repository.find_note(None, commit_id).is_ok()); |
328 | + commits.push(commit); |
329 | } |
330 | } |
331 | } |
332 | @@ -930,6 +937,37 @@ impl Wrapper { |
333 | Ok(branches) |
334 | } |
335 | |
336 | + /// Return all notes with references to whichever commits the point to |
337 | + pub fn note_ids(&self) -> Result<Vec<(Oid, Oid)>, Error> { |
338 | + self.repository |
339 | + .notes(None)? |
340 | + .try_fold(Vec::new(), |mut accm, note| { |
341 | + let (note_id, commit_id) = note?; |
342 | + accm.push((note_id, commit_id)); |
343 | + Ok(accm) |
344 | + }) |
345 | + } |
346 | + |
347 | + pub fn read_note(&self, commit_id: &str) -> Result<lite::Note, Error> { |
348 | + let commit_id = Oid::from_str(commit_id)?; |
349 | + let note = self.repository.find_note(None, commit_id)?; |
350 | + let commit = self.repository.find_commit(commit_id)?; |
351 | + Ok((note, commit).into()) |
352 | + } |
353 | + |
354 | + /// Return all notes fully resolved with their associated commits |
355 | + /// NOTE: unsure on the performance of this |
356 | + pub fn notes(&self) -> Result<Vec<lite::Note>, Error> { |
357 | + self.note_ids()? |
358 | + .iter() |
359 | + .try_fold(Vec::new(), |mut accm, (_, commit_id)| { |
360 | + let note = self.repository.find_note(None, *commit_id)?; |
361 | + let commit = self.repository.find_commit(*commit_id)?; |
362 | + accm.push((note, commit).into()); |
363 | + Ok(accm) |
364 | + }) |
365 | + } |
366 | + |
367 | /// generate an archive of the reference, may be a branch name e.g. main, |
368 | /// a tag e.g. 0.1.9, or a git hash e.g. c73477ba0c7159002bd6d92dbdcd6de8292a034b |
369 | /// This uses the git binary directly since the archive |