| 4 |
4 |
|
use sqlx::{FromRow, PgPool};
|
| 5 |
5 |
|
|
| 6 |
6 |
|
use super::models::DbGitRepo;
|
| 7 |
|
- |
use super::{GitRepoId, ProjectId, UserId, Visibility};
|
|
7 |
+ |
use super::{GitRepoId, GitRepoKind, ProjectId, UserId, Visibility};
|
| 8 |
8 |
|
use crate::error::Result;
|
| 9 |
9 |
|
|
| 10 |
10 |
|
/// A public repo joined with its owner's username, for the explore page.
|
| 23 |
23 |
|
/// this with names supplied by untrusted remote git clients.
|
| 24 |
24 |
|
#[tracing::instrument(skip_all)]
|
| 25 |
25 |
|
pub async fn create_repo(pool: &PgPool, user_id: UserId, name: &str) -> Result<DbGitRepo> {
|
| 26 |
|
- |
crate::validation::validate_git_repo_name(name)?;
|
|
26 |
+ |
crate::validation::validate_creatable_repo_name(name)?;
|
| 27 |
27 |
|
let repo = sqlx::query_as::<_, DbGitRepo>(
|
| 28 |
28 |
|
r"
|
| 29 |
29 |
|
INSERT INTO git_repos (user_id, name)
|
| 47 |
47 |
|
name: &str,
|
| 48 |
48 |
|
visibility: Visibility,
|
| 49 |
49 |
|
) -> Result<DbGitRepo> {
|
| 50 |
|
- |
crate::validation::validate_git_repo_name(name)?;
|
|
50 |
+ |
crate::validation::validate_creatable_repo_name(name)?;
|
| 51 |
51 |
|
let repo = sqlx::query_as::<_, DbGitRepo>(
|
| 52 |
52 |
|
r"
|
| 53 |
53 |
|
INSERT INTO git_repos (user_id, name, visibility)
|
| 64 |
64 |
|
Ok(repo)
|
| 65 |
65 |
|
}
|
| 66 |
66 |
|
|
|
67 |
+ |
/// The account's annotation repository, if it has one yet.
|
|
68 |
+ |
#[tracing::instrument(skip_all)]
|
|
69 |
+ |
pub async fn get_annotation_repo(pool: &PgPool, user_id: UserId) -> Result<Option<DbGitRepo>> {
|
|
70 |
+ |
let repo = sqlx::query_as::<_, DbGitRepo>(
|
|
71 |
+ |
"SELECT * FROM git_repos WHERE user_id = $1 AND kind = 'annotations'",
|
|
72 |
+ |
)
|
|
73 |
+ |
.bind(user_id)
|
|
74 |
+ |
.fetch_optional(pool)
|
|
75 |
+ |
.await?;
|
|
76 |
+ |
|
|
77 |
+ |
Ok(repo)
|
|
78 |
+ |
}
|
|
79 |
+ |
|
|
80 |
+ |
/// Register the account's annotation repository.
|
|
81 |
+ |
///
|
|
82 |
+ |
/// Private and `kind = 'annotations'` in one statement rather than an insert
|
|
83 |
+ |
/// plus two updates: a row that is public for the width of a transaction is a
|
|
84 |
+ |
/// row that can be read. `ON CONFLICT DO NOTHING` plus a re-select, so two
|
|
85 |
+ |
/// first-annotations racing each other both end up with the row the winner
|
|
86 |
+ |
/// made.
|
|
87 |
+ |
///
|
|
88 |
+ |
/// Deliberately does not call `validate_creatable_repo_name`: the name it
|
|
89 |
+ |
/// writes is the reserved one, and this is the code the reservation exists for.
|
|
90 |
+ |
#[tracing::instrument(skip_all)]
|
|
91 |
+ |
pub async fn create_annotation_repo(pool: &PgPool, user_id: UserId) -> Result<DbGitRepo> {
|
|
92 |
+ |
let inserted = sqlx::query_as::<_, DbGitRepo>(
|
|
93 |
+ |
r"
|
|
94 |
+ |
INSERT INTO git_repos (user_id, name, visibility, kind, description)
|
|
95 |
+ |
VALUES ($1, $2, 'private', 'annotations', $3)
|
|
96 |
+ |
ON CONFLICT (user_id, name) DO NOTHING
|
|
97 |
+ |
RETURNING *
|
|
98 |
+ |
",
|
|
99 |
+ |
)
|
|
100 |
+ |
.bind(user_id)
|
|
101 |
+ |
.bind(crate::constants::ANNOTATION_REPO_NAME)
|
|
102 |
+ |
.bind(ANNOTATION_REPO_DESCRIPTION)
|
|
103 |
+ |
.fetch_optional(pool)
|
|
104 |
+ |
.await?;
|
|
105 |
+ |
|
|
106 |
+ |
if let Some(repo) = inserted {
|
|
107 |
+ |
return Ok(repo);
|
|
108 |
+ |
}
|
|
109 |
+ |
|
|
110 |
+ |
// Somebody else inserted it, or the account already owns a repository of
|
|
111 |
+ |
// that name. Either way the row that is there is the answer.
|
|
112 |
+ |
get_repo_by_user_and_name(pool, user_id, crate::constants::ANNOTATION_REPO_NAME)
|
|
113 |
+ |
.await?
|
|
114 |
+ |
.ok_or_else(|| {
|
|
115 |
+ |
crate::error::AppError::Internal(anyhow::anyhow!(
|
|
116 |
+ |
"annotation repo insert conflicted but no row is there"
|
|
117 |
+ |
))
|
|
118 |
+ |
})
|
|
119 |
+ |
}
|
|
120 |
+ |
|
|
121 |
+ |
/// What the annotation repository says on its own page.
|
|
122 |
+ |
pub const ANNOTATION_REPO_DESCRIPTION: &str =
|
|
123 |
+ |
"Personal annotations. Notes on commits across makenot.work, private to this account.";
|
|
124 |
+ |
|
|
125 |
+ |
/// Whether this repository is the account's annotation store, which is private
|
|
126 |
+ |
/// permanently: publishing a set of annotations is a moderation and consent
|
|
127 |
+ |
/// decision, not a toggle.
|
|
128 |
+ |
pub fn is_annotation_repo(repo: &DbGitRepo) -> bool {
|
|
129 |
+ |
repo.kind == GitRepoKind::Annotations
|
|
130 |
+ |
}
|
|
131 |
+ |
|
| 67 |
132 |
|
/// Look up a repo by its primary key. Returns `None` if not found.
|
| 68 |
133 |
|
#[tracing::instrument(skip_all)]
|
| 69 |
134 |
|
pub async fn get_repo_by_id(pool: &PgPool, repo_id: GitRepoId) -> Result<Option<DbGitRepo>> {
|
| 259 |
324 |
|
|
| 260 |
325 |
|
Ok(rows)
|
| 261 |
326 |
|
}
|
|
327 |
+ |
|
|
328 |
+ |
#[cfg(test)]
|
|
329 |
+ |
mod tests {
|
|
330 |
+ |
#[test]
|
|
331 |
+ |
fn annotation_repo_name_is_a_legal_repo_name_and_reserved() {
|
|
332 |
+ |
let name = crate::constants::ANNOTATION_REPO_NAME;
|
|
333 |
+ |
assert!(crate::validation::validate_git_repo_name(name).is_ok());
|
|
334 |
+ |
assert!(crate::validation::is_reserved_repo_name(name));
|
|
335 |
+ |
assert!(crate::validation::validate_creatable_repo_name(name).is_err());
|
|
336 |
+ |
}
|
|
337 |
+ |
}
|