Prepare Gotcha 1.0 for release
This commit is contained in:
@@ -187,7 +187,7 @@ pub fn parse_api_date(value: &str) -> Option<i64> {
|
||||
(civil_from_days(days) == (year, month, day)).then_some(days * 86_400)
|
||||
}
|
||||
|
||||
fn civil_from_days(days: i64) -> (i64, i64, i64) {
|
||||
pub fn civil_from_days(days: i64) -> (i64, i64, i64) {
|
||||
let days = days + 719_468;
|
||||
let era = days.div_euclid(146_097);
|
||||
let day_of_era = days - era * 146_097;
|
||||
@@ -202,7 +202,7 @@ fn civil_from_days(days: i64) -> (i64, i64, i64) {
|
||||
(year, month, day)
|
||||
}
|
||||
|
||||
fn days_from_civil(mut year: i64, month: i64, day: i64) -> i64 {
|
||||
pub fn days_from_civil(mut year: i64, month: i64, day: i64) -> i64 {
|
||||
year -= i64::from(month <= 2);
|
||||
let era = year.div_euclid(400);
|
||||
let year_of_era = year - era * 400;
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
CreateIssue, EditIssue, IssueDetails, IssueDraft, IssueEditorData, IssueQuery, Page,
|
||||
RepositoryId,
|
||||
},
|
||||
models,
|
||||
models, positive,
|
||||
};
|
||||
use gitea_openapi::apis;
|
||||
|
||||
@@ -460,111 +460,8 @@ pub fn comment_can_edit(comment: &models::Comment, viewer_id: Option<i64>) -> bo
|
||||
)
|
||||
}
|
||||
|
||||
fn create_issue_option(draft: IssueDraft) -> models::CreateIssueOption {
|
||||
models::CreateIssueOption {
|
||||
body: Some(draft.body),
|
||||
closed: Some(draft.closed),
|
||||
due_date: draft.due_date,
|
||||
labels: Some(draft.label_ids),
|
||||
milestone: draft.milestone_id,
|
||||
..models::CreateIssueOption::new(draft.title)
|
||||
}
|
||||
}
|
||||
|
||||
fn edit_issue_draft(draft: IssueDraft) -> EditIssue {
|
||||
EditIssue {
|
||||
option: models::EditIssueOption {
|
||||
body: Some(draft.body),
|
||||
due_date: draft.due_date.clone(),
|
||||
milestone: Some(draft.milestone_id.unwrap_or_default()),
|
||||
state: Some(if draft.closed { "closed" } else { "open" }.into()),
|
||||
title: Some(draft.title),
|
||||
unset_due_date: draft.due_date.is_none().then_some(true),
|
||||
..models::EditIssueOption::new()
|
||||
},
|
||||
replace_labels: Some(draft.label_ids),
|
||||
add_labels: Vec::new(),
|
||||
remove_labels: Vec::new(),
|
||||
add_assignees: Vec::new(),
|
||||
milestone_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn label_option(labels: &[i64]) -> models::IssueLabelsOption {
|
||||
models::IssueLabelsOption {
|
||||
labels: Some(
|
||||
labels
|
||||
.iter()
|
||||
.copied()
|
||||
.map(serde_json::Value::from)
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn comment_belongs_to_issue(comment: &models::Comment, number: i64) -> bool {
|
||||
comment
|
||||
.issue_url
|
||||
.as_deref()
|
||||
.map(|url| url.trim_end_matches('/'))
|
||||
.and_then(|url| url.rsplit('/').next())
|
||||
.and_then(|index| index.parse().ok())
|
||||
== Some(number)
|
||||
}
|
||||
|
||||
fn positive(value: i64, name: &str) -> Result<()> {
|
||||
if value < 1 {
|
||||
return Err(Error::InvalidInput(format!(
|
||||
"{name} must be a positive integer"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
mod helpers;
|
||||
use helpers::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn scopes_comments_and_builds_label_payloads() {
|
||||
let comment = models::Comment {
|
||||
issue_url: Some("https://example.test/api/v1/repos/a/b/issues/7".into()),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(comment_belongs_to_issue(&comment, 7));
|
||||
assert!(!comment_belongs_to_issue(&comment, 8));
|
||||
assert!(!comment_can_edit(&comment, Some(1)));
|
||||
let owned = models::Comment {
|
||||
id: Some(3),
|
||||
user: Some(Box::new(models::User {
|
||||
id: Some(1),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(comment_can_edit(&owned, Some(1)));
|
||||
assert_eq!(label_option(&[2, 4]).labels.unwrap().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_issue_drafts_without_losing_clear_operations() {
|
||||
let draft = IssueDraft {
|
||||
title: "Title".into(),
|
||||
body: "Body".into(),
|
||||
label_ids: vec![2, 4],
|
||||
milestone_id: None,
|
||||
due_date: None,
|
||||
closed: true,
|
||||
};
|
||||
let create = create_issue_option(draft.clone());
|
||||
assert_eq!(create.title, "Title");
|
||||
assert_eq!(create.body.as_deref(), Some("Body"));
|
||||
assert_eq!(create.closed, Some(true));
|
||||
assert_eq!(create.labels, Some(vec![2, 4]));
|
||||
let edit = edit_issue_draft(draft);
|
||||
assert_eq!(edit.option.milestone, Some(0));
|
||||
assert_eq!(edit.option.state.as_deref(), Some("closed"));
|
||||
assert_eq!(edit.option.unset_due_date, Some(true));
|
||||
assert_eq!(edit.replace_labels, Some(vec![2, 4]));
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
|
||||
56
crates/gitea/src/issues/helpers.rs
Normal file
56
crates/gitea/src/issues/helpers.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use crate::{
|
||||
domain::{EditIssue, IssueDraft},
|
||||
models,
|
||||
};
|
||||
|
||||
pub(super) fn create_issue_option(draft: IssueDraft) -> models::CreateIssueOption {
|
||||
models::CreateIssueOption {
|
||||
body: Some(draft.body),
|
||||
closed: Some(draft.closed),
|
||||
due_date: draft.due_date,
|
||||
labels: Some(draft.label_ids),
|
||||
milestone: draft.milestone_id,
|
||||
..models::CreateIssueOption::new(draft.title)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn edit_issue_draft(draft: IssueDraft) -> EditIssue {
|
||||
EditIssue {
|
||||
option: models::EditIssueOption {
|
||||
body: Some(draft.body),
|
||||
due_date: draft.due_date.clone(),
|
||||
milestone: Some(draft.milestone_id.unwrap_or_default()),
|
||||
state: Some(if draft.closed { "closed" } else { "open" }.into()),
|
||||
title: Some(draft.title),
|
||||
unset_due_date: draft.due_date.is_none().then_some(true),
|
||||
..models::EditIssueOption::new()
|
||||
},
|
||||
replace_labels: Some(draft.label_ids),
|
||||
add_labels: Vec::new(),
|
||||
remove_labels: Vec::new(),
|
||||
add_assignees: Vec::new(),
|
||||
milestone_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn label_option(labels: &[i64]) -> models::IssueLabelsOption {
|
||||
models::IssueLabelsOption {
|
||||
labels: Some(
|
||||
labels
|
||||
.iter()
|
||||
.copied()
|
||||
.map(serde_json::Value::from)
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn comment_belongs_to_issue(comment: &models::Comment, number: i64) -> bool {
|
||||
comment
|
||||
.issue_url
|
||||
.as_deref()
|
||||
.map(|url| url.trim_end_matches('/'))
|
||||
.and_then(|url| url.rsplit('/').next())
|
||||
.and_then(|index| index.parse().ok())
|
||||
== Some(number)
|
||||
}
|
||||
44
crates/gitea/src/issues/tests.rs
Normal file
44
crates/gitea/src/issues/tests.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn scopes_comments_and_builds_label_payloads() {
|
||||
let comment = models::Comment {
|
||||
issue_url: Some("https://example.test/api/v1/repos/a/b/issues/7".into()),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(comment_belongs_to_issue(&comment, 7));
|
||||
assert!(!comment_belongs_to_issue(&comment, 8));
|
||||
assert!(!comment_can_edit(&comment, Some(1)));
|
||||
let owned = models::Comment {
|
||||
id: Some(3),
|
||||
user: Some(Box::new(models::User {
|
||||
id: Some(1),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(comment_can_edit(&owned, Some(1)));
|
||||
assert_eq!(label_option(&[2, 4]).labels.unwrap().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_issue_drafts_without_losing_clear_operations() {
|
||||
let draft = IssueDraft {
|
||||
title: "Title".into(),
|
||||
body: "Body".into(),
|
||||
label_ids: vec![2, 4],
|
||||
milestone_id: None,
|
||||
due_date: None,
|
||||
closed: true,
|
||||
};
|
||||
let create = create_issue_option(draft.clone());
|
||||
assert_eq!(create.title, "Title");
|
||||
assert_eq!(create.body.as_deref(), Some("Body"));
|
||||
assert_eq!(create.closed, Some(true));
|
||||
assert_eq!(create.labels, Some(vec![2, 4]));
|
||||
let edit = edit_issue_draft(draft);
|
||||
assert_eq!(edit.option.milestone, Some(0));
|
||||
assert_eq!(edit.option.state.as_deref(), Some("closed"));
|
||||
assert_eq!(edit.option.unset_due_date, Some(true));
|
||||
assert_eq!(edit.replace_labels, Some(vec![2, 4]));
|
||||
}
|
||||
@@ -23,13 +23,31 @@ pub use activity::ActivityFilter;
|
||||
pub use domain::{
|
||||
CreateIssue, DEFAULT_PAGE_SIZE, EditIssue, HistoryCommit, HomeData, IssueDetails, IssueDraft,
|
||||
IssueEditorData, IssueQuery, MilestoneDetails, MilestoneDraft, Page, PullDetails, RepositoryId,
|
||||
api_date, parse_api_date,
|
||||
api_date, civil_from_days, days_from_civil, parse_api_date,
|
||||
};
|
||||
pub use issues::comment_can_edit;
|
||||
pub use pulls::{PullFileSource, pull_file_source, pull_state};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub(crate) fn validate_page(page: i32, limit: i32) -> Result<()> {
|
||||
if page < 1 || limit < 1 {
|
||||
return Err(Error::InvalidInput(
|
||||
"page and limit must be positive".into(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn positive(value: i64, name: &str) -> Result<()> {
|
||||
if value < 1 {
|
||||
return Err(Error::InvalidInput(format!(
|
||||
"{name} must be a positive integer"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Configuration(String),
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::collections::BTreeSet;
|
||||
use crate::{
|
||||
Client, Error, Result,
|
||||
domain::{DEFAULT_PAGE_SIZE, Page, PullDetails, RepositoryId},
|
||||
models,
|
||||
models, positive, validate_page,
|
||||
};
|
||||
use gitea_openapi::apis;
|
||||
|
||||
@@ -323,21 +323,3 @@ impl Client {
|
||||
.map_err(Error::generated)
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_page(page: i32, limit: i32) -> Result<()> {
|
||||
if page < 1 || limit < 1 {
|
||||
return Err(Error::InvalidInput(
|
||||
"page and limit must be positive".into(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn positive(value: i64, name: &str) -> Result<()> {
|
||||
if value < 1 {
|
||||
return Err(Error::InvalidInput(format!(
|
||||
"{name} must be a positive integer"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use tokio::task::JoinSet;
|
||||
use crate::{
|
||||
Client, Error, Result,
|
||||
domain::{DEFAULT_PAGE_SIZE, HistoryCommit, Page, RepositoryId},
|
||||
models,
|
||||
models, validate_page,
|
||||
};
|
||||
use gitea_openapi::apis;
|
||||
|
||||
@@ -481,112 +481,5 @@ fn commit_parents(commit: &models::Commit) -> impl Iterator<Item = &str> {
|
||||
.filter_map(|parent| parent.sha.as_deref())
|
||||
}
|
||||
|
||||
fn validate_page(page: i32, limit: i32) -> Result<()> {
|
||||
if page < 1 || limit < 1 {
|
||||
return Err(Error::InvalidInput(
|
||||
"page and limit must be positive".into(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn commit(sha: &str, parents: &[&str]) -> models::Commit {
|
||||
models::Commit {
|
||||
sha: Some(sha.into()),
|
||||
parents: Some(
|
||||
parents
|
||||
.iter()
|
||||
.map(|sha| models::CommitMeta {
|
||||
sha: Some((*sha).into()),
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lays_out_shared_commit_graph() {
|
||||
let rows = build_graph(
|
||||
vec![
|
||||
(
|
||||
0,
|
||||
"main".into(),
|
||||
vec![
|
||||
commit("merge", &["left", "right"]),
|
||||
commit("left", &["base"]),
|
||||
commit("base", &[]),
|
||||
],
|
||||
),
|
||||
(
|
||||
1,
|
||||
"feature".into(),
|
||||
vec![commit("right", &["base"]), commit("base", &[])],
|
||||
),
|
||||
],
|
||||
PullMetadata::default(),
|
||||
);
|
||||
assert_eq!(rows.len(), 4);
|
||||
assert!(rows.iter().any(|row| row.refs == ["main"]));
|
||||
assert!(rows.iter().any(|row| row.refs == ["feature"]));
|
||||
let merge = rows
|
||||
.iter()
|
||||
.find(|row| row.commit.sha.as_deref() == Some("merge"))
|
||||
.unwrap();
|
||||
assert_eq!(merge.node_lane, Some(0));
|
||||
assert_eq!(merge.bottom_connections, [1]);
|
||||
let right = rows
|
||||
.iter()
|
||||
.find(|row| row.commit.sha.as_deref() == Some("right"))
|
||||
.unwrap();
|
||||
assert_eq!(right.node_lane, Some(1));
|
||||
let base = rows
|
||||
.iter()
|
||||
.find(|row| row.commit.sha.as_deref() == Some("base"))
|
||||
.unwrap();
|
||||
assert_eq!(base.node_lane, Some(0));
|
||||
assert_eq!(base.top_connections, [1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn labels_first_commit_of_pull_branch() {
|
||||
let rows = build_graph(
|
||||
vec![(
|
||||
0,
|
||||
"main".into(),
|
||||
vec![
|
||||
commit("merge", &["main", "head"]),
|
||||
commit("head", &["first"]),
|
||||
commit("first", &["base"]),
|
||||
commit("main", &["base"]),
|
||||
commit("base", &[]),
|
||||
],
|
||||
)],
|
||||
PullMetadata {
|
||||
tips: HashMap::from([("head".into(), vec!["feature".into()])]),
|
||||
starts: vec![PullBranch {
|
||||
head: "head".into(),
|
||||
base: "base".into(),
|
||||
label: "feature".into(),
|
||||
}],
|
||||
},
|
||||
);
|
||||
let first = rows
|
||||
.iter()
|
||||
.find(|row| row.commit.sha.as_deref() == Some("first"))
|
||||
.unwrap();
|
||||
assert_eq!(first.branch_starts, ["feature"]);
|
||||
assert!(
|
||||
rows.iter()
|
||||
.find(|row| row.commit.sha.as_deref() == Some("head"))
|
||||
.unwrap()
|
||||
.refs
|
||||
.contains(&"feature".into())
|
||||
);
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
|
||||
97
crates/gitea/src/repositories/tests.rs
Normal file
97
crates/gitea/src/repositories/tests.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use super::*;
|
||||
|
||||
fn commit(sha: &str, parents: &[&str]) -> models::Commit {
|
||||
models::Commit {
|
||||
sha: Some(sha.into()),
|
||||
parents: Some(
|
||||
parents
|
||||
.iter()
|
||||
.map(|sha| models::CommitMeta {
|
||||
sha: Some((*sha).into()),
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lays_out_shared_commit_graph() {
|
||||
let rows = build_graph(
|
||||
vec![
|
||||
(
|
||||
0,
|
||||
"main".into(),
|
||||
vec![
|
||||
commit("merge", &["left", "right"]),
|
||||
commit("left", &["base"]),
|
||||
commit("base", &[]),
|
||||
],
|
||||
),
|
||||
(
|
||||
1,
|
||||
"feature".into(),
|
||||
vec![commit("right", &["base"]), commit("base", &[])],
|
||||
),
|
||||
],
|
||||
PullMetadata::default(),
|
||||
);
|
||||
assert_eq!(rows.len(), 4);
|
||||
assert!(rows.iter().any(|row| row.refs == ["main"]));
|
||||
assert!(rows.iter().any(|row| row.refs == ["feature"]));
|
||||
let merge = rows
|
||||
.iter()
|
||||
.find(|row| row.commit.sha.as_deref() == Some("merge"))
|
||||
.unwrap();
|
||||
assert_eq!(merge.node_lane, Some(0));
|
||||
assert_eq!(merge.bottom_connections, [1]);
|
||||
let right = rows
|
||||
.iter()
|
||||
.find(|row| row.commit.sha.as_deref() == Some("right"))
|
||||
.unwrap();
|
||||
assert_eq!(right.node_lane, Some(1));
|
||||
let base = rows
|
||||
.iter()
|
||||
.find(|row| row.commit.sha.as_deref() == Some("base"))
|
||||
.unwrap();
|
||||
assert_eq!(base.node_lane, Some(0));
|
||||
assert_eq!(base.top_connections, [1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn labels_first_commit_of_pull_branch() {
|
||||
let rows = build_graph(
|
||||
vec![(
|
||||
0,
|
||||
"main".into(),
|
||||
vec![
|
||||
commit("merge", &["main", "head"]),
|
||||
commit("head", &["first"]),
|
||||
commit("first", &["base"]),
|
||||
commit("main", &["base"]),
|
||||
commit("base", &[]),
|
||||
],
|
||||
)],
|
||||
PullMetadata {
|
||||
tips: HashMap::from([("head".into(), vec!["feature".into()])]),
|
||||
starts: vec![PullBranch {
|
||||
head: "head".into(),
|
||||
base: "base".into(),
|
||||
label: "feature".into(),
|
||||
}],
|
||||
},
|
||||
);
|
||||
let first = rows
|
||||
.iter()
|
||||
.find(|row| row.commit.sha.as_deref() == Some("first"))
|
||||
.unwrap();
|
||||
assert_eq!(first.branch_starts, ["feature"]);
|
||||
assert!(
|
||||
rows.iter()
|
||||
.find(|row| row.commit.sha.as_deref() == Some("head"))
|
||||
.unwrap()
|
||||
.refs
|
||||
.contains(&"feature".into())
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user