summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2025-02-06 00:05:57 +0100
committerJoel Klinghed <the_jk@spawned.biz>2025-02-06 00:05:57 +0100
commitbd74717e10fb36e19893c15941876b2383b94714 (patch)
tree5fc26ace64c37fb439ba94cc5ea437a0e93913d2
parent350fc534de745f4cc62000fa25d67afcddb7918a (diff)
Add DELETE command for review
Only the owner or a maintainer of the project can remove a review. Removing a review also removes the git branch. Only reviews that are either draft or dropped can be removed.
-rw-r--r--server/.sqlx/query-7715484cf01394aad53517f6e68a116e9f1b67bc75eb459a45d18f9c67e03e34.json47
-rw-r--r--server/.sqlx/query-d951a84ad9d38b745f12217a31200667ca4d499d06b37e2ae9c36e65c0eb3c49.json12
-rw-r--r--server/.sqlx/query-e53285109ec191077d218ec990020165f44b8432d9c2583c9f14cd7d7c2e37bb.json47
-rw-r--r--server/common/src/git.rs13
-rw-r--r--server/common/src/tests.rs19
-rw-r--r--server/src/git_root.rs14
-rw-r--r--server/src/main.rs176
7 files changed, 327 insertions, 1 deletions
diff --git a/server/.sqlx/query-7715484cf01394aad53517f6e68a116e9f1b67bc75eb459a45d18f9c67e03e34.json b/server/.sqlx/query-7715484cf01394aad53517f6e68a116e9f1b67bc75eb459a45d18f9c67e03e34.json
new file mode 100644
index 0000000..1cf179c
--- /dev/null
+++ b/server/.sqlx/query-7715484cf01394aad53517f6e68a116e9f1b67bc75eb459a45d18f9c67e03e34.json
@@ -0,0 +1,47 @@
+{
+ "db_name": "MySQL",
+ "query": "SELECT state,branch,owner FROM reviews WHERE project=? AND id=?",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "state",
+ "type_info": {
+ "type": "Tiny",
+ "flags": "NOT_NULL | UNSIGNED",
+ "char_set": 63,
+ "max_size": 3
+ }
+ },
+ {
+ "ordinal": 1,
+ "name": "branch",
+ "type_info": {
+ "type": "VarString",
+ "flags": "NOT_NULL | NO_DEFAULT_VALUE",
+ "char_set": 224,
+ "max_size": 4096
+ }
+ },
+ {
+ "ordinal": 2,
+ "name": "owner",
+ "type_info": {
+ "type": "VarString",
+ "flags": "NOT_NULL | MULTIPLE_KEY | NO_DEFAULT_VALUE",
+ "char_set": 224,
+ "max_size": 512
+ }
+ }
+ ],
+ "parameters": {
+ "Right": 2
+ },
+ "nullable": [
+ false,
+ false,
+ false
+ ]
+ },
+ "hash": "7715484cf01394aad53517f6e68a116e9f1b67bc75eb459a45d18f9c67e03e34"
+}
diff --git a/server/.sqlx/query-d951a84ad9d38b745f12217a31200667ca4d499d06b37e2ae9c36e65c0eb3c49.json b/server/.sqlx/query-d951a84ad9d38b745f12217a31200667ca4d499d06b37e2ae9c36e65c0eb3c49.json
new file mode 100644
index 0000000..89cb878
--- /dev/null
+++ b/server/.sqlx/query-d951a84ad9d38b745f12217a31200667ca4d499d06b37e2ae9c36e65c0eb3c49.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "MySQL",
+ "query": "DELETE FROM reviews WHERE project=? AND id=?",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 2
+ },
+ "nullable": []
+ },
+ "hash": "d951a84ad9d38b745f12217a31200667ca4d499d06b37e2ae9c36e65c0eb3c49"
+}
diff --git a/server/.sqlx/query-e53285109ec191077d218ec990020165f44b8432d9c2583c9f14cd7d7c2e37bb.json b/server/.sqlx/query-e53285109ec191077d218ec990020165f44b8432d9c2583c9f14cd7d7c2e37bb.json
new file mode 100644
index 0000000..f04a93c
--- /dev/null
+++ b/server/.sqlx/query-e53285109ec191077d218ec990020165f44b8432d9c2583c9f14cd7d7c2e37bb.json
@@ -0,0 +1,47 @@
+{
+ "db_name": "MySQL",
+ "query": "SELECT id,state,owner FROM reviews WHERE project=? AND branch=?",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": {
+ "type": "LongLong",
+ "flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
+ "char_set": 63,
+ "max_size": 20
+ }
+ },
+ {
+ "ordinal": 1,
+ "name": "state",
+ "type_info": {
+ "type": "Tiny",
+ "flags": "NOT_NULL | UNSIGNED",
+ "char_set": 63,
+ "max_size": 3
+ }
+ },
+ {
+ "ordinal": 2,
+ "name": "owner",
+ "type_info": {
+ "type": "VarString",
+ "flags": "NOT_NULL | MULTIPLE_KEY | NO_DEFAULT_VALUE",
+ "char_set": 224,
+ "max_size": 512
+ }
+ }
+ ],
+ "parameters": {
+ "Right": 2
+ },
+ "nullable": [
+ false,
+ false,
+ false
+ ]
+ },
+ "hash": "e53285109ec191077d218ec990020165f44b8432d9c2583c9f14cd7d7c2e37bb"
+}
diff --git a/server/common/src/git.rs b/server/common/src/git.rs
index 74c3247..ac0bdb3 100644
--- a/server/common/src/git.rs
+++ b/server/common/src/git.rs
@@ -326,6 +326,12 @@ impl RepoData {
.await
}
+ async fn delete_branch(&self, repo: &Repository, branch: &str) -> Result<(), Error> {
+ let mut cmd = self.git_cmd(repo);
+ cmd.arg("branch").arg("--delete").arg("--force").arg(branch);
+ self.run(&mut cmd).await
+ }
+
async fn get_log_format(
&self,
repo: &Repository,
@@ -564,4 +570,11 @@ impl Repository {
data.get_commiter(self, commit.as_str()).await
}
+
+ pub async fn delete_branch(&self, branch: impl Into<String>) -> Result<(), Error> {
+ let branch = branch.into();
+ let data = self.lock.read().await;
+
+ data.delete_branch(self, branch.as_str()).await
+ }
}
diff --git a/server/common/src/tests.rs b/server/common/src/tests.rs
index f08ca44..41f44fe 100644
--- a/server/common/src/tests.rs
+++ b/server/common/src/tests.rs
@@ -202,7 +202,7 @@ async fn git_get_author_commiter(repo: &git::Repository) {
assert!(repo.get_author("<invalid>").await.is_err());
}
-async fn git_fetch(bare: bool) {
+async fn git_fetch(bare: bool) -> git::Repository {
let path = testdir!().join("repo");
let remote_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/testdata/bare");
let remote = remote_path.to_string_lossy().into_owned();
@@ -231,6 +231,7 @@ async fn git_fetch(bare: bool) {
branch.unwrap();
other.unwrap();
}
+ repo
}
#[tokio::test]
@@ -266,3 +267,19 @@ async fn test_git_fetch() {
async fn test_git_bare_fetch() {
git_fetch(true).await;
}
+
+#[tokio::test]
+async fn test_git_delete_branch() {
+ // Using git_fetch as we need a writeable git repo
+ let repo = git_fetch(false).await;
+ assert!(repo.delete_branch("other").await.is_ok());
+ assert!(repo.delete_branch("does-not-exist").await.is_err());
+}
+
+#[tokio::test]
+async fn test_git_bare_delete_branch() {
+ // Using git_fetch as we need a writeable git repo
+ let repo = git_fetch(true).await;
+ assert!(repo.delete_branch("other").await.is_ok());
+ assert!(repo.delete_branch("does-not-exist").await.is_err());
+}
diff --git a/server/src/git_root.rs b/server/src/git_root.rs
index 31e4d45..f818495 100644
--- a/server/src/git_root.rs
+++ b/server/src/git_root.rs
@@ -62,6 +62,20 @@ impl Roots {
Ok(())
}
+
+ pub async fn del_branch(&self, project_id: &str, branch: &str) -> Result<(), git::Error> {
+ let repo;
+ {
+ let data = self.data.lock().unwrap();
+ if let Some(tmp_repo) = data.project_repo.get(project_id) {
+ repo = tmp_repo.clone();
+ } else {
+ return Ok(());
+ }
+ }
+
+ repo.delete_branch(branch).await
+ }
}
#[derive(Debug)]
diff --git a/server/src/main.rs b/server/src/main.rs
index 019f28f..f07c372 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -46,6 +46,8 @@ struct Db(sqlx::MySqlPool);
reviews,
review,
review_id,
+ review_del,
+ review_id_del,
users,
user_key_add,
user_key_get,
@@ -633,6 +635,114 @@ async fn get_review_from_id(
Ok(Json(review))
}
+#[allow(clippy::too_many_arguments)]
+async fn del_review(
+ mut db: Connection<Db>,
+ roots: &State<git_root::Roots>,
+ session: auth::Session,
+ projectid: &str,
+ reviewid: u64,
+ state: u8,
+ owner: &str,
+ branch: &str,
+) -> Result<&'static str, Custom<&'static str>> {
+ let state = api_model::ReviewState::try_from(state).unwrap();
+
+ if owner != session.user_id
+ && project_check_maintainer(&mut db, session, projectid)
+ .await
+ .is_err()
+ {
+ return Err(Custom(
+ Status::Unauthorized,
+ "Not owner of review or maintainer of project",
+ ));
+ }
+
+ match state {
+ api_model::ReviewState::Draft | api_model::ReviewState::Dropped => {}
+ api_model::ReviewState::Open | api_model::ReviewState::Closed => {
+ return Err(Custom(Status::BadRequest, "Review is open or closed"));
+ }
+ }
+
+ roots
+ .del_branch(projectid, branch)
+ .map_err(|_| Custom(Status::InternalServerError, "git error"))
+ .await?;
+
+ sqlx::query!(
+ "DELETE FROM reviews WHERE project=? AND id=?",
+ projectid,
+ reviewid
+ )
+ .execute(&mut **db)
+ .await
+ .unwrap();
+
+ Ok("")
+}
+
+async fn del_review_from_branch(
+ mut db: Connection<Db>,
+ roots: &State<git_root::Roots>,
+ session: auth::Session,
+ projectid: &str,
+ branch: &str,
+) -> Result<&'static str, Custom<&'static str>> {
+ let (id, state, owner) = sqlx::query!(
+ "SELECT id,state,owner FROM reviews WHERE project=? AND branch=?",
+ projectid,
+ branch
+ )
+ .fetch_one(&mut **db)
+ .map_ok(|r| (r.id, r.state, r.owner))
+ .map_err(|_| Custom(Status::NotFound, "No such review"))
+ .await?;
+
+ del_review(
+ db,
+ roots,
+ session,
+ projectid,
+ id,
+ state,
+ owner.as_str(),
+ branch,
+ )
+ .await
+}
+
+async fn del_review_from_id(
+ mut db: Connection<Db>,
+ roots: &State<git_root::Roots>,
+ session: auth::Session,
+ projectid: &str,
+ reviewid: u64,
+) -> Result<&'static str, Custom<&'static str>> {
+ let (state, branch, owner) = sqlx::query!(
+ "SELECT state,branch,owner FROM reviews WHERE project=? AND id=?",
+ projectid,
+ reviewid
+ )
+ .fetch_one(&mut **db)
+ .map_ok(|r| (r.state, r.branch, r.owner))
+ .map_err(|_| Custom(Status::NotFound, "No such review"))
+ .await?;
+
+ del_review(
+ db,
+ roots,
+ session,
+ projectid,
+ reviewid,
+ state,
+ owner.as_str(),
+ branch.as_str(),
+ )
+ .await
+}
+
#[utoipa::path(
responses(
(status = 200, description = "Get review", body = api_model::Review),
@@ -665,6 +775,47 @@ async fn review_encoded_path(
#[utoipa::path(
responses(
+ (status = 200, description = "Review deleted"),
+ (status = 400, description = "Review is open or closed"),
+ (status = 401, description = "Not owner of review or maintainer of project"),
+ (status = 404, description = "No such review"),
+ ),
+ security(
+ ("session" = []),
+ ),
+)]
+#[delete("/review/<projectid>/<branch..>")]
+async fn review_del(
+ db: Connection<Db>,
+ roots: &State<git_root::Roots>,
+ session: auth::Session,
+ projectid: &str,
+ branch: PathBuf,
+) -> Result<&'static str, Custom<&'static str>> {
+ del_review_from_branch(
+ db,
+ roots,
+ session,
+ projectid,
+ branch.as_path().to_str().unwrap(),
+ )
+ .await
+}
+
+// Backup for the above. Matches if <branch> ends up encoded (with / as %2f)
+#[delete("/review/<projectid>/<branch>", rank = 1)]
+async fn review_encoded_path_del(
+ db: Connection<Db>,
+ roots: &State<git_root::Roots>,
+ session: auth::Session,
+ projectid: &str,
+ branch: &str,
+) -> Result<&'static str, Custom<&'static str>> {
+ del_review_from_branch(db, roots, session, projectid, branch).await
+}
+
+#[utoipa::path(
+ responses(
(status = 200, description = "Get review", body = api_model::Review),
(status = 404, description = "No such review"),
),
@@ -684,6 +835,28 @@ async fn review_id(
#[utoipa::path(
responses(
+ (status = 200, description = "Remove deleted"),
+ (status = 400, description = "Review is open or closed"),
+ (status = 401, description = "Not owner of review or maintainer of project"),
+ (status = 404, description = "No such review"),
+ ),
+ security(
+ ("session" = []),
+ ),
+)]
+#[delete("/review/<projectid>?<reviewid>")]
+async fn review_id_del(
+ db: Connection<Db>,
+ roots: &State<git_root::Roots>,
+ session: auth::Session,
+ projectid: &str,
+ reviewid: u64,
+) -> Result<&'static str, Custom<&'static str>> {
+ del_review_from_id(db, roots, session, projectid, reviewid).await
+}
+
+#[utoipa::path(
+ responses(
(status = 200, description = "Get all users", body = api_model::Users),
),
security(
@@ -941,8 +1114,11 @@ fn rocket_from_config(figment: Figment) -> Rocket<Build> {
project_user_del,
reviews,
review,
+ review_del,
review_encoded_path,
+ review_encoded_path_del,
review_id,
+ review_id_del,
users,
user_key_add,
user_key_get,