Author:
Hash:
Timestamp:
+158 -21 +/-10 browse
Kevin Schoon [me@kevinschoon.com]
161b1139add5178d20da8094fe3e100bb67ae338
Sat, 19 Oct 2024 15:49:28 +0000 (1.2 years ago)
| 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 |