1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11pub struct Org {
12 pub login: String,
14 pub id: u64,
16 #[serde(default)]
18 pub description: Option<String>,
19}
20
21impl Org {
22 pub fn new(login: impl Into<String>, id: u64) -> Self {
24 Self {
25 login: login.into(),
26 id,
27 description: None,
28 }
29 }
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
34pub struct Repo {
35 pub id: u64,
37 pub name: String,
39 pub full_name: String,
41 pub ssh_url: String,
43 pub clone_url: String,
45 pub default_branch: String,
47 #[serde(default)]
49 pub private: bool,
50 #[serde(default)]
52 pub archived: bool,
53 #[serde(default)]
55 pub fork: bool,
56 #[serde(default)]
58 pub pushed_at: Option<DateTime<Utc>>,
59 #[serde(default)]
61 pub description: Option<String>,
62}
63
64impl Repo {
65 #[cfg(test)]
67 pub fn test(name: &str, owner: &str) -> Self {
68 Self {
69 id: rand_id(),
70 name: name.to_string(),
71 full_name: format!("{}/{}", owner, name),
72 ssh_url: format!("git@github.com:{}/{}.git", owner, name),
73 clone_url: format!("https://github.com/{}/{}.git", owner, name),
74 default_branch: "main".to_string(),
75 private: false,
76 archived: false,
77 fork: false,
78 pushed_at: None,
79 description: None,
80 }
81 }
82
83 pub fn owner(&self) -> &str {
85 self.full_name.split('/').next().unwrap_or(&self.full_name)
86 }
87}
88
89#[cfg(test)]
90fn rand_id() -> u64 {
91 use std::sync::atomic::{AtomicU64, Ordering};
92
93 static COUNTER: AtomicU64 = AtomicU64::new(1);
94 COUNTER.fetch_add(1, Ordering::Relaxed)
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct OwnedRepo {
103 pub owner: String,
105 pub repo: Repo,
107}
108
109impl OwnedRepo {
110 pub fn new(owner: impl Into<String>, repo: Repo) -> Self {
112 Self {
113 owner: owner.into(),
114 repo,
115 }
116 }
117
118 pub fn full_name(&self) -> &str {
120 &self.repo.full_name
121 }
122
123 pub fn name(&self) -> &str {
125 &self.repo.name
126 }
127}
128
129#[derive(Debug, Default)]
133pub struct ActionPlan {
134 pub to_clone: Vec<OwnedRepo>,
136 pub to_sync: Vec<OwnedRepo>,
138 pub skipped: Vec<SkippedRepo>,
140}
141
142impl ActionPlan {
143 pub fn new() -> Self {
145 Self::default()
146 }
147
148 pub fn total(&self) -> usize {
150 self.to_clone.len() + self.to_sync.len() + self.skipped.len()
151 }
152
153 pub fn is_empty(&self) -> bool {
155 self.to_clone.is_empty() && self.to_sync.is_empty()
156 }
157
158 pub fn add_clone(&mut self, repo: OwnedRepo) {
160 self.to_clone.push(repo);
161 }
162
163 pub fn add_sync(&mut self, repo: OwnedRepo) {
165 self.to_sync.push(repo);
166 }
167
168 pub fn add_skipped(&mut self, repo: OwnedRepo, reason: impl Into<String>) {
170 self.skipped.push(SkippedRepo {
171 repo,
172 reason: reason.into(),
173 });
174 }
175}
176
177#[derive(Debug)]
179pub struct SkippedRepo {
180 pub repo: OwnedRepo,
182 pub reason: String,
184}
185
186#[derive(Debug, Clone, PartialEq, Eq)]
188pub enum OpResult {
189 Success,
191 Failed(String),
193 Skipped(String),
195}
196
197impl OpResult {
198 pub fn is_success(&self) -> bool {
200 matches!(self, OpResult::Success)
201 }
202
203 pub fn is_failed(&self) -> bool {
205 matches!(self, OpResult::Failed(_))
206 }
207
208 pub fn is_skipped(&self) -> bool {
210 matches!(self, OpResult::Skipped(_))
211 }
212
213 pub fn error_message(&self) -> Option<&str> {
215 match self {
216 OpResult::Failed(msg) => Some(msg),
217 _ => None,
218 }
219 }
220
221 pub fn skip_reason(&self) -> Option<&str> {
223 match self {
224 OpResult::Skipped(reason) => Some(reason),
225 _ => None,
226 }
227 }
228}
229
230#[derive(Debug, Default, Clone)]
232pub struct OpSummary {
233 pub success: usize,
235 pub failed: usize,
237 pub skipped: usize,
239}
240
241impl OpSummary {
242 pub fn new() -> Self {
244 Self::default()
245 }
246
247 pub fn record(&mut self, result: &OpResult) {
249 match result {
250 OpResult::Success => self.success += 1,
251 OpResult::Failed(_) => self.failed += 1,
252 OpResult::Skipped(_) => self.skipped += 1,
253 }
254 }
255
256 pub fn total(&self) -> usize {
258 self.success + self.failed + self.skipped
259 }
260
261 pub fn has_failures(&self) -> bool {
263 self.failed > 0
264 }
265}
266
267#[cfg(test)]
268#[path = "repo_tests.rs"]
269mod tests;