use guts_storage::ObjectId;
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::{CollaborationError, Label, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PullRequestState {
Open,
Closed,
Merged,
}
impl std::fmt::Display for PullRequestState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PullRequestState::Open => write!(f, "open"),
PullRequestState::Closed => write!(f, "closed"),
PullRequestState::Merged => write!(f, "merged"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PullRequest {
pub id: u64,
pub repo_key: String,
pub number: u32,
pub title: String,
pub description: String,
pub author: String,
pub state: PullRequestState,
pub source_branch: String,
pub target_branch: String,
pub source_commit: ObjectId,
pub target_commit: ObjectId,
pub labels: Vec<Label>,
pub created_at: u64,
pub updated_at: u64,
pub merged_at: Option<u64>,
pub merged_by: Option<String>,
}
impl PullRequest {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: u64,
repo_key: impl Into<String>,
number: u32,
title: impl Into<String>,
description: impl Into<String>,
author: impl Into<String>,
source_branch: impl Into<String>,
target_branch: impl Into<String>,
source_commit: ObjectId,
target_commit: ObjectId,
) -> Self {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
Self {
id,
repo_key: repo_key.into(),
number,
title: title.into(),
description: description.into(),
author: author.into(),
state: PullRequestState::Open,
source_branch: source_branch.into(),
target_branch: target_branch.into(),
source_commit,
target_commit,
labels: Vec::new(),
created_at: now,
updated_at: now,
merged_at: None,
merged_by: None,
}
}
pub fn is_open(&self) -> bool {
self.state == PullRequestState::Open
}
pub fn is_merged(&self) -> bool {
self.state == PullRequestState::Merged
}
pub fn is_closed(&self) -> bool {
self.state == PullRequestState::Closed
}
pub fn close(&mut self) -> Result<()> {
if self.state == PullRequestState::Merged {
return Err(CollaborationError::InvalidStateTransition {
action: "close".to_string(),
current_state: self.state.to_string(),
});
}
self.state = PullRequestState::Closed;
self.updated_at = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
Ok(())
}
pub fn reopen(&mut self) -> Result<()> {
if self.state != PullRequestState::Closed {
return Err(CollaborationError::InvalidStateTransition {
action: "reopen".to_string(),
current_state: self.state.to_string(),
});
}
self.state = PullRequestState::Open;
self.updated_at = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
Ok(())
}
pub fn merge(&mut self, merged_by: impl Into<String>) -> Result<()> {
if self.state != PullRequestState::Open {
return Err(CollaborationError::InvalidStateTransition {
action: "merge".to_string(),
current_state: self.state.to_string(),
});
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
self.state = PullRequestState::Merged;
self.merged_at = Some(now);
self.merged_by = Some(merged_by.into());
self.updated_at = now;
Ok(())
}
pub fn update_title(&mut self, title: impl Into<String>) {
self.title = title.into();
self.updated_at = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
}
pub fn update_description(&mut self, description: impl Into<String>) {
self.description = description.into();
self.updated_at = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
}
pub fn add_label(&mut self, label: Label) {
if !self.labels.iter().any(|l| l.name == label.name) {
self.labels.push(label);
self.updated_at = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
}
}
pub fn remove_label(&mut self, name: &str) {
let before = self.labels.len();
self.labels.retain(|l| l.name != name);
if self.labels.len() != before {
self.updated_at = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
}
}
pub fn update_source_commit(&mut self, commit: ObjectId) {
self.source_commit = commit;
self.updated_at = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_pr() -> PullRequest {
PullRequest::new(
1,
"alice/repo",
1,
"Add feature X",
"This PR adds feature X",
"alice_pubkey",
"feature-x",
"main",
ObjectId::from_bytes([1u8; 20]),
ObjectId::from_bytes([2u8; 20]),
)
}
#[test]
fn test_pr_creation() {
let pr = create_test_pr();
assert_eq!(pr.number, 1);
assert_eq!(pr.title, "Add feature X");
assert!(pr.is_open());
assert!(!pr.is_merged());
assert!(!pr.is_closed());
}
#[test]
fn test_pr_close_and_reopen() {
let mut pr = create_test_pr();
pr.close().unwrap();
assert!(pr.is_closed());
assert!(!pr.is_open());
pr.reopen().unwrap();
assert!(pr.is_open());
assert!(!pr.is_closed());
}
#[test]
fn test_pr_merge() {
let mut pr = create_test_pr();
pr.merge("bob_pubkey").unwrap();
assert!(pr.is_merged());
assert!(!pr.is_open());
assert!(pr.merged_at.is_some());
assert_eq!(pr.merged_by, Some("bob_pubkey".to_string()));
}
#[test]
fn test_cannot_merge_closed_pr() {
let mut pr = create_test_pr();
pr.close().unwrap();
let result = pr.merge("bob");
assert!(result.is_err());
}
#[test]
fn test_cannot_close_merged_pr() {
let mut pr = create_test_pr();
pr.merge("bob").unwrap();
let result = pr.close();
assert!(result.is_err());
}
#[test]
fn test_labels() {
let mut pr = create_test_pr();
pr.add_label(Label::bug());
pr.add_label(Label::enhancement());
assert_eq!(pr.labels.len(), 2);
pr.add_label(Label::bug());
assert_eq!(pr.labels.len(), 2);
pr.remove_label("bug");
assert_eq!(pr.labels.len(), 1);
assert_eq!(pr.labels[0].name, "enhancement");
}
}