1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
//! Milestone operations: create, close, delete, assign, unassign.
use anyhow::Result;
use chrono::Utc;
use uuid::Uuid;
use crate::db::Database;
use super::core::{SharedWriter, WriteSet};
impl SharedWriter {
/// Create a milestone on the coordination branch.
///
/// Returns the assigned milestone display ID.
///
/// # Errors
/// Returns an error if writing or pushing to the coordination branch fails.
pub fn create_milestone(
&self,
db: &Database,
name: &str,
description: Option<&str>,
) -> Result<i64> {
let uuid = Uuid::new_v4();
let now = Utc::now();
let name_owned = name.to_string();
let desc_owned = description.map(std::string::ToString::to_string);
let _ = self.write_commit_push(
|_writer| {
let event = crate::events::Event::MilestoneCreated {
uuid,
display_id: None,
name: name_owned.clone(),
description: desc_owned.clone(),
created_at: now,
};
Ok(WriteSet {
events: vec![event],
})
},
&format!("create milestone: {name}"),
)?;
self.hydrate_with_retry(db);
// The milestone id is reduction-assigned (REQ-4); read it from the
// cached reduced state, falling back to a SQLite lookup when provisional.
if let Some(id) = self.v3_assigned_milestone_id(&uuid) {
return Ok(id);
}
db.get_milestone_id_by_uuid(&uuid.to_string())
}
/// Close a milestone on the coordination branch.
///
/// # Errors
/// Returns an error if the milestone cannot be loaded or the write fails.
pub fn close_milestone(&self, db: &Database, milestone_id: i64) -> Result<()> {
let _ = self.write_commit_push(
|writer| {
let entry = writer.load_milestone_by_id(milestone_id)?;
let closed_at = Utc::now();
let event = crate::events::Event::MilestoneClosed {
uuid: entry.uuid,
closed_at,
};
Ok(WriteSet {
events: vec![event],
})
},
&format!("close milestone #{milestone_id}"),
)?;
self.hydrate_with_retry(db);
Ok(())
}
/// Delete a milestone file from the coordination branch.
///
/// # Errors
/// Returns an error if the milestone cannot be loaded or the write fails.
pub fn delete_milestone(&self, db: &Database, milestone_id: i64) -> Result<()> {
let entry = self.load_milestone_by_id(milestone_id)?;
let _ = self.write_commit_push(
|_writer| {
let event = crate::events::Event::MilestoneDeleted { uuid: entry.uuid };
Ok(WriteSet {
events: vec![event],
})
},
&format!("delete milestone #{milestone_id}"),
)?;
self.hydrate_with_retry(db);
Ok(())
}
/// Set `milestone_uuid` on issue JSON files for the given issue IDs.
///
/// Loads the milestone to get its UUID, then patches each issue file.
/// Also adds the issues to the `SQLite` `milestone_issues` table via hydration.
///
/// # Errors
/// Returns an error if the milestone or any issue cannot be loaded, or the write fails.
pub fn set_milestone_on_issues(
&self,
db: &Database,
milestone_id: i64,
issue_ids: &[i64],
) -> Result<()> {
let milestone = self.load_milestone_by_id(milestone_id)?;
let ms_uuid = milestone.uuid;
let ids: Vec<i64> = issue_ids.to_vec();
let _ = self.write_commit_push(
|writer| {
let mut events = Vec::new();
for &issue_id in &ids {
let issue = writer.load_issue_by_id(issue_id, db)?;
events.push(crate::events::Event::MilestoneAssigned {
issue_uuid: issue.uuid,
milestone_uuid: Some(ms_uuid),
});
}
Ok(WriteSet { events })
},
&format!("add {} issue(s) to milestone #{}", ids.len(), milestone_id),
)?;
self.hydrate_with_retry(db);
Ok(())
}
/// Clear `milestone_uuid` on an issue JSON file.
///
/// # Errors
/// Returns an error if the issue cannot be loaded or the write fails.
pub fn clear_milestone_on_issue(&self, db: &Database, issue_id: i64) -> Result<()> {
let _ = self.write_commit_push(
|writer| {
let issue = writer.load_issue_by_id(issue_id, db)?;
let event = crate::events::Event::MilestoneAssigned {
issue_uuid: issue.uuid,
milestone_uuid: None,
};
Ok(WriteSet {
events: vec![event],
})
},
&format!("remove issue #{issue_id} from milestone"),
)?;
self.hydrate_with_retry(db);
Ok(())
}
}