| 12 |
12 |
|
//!
|
| 13 |
13 |
|
//! The repository is private permanently. Publishing a set of annotations is a
|
| 14 |
14 |
|
//! moderation and consent decision, not a toggle.
|
|
15 |
+ |
//!
|
|
16 |
+ |
//! The repository the annotation was written against is recorded in the ref
|
|
17 |
+ |
//! name, `refs/notes/annotations/<owner>/<repo>`, and nowhere else. It has to
|
|
18 |
+ |
//! be in the repository for the same reason the annotation is: a clone that
|
|
19 |
+ |
//! carried the writing without the context it was written in would have lost
|
|
20 |
+ |
//! half of it.
|
| 15 |
21 |
|
|
| 16 |
22 |
|
use axum::{
|
| 17 |
23 |
|
Form,
|
| 25 |
31 |
|
auth::AuthUser,
|
| 26 |
32 |
|
config::Config,
|
| 27 |
33 |
|
constants::ANNOTATION_REPO_NAME,
|
| 28 |
|
- |
db::{self, UserId},
|
|
34 |
+ |
db,
|
| 29 |
35 |
|
error::{AppError, Result},
|
| 30 |
|
- |
git::notes::{self, GixEngine},
|
| 31 |
36 |
|
validation,
|
| 32 |
37 |
|
};
|
| 33 |
38 |
|
|
| 34 |
39 |
|
use super::{ResolvedRepo, notes_write::write_note_across};
|
| 35 |
40 |
|
|
|
41 |
+ |
/// The first component of every namespace makenot.work writes into an
|
|
42 |
+ |
/// annotation repository.
|
|
43 |
+ |
///
|
|
44 |
+ |
/// The origin lives in the ref name because it has to live in the repository.
|
|
45 |
+ |
/// A clone of the annotation repository carries `refs/notes/annotations/o/r`
|
|
46 |
+ |
/// and so carries the fact that the note was written against `o/r`; a column in
|
|
47 |
+ |
/// Postgres saying the same thing would be lost the moment the repository is
|
|
48 |
+ |
/// exported, which is the promise this feature is built on.
|
|
49 |
+ |
pub(crate) const ANNOTATION_NS_PREFIX: &str = "annotations";
|
|
50 |
+ |
|
|
51 |
+ |
/// The namespace an annotation on a commit in `owner/repo` is written to.
|
|
52 |
+ |
///
|
|
53 |
+ |
/// Namespaced per origin rather than written to `commits`, and both halves of
|
|
54 |
+ |
/// that matter. Per origin, because the same hash can be served by a fork and
|
|
55 |
+ |
/// by its parent and the reader wants to know which page they were reading.
|
|
56 |
+ |
/// Not under `commits`, because git stores loose refs as files and
|
|
57 |
+ |
/// `refs/notes/commits` as a file cannot coexist with `refs/notes/commits/o/r`
|
|
58 |
+ |
/// as a directory: a reader who pushes an ordinary `commits` note into their
|
|
59 |
+ |
/// own annotation repository would break every annotation in it.
|
|
60 |
+ |
pub(crate) fn annotation_namespace(owner: &str, repo: &str) -> String {
|
|
61 |
+ |
format!("{ANNOTATION_NS_PREFIX}/{owner}/{repo}")
|
|
62 |
+ |
}
|
|
63 |
+ |
|
|
64 |
+ |
/// The repository an annotation namespace was written against, or `None`.
|
|
65 |
+ |
///
|
|
66 |
+ |
/// `None` for anything the reader pushed into their own annotation repository
|
|
67 |
+ |
/// by hand. That is readable and shown, and it is not editable through the
|
|
68 |
+ |
/// browser: the edit form lives on the origin's commit page and there is no
|
|
69 |
+ |
/// origin to put it on.
|
|
70 |
+ |
pub(crate) fn origin_of_namespace(namespace: &str) -> Option<(&str, &str)> {
|
|
71 |
+ |
let rest = namespace
|
|
72 |
+ |
.strip_prefix(ANNOTATION_NS_PREFIX)?
|
|
73 |
+ |
.strip_prefix('/')?;
|
|
74 |
+ |
let (owner, repo) = rest.split_once('/')?;
|
|
75 |
+ |
if owner.is_empty() || repo.is_empty() || repo.contains('/') {
|
|
76 |
+ |
return None;
|
|
77 |
+ |
}
|
|
78 |
+ |
Some((owner, repo))
|
|
79 |
+ |
}
|
|
80 |
+ |
|
| 36 |
81 |
|
/// The account's annotation repository, creating the row and the bare
|
| 37 |
82 |
|
/// repository the first time it is needed.
|
| 38 |
83 |
|
///
|
| 93 |
138 |
|
super::resolve_repo(db, config, &username, ANNOTATION_REPO_NAME, Some(user.id)).await
|
| 94 |
139 |
|
}
|
| 95 |
140 |
|
|
| 96 |
|
- |
/// The reader's own annotation on `oid`, as written, or `None`.
|
| 97 |
|
- |
///
|
| 98 |
|
- |
/// A direct git read of one repository, not an index query: finding every
|
| 99 |
|
- |
/// annotation on a commit across the browse surface is P7b's job. Every error
|
| 100 |
|
- |
/// path yields `None` and logs, because an annotation is decoration on a commit
|
| 101 |
|
- |
/// page and must never take the page down with it.
|
| 102 |
|
- |
pub(crate) async fn annotation_source(
|
| 103 |
|
- |
db: &PgPool,
|
| 104 |
|
- |
config: &Config,
|
| 105 |
|
- |
user_id: UserId,
|
| 106 |
|
- |
username: &str,
|
| 107 |
|
- |
oid: &str,
|
| 108 |
|
- |
) -> Option<String> {
|
| 109 |
|
- |
let target = notes::Oid::from_hex(oid.as_bytes()).ok()?;
|
| 110 |
|
- |
|
| 111 |
|
- |
let row = match db::git_repos::get_annotation_repo(db, user_id).await {
|
| 112 |
|
- |
Ok(Some(repo)) => repo,
|
| 113 |
|
- |
Ok(None) => return None,
|
| 114 |
|
- |
Err(error) => {
|
| 115 |
|
- |
tracing::warn!(error = ?error, "annotation repo lookup failed");
|
| 116 |
|
- |
return None;
|
| 117 |
|
- |
}
|
| 118 |
|
- |
};
|
| 119 |
|
- |
if !db::git_repos::is_annotation_repo(&row) {
|
| 120 |
|
- |
return None;
|
| 121 |
|
- |
}
|
| 122 |
|
- |
|
| 123 |
|
- |
let resolved = match super::resolve_repo(db, config, username, &row.name, Some(user_id)).await {
|
| 124 |
|
- |
Ok(resolved) => resolved,
|
| 125 |
|
- |
Err(error) => {
|
| 126 |
|
- |
tracing::warn!(error = ?error, "annotation repo did not resolve");
|
| 127 |
|
- |
return None;
|
| 128 |
|
- |
}
|
| 129 |
|
- |
};
|
| 130 |
|
- |
|
| 131 |
|
- |
let read = resolved
|
| 132 |
|
- |
.with_repo(move |gix_repo| {
|
| 133 |
|
- |
let engine = GixEngine::new(gix_repo);
|
| 134 |
|
- |
let Some(ns) = notes::resolve_namespace(&engine, notes::DEFAULT_NAMESPACE)
|
| 135 |
|
- |
.map_err(crate::git::GitError::from)?
|
| 136 |
|
- |
else {
|
| 137 |
|
- |
return Ok(None);
|
| 138 |
|
- |
};
|
| 139 |
|
- |
let note = notes::note_for(&engine, ns.tip, target)
|
| 140 |
|
- |
.map_err(crate::git::GitError::from)?
|
| 141 |
|
- |
.map(|n| n.content_lossy().into_owned());
|
| 142 |
|
- |
Ok(note)
|
| 143 |
|
- |
})
|
| 144 |
|
- |
.await;
|
| 145 |
|
- |
|
| 146 |
|
- |
match read {
|
| 147 |
|
- |
Ok(note) => note,
|
| 148 |
|
- |
Err(error) => {
|
| 149 |
|
- |
tracing::warn!(error = ?error, "reading a personal annotation failed");
|
| 150 |
|
- |
None
|
| 151 |
|
- |
}
|
| 152 |
|
- |
}
|
| 153 |
|
- |
}
|
| 154 |
|
- |
|
| 155 |
141 |
|
#[derive(Deserialize)]
|
| 156 |
142 |
|
pub(super) struct AnnotationForm {
|
| 157 |
143 |
|
content: String,
|
| 225 |
211 |
|
// account rather than of which repository the note landed in.
|
| 226 |
212 |
|
let who = super::notes_write::identity(user.display_name.as_deref(), user.username.as_str());
|
| 227 |
213 |
|
|
| 228 |
|
- |
// No namespace picker: the surface is "my note on this commit", and the
|
| 229 |
|
- |
// default is what stock `git notes` reads. Pushing any other namespace into
|
| 230 |
|
- |
// their own repository stays open to them.
|
| 231 |
|
- |
let merged = write_note_across(
|
| 232 |
|
- |
&source,
|
| 233 |
|
- |
&dest,
|
| 234 |
|
- |
oid_str,
|
| 235 |
|
- |
notes::DEFAULT_NAMESPACE.to_string(),
|
| 236 |
|
- |
content,
|
| 237 |
|
- |
who,
|
| 238 |
|
- |
)
|
| 239 |
|
- |
.await?;
|
|
214 |
+ |
// No namespace picker: the surface is "my note on this commit", and which
|
|
215 |
+ |
// repository it was read in is the only thing that varies. Taken from the
|
|
216 |
+ |
// resolved row rather than from the URL, so a clone URL spelt
|
|
217 |
+ |
// `testrepo.git` cannot open a second namespace for the same repository.
|
|
218 |
+ |
let namespace = annotation_namespace(source.db_user.username.as_str(), &source.db_repo.name);
|
|
219 |
+ |
|
|
220 |
+ |
let merged =
|
|
221 |
+ |
write_note_across(&source, &dest, oid_str, namespace.clone(), content, who).await?;
|
| 240 |
222 |
|
|
| 241 |
223 |
|
super::notes_index::reindex_after_write(
|
| 242 |
224 |
|
db,
|
| 244 |
226 |
|
dest.db_repo.id,
|
| 245 |
227 |
|
user.username.as_str(),
|
| 246 |
228 |
|
ANNOTATION_REPO_NAME,
|
| 247 |
|
- |
notes::DEFAULT_NAMESPACE,
|
|
229 |
+ |
&namespace,
|
| 248 |
230 |
|
)
|
| 249 |
231 |
|
.await;
|
| 250 |
232 |
|
|
| 251 |
233 |
|
let query = if merged { "?annotation=merged" } else { "" };
|
| 252 |
234 |
|
Ok(Redirect::to(&format!(
|
| 253 |
|
- |
"/git/{owner}/{repo_name}/commit/{oid_str}{query}#annotation"
|
|
235 |
+ |
"/git/{owner}/{repo_name}/commit/{oid_str}{query}#annotations"
|
| 254 |
236 |
|
)))
|
| 255 |
237 |
|
}
|
| 256 |
238 |
|
|