1use std::collections::HashMap;
2
3use chrono::Utc;
4
5use crate::types::*;
6
7#[derive(Debug, Default)]
8pub struct CodeCommitState {
9 pub repositories: HashMap<String, Repository>,
10 pub branches: HashMap<String, HashMap<String, Branch>>,
12 pub commits: HashMap<String, HashMap<String, CommitRecord>>,
14 pub files: HashMap<String, HashMap<String, HashMap<String, FileEntry>>>,
16 pub pull_requests: HashMap<String, PullRequestRecord>,
18 pub pull_request_counter: u64,
20}
21
22#[derive(Debug, thiserror::Error)]
23pub enum CodeCommitError {
24 #[error("Repository named {name} already exists in this account.")]
25 RepositoryAlreadyExists { name: String },
26 #[error("Repository named {name} already exists")]
27 RepositoryNameTaken { name: String },
28 #[error("{name} does not exist")]
29 RepositoryDoesNotExist { name: String },
30 #[error("Repository with ARN {arn} does not exist")]
31 RepositoryDoesNotExistByArn { arn: String },
32 #[error("Branch {branch_name} already exists in {repo_name}")]
33 BranchAlreadyExists {
34 branch_name: String,
35 repo_name: String,
36 },
37 #[error("Branch {branch_name} does not exist in {repo_name}")]
38 BranchDoesNotExist {
39 branch_name: String,
40 repo_name: String,
41 },
42 #[error("Branch {branch_name} not found")]
43 BranchNotFound { branch_name: String },
44 #[error("The default branch cannot be deleted")]
45 DefaultBranchCannotBeDeleted,
46 #[error("Commit {commit_id} does not exist in {repo_name}")]
47 CommitDoesNotExist {
48 commit_id: String,
49 repo_name: String,
50 },
51 #[error("Specifier {spec} does not resolve to a commit")]
52 SpecifierDoesNotResolve { spec: String },
53 #[error("File {file_path} does not exist in commit {commit_id}")]
54 FileDoesNotExist {
55 file_path: String,
56 commit_id: String,
57 },
58 #[error("Pull request {pr_id} does not exist")]
59 PullRequestDoesNotExist { pr_id: String },
60 #[error("Repository has no default branch")]
61 RepositoryEmpty,
62}
63
64impl CodeCommitState {
65 pub fn create_repository(
66 &mut self,
67 name: &str,
68 description: &str,
69 account_id: &str,
70 region: &str,
71 ) -> Result<&Repository, CodeCommitError> {
72 if self.repositories.contains_key(name) {
73 return Err(CodeCommitError::RepositoryAlreadyExists {
74 name: name.to_string(),
75 });
76 }
77
78 let repo_id = uuid::Uuid::new_v4().to_string();
79 let arn = format!("arn:aws:codecommit:{region}:{account_id}:{name}");
80 let now = Utc::now();
81
82 let repo = Repository {
83 repository_id: repo_id,
84 repository_name: name.to_string(),
85 arn,
86 description: description.to_string(),
87 clone_url_http: format!(
88 "https://git-codecommit.{region}.amazonaws.com/v1/repos/{name}"
89 ),
90 clone_url_ssh: format!("ssh://git-codecommit.{region}.amazonaws.com/v1/repos/{name}"),
91 creation_date: now,
92 last_modified_date: now,
93 account_id: account_id.to_string(),
94 default_branch: None,
95 tags: HashMap::new(),
96 };
97
98 self.repositories.insert(name.to_string(), repo);
99 Ok(self.repositories.get(name).unwrap())
100 }
101
102 pub fn get_repository(&self, name: &str) -> Result<&Repository, CodeCommitError> {
103 self.repositories
104 .get(name)
105 .ok_or_else(|| CodeCommitError::RepositoryDoesNotExist {
106 name: name.to_string(),
107 })
108 }
109
110 pub fn delete_repository(&mut self, name: &str) -> String {
111 self.branches.remove(name);
114 self.commits.remove(name);
115 self.files.remove(name);
116 match self.repositories.remove(name) {
117 Some(repo) => repo.repository_id,
118 None => String::new(),
119 }
120 }
121
122 pub fn list_repositories(&self) -> Vec<&Repository> {
123 let mut repos: Vec<&Repository> = self.repositories.values().collect();
124 repos.sort_by(|a, b| a.repository_name.cmp(&b.repository_name));
125 repos
126 }
127
128 pub fn update_repository_description(
129 &mut self,
130 name: &str,
131 description: Option<&str>,
132 ) -> Result<(), CodeCommitError> {
133 let repo = self.repositories.get_mut(name).ok_or_else(|| {
134 CodeCommitError::RepositoryDoesNotExist {
135 name: name.to_string(),
136 }
137 })?;
138 repo.description = description.unwrap_or("").to_string();
139 repo.last_modified_date = Utc::now();
140 Ok(())
141 }
142
143 pub fn update_repository_name(
144 &mut self,
145 old_name: &str,
146 new_name: &str,
147 region: &str,
148 account_id: &str,
149 ) -> Result<(), CodeCommitError> {
150 if !self.repositories.contains_key(old_name) {
151 return Err(CodeCommitError::RepositoryDoesNotExist {
152 name: old_name.to_string(),
153 });
154 }
155 if self.repositories.contains_key(new_name) {
156 return Err(CodeCommitError::RepositoryNameTaken {
157 name: new_name.to_string(),
158 });
159 }
160 let mut repo = self.repositories.remove(old_name).unwrap();
161 repo.repository_name = new_name.to_string();
162 repo.arn = format!("arn:aws:codecommit:{region}:{account_id}:{new_name}");
163 repo.clone_url_http =
164 format!("https://git-codecommit.{region}.amazonaws.com/v1/repos/{new_name}");
165 repo.clone_url_ssh =
166 format!("ssh://git-codecommit.{region}.amazonaws.com/v1/repos/{new_name}");
167 repo.last_modified_date = Utc::now();
168 self.repositories.insert(new_name.to_string(), repo);
169 if let Some(branches) = self.branches.remove(old_name) {
171 self.branches.insert(new_name.to_string(), branches);
172 }
173 if let Some(commits) = self.commits.remove(old_name) {
174 self.commits.insert(new_name.to_string(), commits);
175 }
176 if let Some(files) = self.files.remove(old_name) {
177 self.files.insert(new_name.to_string(), files);
178 }
179 Ok(())
180 }
181
182 pub fn create_branch(
185 &mut self,
186 repo_name: &str,
187 branch_name: &str,
188 commit_id: &str,
189 ) -> Result<(), CodeCommitError> {
190 if !self.repositories.contains_key(repo_name) {
191 return Err(CodeCommitError::RepositoryDoesNotExist {
192 name: repo_name.to_string(),
193 });
194 }
195 let repo_branches = self.branches.entry(repo_name.to_string()).or_default();
196 if repo_branches.contains_key(branch_name) {
197 return Err(CodeCommitError::BranchAlreadyExists {
198 branch_name: branch_name.to_string(),
199 repo_name: repo_name.to_string(),
200 });
201 }
202 repo_branches.insert(
203 branch_name.to_string(),
204 Branch {
205 branch_name: branch_name.to_string(),
206 commit_id: commit_id.to_string(),
207 },
208 );
209 Ok(())
210 }
211
212 pub fn get_branch(
213 &self,
214 repo_name: &str,
215 branch_name: &str,
216 ) -> Result<&Branch, CodeCommitError> {
217 if !self.repositories.contains_key(repo_name) {
218 return Err(CodeCommitError::RepositoryDoesNotExist {
219 name: repo_name.to_string(),
220 });
221 }
222 self.branches
223 .get(repo_name)
224 .and_then(|m| m.get(branch_name))
225 .ok_or_else(|| CodeCommitError::BranchDoesNotExist {
226 branch_name: branch_name.to_string(),
227 repo_name: repo_name.to_string(),
228 })
229 }
230
231 pub fn list_branches(&self, repo_name: &str) -> Result<Vec<String>, CodeCommitError> {
232 if !self.repositories.contains_key(repo_name) {
233 return Err(CodeCommitError::RepositoryDoesNotExist {
234 name: repo_name.to_string(),
235 });
236 }
237 let mut names: Vec<String> = self
238 .branches
239 .get(repo_name)
240 .map(|m| m.keys().cloned().collect())
241 .unwrap_or_default();
242 names.sort();
243 Ok(names)
244 }
245
246 pub fn delete_branch(
247 &mut self,
248 repo_name: &str,
249 branch_name: &str,
250 ) -> Result<Branch, CodeCommitError> {
251 if !self.repositories.contains_key(repo_name) {
252 return Err(CodeCommitError::RepositoryDoesNotExist {
253 name: repo_name.to_string(),
254 });
255 }
256 if let Some(repo) = self.repositories.get(repo_name) {
258 if repo.default_branch.as_deref() == Some(branch_name) {
259 return Err(CodeCommitError::DefaultBranchCannotBeDeleted);
260 }
261 }
262 self.branches
263 .get_mut(repo_name)
264 .and_then(|m| m.remove(branch_name))
265 .ok_or_else(|| CodeCommitError::BranchDoesNotExist {
266 branch_name: branch_name.to_string(),
267 repo_name: repo_name.to_string(),
268 })
269 }
270
271 pub fn update_default_branch(
272 &mut self,
273 repo_name: &str,
274 branch_name: &str,
275 ) -> Result<(), CodeCommitError> {
276 if !self.repositories.contains_key(repo_name) {
277 return Err(CodeCommitError::RepositoryDoesNotExist {
278 name: repo_name.to_string(),
279 });
280 }
281 let branch_exists = self
283 .branches
284 .get(repo_name)
285 .map(|m| m.contains_key(branch_name))
286 .unwrap_or(false);
287 if !branch_exists {
288 return Err(CodeCommitError::BranchDoesNotExist {
289 branch_name: branch_name.to_string(),
290 repo_name: repo_name.to_string(),
291 });
292 }
293 let repo = self.repositories.get_mut(repo_name).unwrap();
294 repo.default_branch = Some(branch_name.to_string());
295 Ok(())
296 }
297
298 pub fn create_commit(
301 &mut self,
302 repo_name: &str,
303 branch_name: &str,
304 parent_commit_id: Option<&str>,
305 author_name: Option<&str>,
306 author_email: Option<&str>,
307 commit_message: Option<&str>,
308 put_files: Vec<(String, String)>, delete_files: Vec<String>,
310 ) -> Result<CommitRecord, CodeCommitError> {
311 if !self.repositories.contains_key(repo_name) {
312 return Err(CodeCommitError::RepositoryDoesNotExist {
313 name: repo_name.to_string(),
314 });
315 }
316
317 let now = Utc::now();
318 let commit_id = format!("{:x}", uuid::Uuid::new_v4().as_u128());
319 let tree_id = format!("{:x}", uuid::Uuid::new_v4().as_u128());
320
321 let parent_files: HashMap<String, FileEntry> = if let Some(pid) = parent_commit_id {
323 self.files
324 .get(repo_name)
325 .and_then(|r| r.get(pid))
326 .cloned()
327 .unwrap_or_default()
328 } else {
329 HashMap::new()
330 };
331
332 let mut new_files = parent_files;
334 for path in delete_files {
335 new_files.remove(&path);
336 }
337 for (path, mode) in put_files {
338 let blob_id = format!("{:x}", uuid::Uuid::new_v4().as_u128());
339 new_files.insert(
340 path.clone(),
341 FileEntry {
342 file_path: path,
343 blob_id,
344 file_mode: mode,
345 },
346 );
347 }
348
349 let record = CommitRecord {
350 commit_id: commit_id.clone(),
351 tree_id,
352 parent_ids: parent_commit_id
353 .map(|p| vec![p.to_string()])
354 .unwrap_or_default(),
355 message: commit_message.unwrap_or("").to_string(),
356 author_name: author_name.unwrap_or("").to_string(),
357 author_email: author_email.unwrap_or("").to_string(),
358 date: now,
359 };
360
361 self.commits
362 .entry(repo_name.to_string())
363 .or_default()
364 .insert(commit_id.clone(), record.clone());
365
366 self.files
367 .entry(repo_name.to_string())
368 .or_default()
369 .insert(commit_id.clone(), new_files);
370
371 let repo_branches = self.branches.entry(repo_name.to_string()).or_default();
373 if let Some(branch) = repo_branches.get_mut(branch_name) {
374 branch.commit_id = commit_id.clone();
375 } else {
376 repo_branches.insert(
378 branch_name.to_string(),
379 Branch {
380 branch_name: branch_name.to_string(),
381 commit_id: commit_id.clone(),
382 },
383 );
384 let repo = self.repositories.get_mut(repo_name).unwrap();
386 if repo.default_branch.is_none() {
387 repo.default_branch = Some(branch_name.to_string());
388 }
389 }
390
391 Ok(record)
392 }
393
394 pub fn get_commit(
395 &self,
396 repo_name: &str,
397 commit_id: &str,
398 ) -> Result<&CommitRecord, CodeCommitError> {
399 if !self.repositories.contains_key(repo_name) {
400 return Err(CodeCommitError::RepositoryDoesNotExist {
401 name: repo_name.to_string(),
402 });
403 }
404 self.commits
405 .get(repo_name)
406 .and_then(|m| m.get(commit_id))
407 .ok_or_else(|| CodeCommitError::CommitDoesNotExist {
408 commit_id: commit_id.to_string(),
409 repo_name: repo_name.to_string(),
410 })
411 }
412
413 pub fn get_file(
416 &self,
417 repo_name: &str,
418 commit_specifier: Option<&str>,
419 file_path: &str,
420 ) -> Result<(&CommitRecord, &FileEntry), CodeCommitError> {
421 if !self.repositories.contains_key(repo_name) {
422 return Err(CodeCommitError::RepositoryDoesNotExist {
423 name: repo_name.to_string(),
424 });
425 }
426
427 let commit_id = self.resolve_specifier(repo_name, commit_specifier)?;
429 let commit = self.get_commit(repo_name, &commit_id)?;
430
431 let file = self
432 .files
433 .get(repo_name)
434 .and_then(|r| r.get(&commit_id))
435 .and_then(|f| f.get(file_path))
436 .ok_or_else(|| CodeCommitError::FileDoesNotExist {
437 file_path: file_path.to_string(),
438 commit_id: commit_id.clone(),
439 })?;
440 Ok((commit, file))
441 }
442
443 pub fn get_folder(
444 &self,
445 repo_name: &str,
446 commit_specifier: Option<&str>,
447 folder_path: &str,
448 ) -> Result<(String, Vec<FileEntry>, Vec<String>), CodeCommitError> {
449 if !self.repositories.contains_key(repo_name) {
450 return Err(CodeCommitError::RepositoryDoesNotExist {
451 name: repo_name.to_string(),
452 });
453 }
454 let commit_id = self.resolve_specifier(repo_name, commit_specifier)?;
455 let prefix = if folder_path == "/" || folder_path.is_empty() {
456 "".to_string()
457 } else {
458 let p = folder_path.trim_start_matches('/');
459 format!("{p}/")
460 };
461
462 let all_files: Vec<FileEntry> = self
463 .files
464 .get(repo_name)
465 .and_then(|r| r.get(&commit_id))
466 .map(|f| {
467 f.values()
468 .filter(|fe| fe.file_path.starts_with(&prefix))
469 .cloned()
470 .collect()
471 })
472 .unwrap_or_default();
473
474 let mut direct_files: Vec<FileEntry> = Vec::new();
476 let mut sub_folders: std::collections::HashSet<String> = std::collections::HashSet::new();
477 for fe in &all_files {
478 let rest = &fe.file_path[prefix.len()..];
479 if rest.contains('/') {
480 let sub = rest.split('/').next().unwrap_or("");
481 sub_folders.insert(sub.to_string());
482 } else {
483 direct_files.push(fe.clone());
484 }
485 }
486 let sub_folder_list: Vec<String> = sub_folders.into_iter().collect();
487 Ok((commit_id, direct_files, sub_folder_list))
488 }
489
490 pub fn put_file(
491 &mut self,
492 repo_name: &str,
493 branch_name: &str,
494 parent_commit_id: &str,
495 file_path: &str,
496 file_mode: Option<&str>,
497 author_name: Option<&str>,
498 author_email: Option<&str>,
499 commit_message: Option<&str>,
500 ) -> Result<CommitRecord, CodeCommitError> {
501 let mode = file_mode.unwrap_or("NORMAL").to_string();
502 self.create_commit(
503 repo_name,
504 branch_name,
505 Some(parent_commit_id),
506 author_name,
507 author_email,
508 commit_message,
509 vec![(file_path.to_string(), mode)],
510 vec![],
511 )
512 }
513
514 pub fn delete_file(
515 &mut self,
516 repo_name: &str,
517 branch_name: &str,
518 parent_commit_id: &str,
519 file_path: &str,
520 author_name: Option<&str>,
521 author_email: Option<&str>,
522 commit_message: Option<&str>,
523 ) -> Result<CommitRecord, CodeCommitError> {
524 self.create_commit(
525 repo_name,
526 branch_name,
527 Some(parent_commit_id),
528 author_name,
529 author_email,
530 commit_message,
531 vec![],
532 vec![file_path.to_string()],
533 )
534 }
535
536 pub fn get_differences(
537 &self,
538 repo_name: &str,
539 after_commit_specifier: &str,
540 before_commit_specifier: Option<&str>,
541 ) -> Result<Vec<(Option<FileEntry>, Option<FileEntry>, String)>, CodeCommitError> {
542 if !self.repositories.contains_key(repo_name) {
543 return Err(CodeCommitError::RepositoryDoesNotExist {
544 name: repo_name.to_string(),
545 });
546 }
547 let after_id = self.resolve_specifier(repo_name, Some(after_commit_specifier))?;
548 let after_files: HashMap<String, FileEntry> = self
549 .files
550 .get(repo_name)
551 .and_then(|r| r.get(&after_id))
552 .cloned()
553 .unwrap_or_default();
554
555 let before_files: HashMap<String, FileEntry> = if let Some(spec) = before_commit_specifier {
556 let before_id = self.resolve_specifier(repo_name, Some(spec))?;
557 self.files
558 .get(repo_name)
559 .and_then(|r| r.get(&before_id))
560 .cloned()
561 .unwrap_or_default()
562 } else {
563 HashMap::new()
564 };
565
566 let mut diffs = Vec::new();
567
568 for (path, after_fe) in &after_files {
570 if let Some(before_fe) = before_files.get(path) {
571 if before_fe.blob_id != after_fe.blob_id {
572 diffs.push((
573 Some(before_fe.clone()),
574 Some(after_fe.clone()),
575 "M".to_string(),
576 ));
577 }
578 } else {
579 diffs.push((None, Some(after_fe.clone()), "A".to_string()));
580 }
581 }
582 for (path, before_fe) in &before_files {
584 if !after_files.contains_key(path) {
585 diffs.push((Some(before_fe.clone()), None, "D".to_string()));
586 }
587 }
588
589 Ok(diffs)
590 }
591
592 pub fn create_pull_request(
595 &mut self,
596 title: &str,
597 description: &str,
598 repo_name: &str,
599 source_reference: &str,
600 destination_reference: &str,
601 ) -> Result<PullRequestRecord, CodeCommitError> {
602 if !self.repositories.contains_key(repo_name) {
603 return Err(CodeCommitError::RepositoryDoesNotExist {
604 name: repo_name.to_string(),
605 });
606 }
607
608 let source_commit = self
610 .branches
611 .get(repo_name)
612 .and_then(|m| m.get(source_reference.trim_start_matches("refs/heads/")))
613 .map(|b| b.commit_id.clone())
614 .unwrap_or_default();
615 let destination_commit = self
616 .branches
617 .get(repo_name)
618 .and_then(|m| m.get(destination_reference.trim_start_matches("refs/heads/")))
619 .map(|b| b.commit_id.clone())
620 .unwrap_or_default();
621
622 self.pull_request_counter += 1;
623 let pr_id = self.pull_request_counter.to_string();
624 let now = Utc::now();
625 let pr = PullRequestRecord {
626 pull_request_id: pr_id.clone(),
627 title: title.to_string(),
628 description: description.to_string(),
629 status: "OPEN".to_string(),
630 repository_name: repo_name.to_string(),
631 source_reference: source_reference.to_string(),
632 destination_reference: destination_reference.to_string(),
633 source_commit,
634 destination_commit,
635 creation_date: now,
636 last_activity_date: now,
637 author_arn: "arn:aws:iam::123456789012:root".to_string(),
638 };
639 self.pull_requests.insert(pr_id, pr.clone());
640 Ok(pr)
641 }
642
643 pub fn get_pull_request(&self, pr_id: &str) -> Result<&PullRequestRecord, CodeCommitError> {
644 self.pull_requests
645 .get(pr_id)
646 .ok_or_else(|| CodeCommitError::PullRequestDoesNotExist {
647 pr_id: pr_id.to_string(),
648 })
649 }
650
651 pub fn list_pull_requests(&self, repo_name: &str, status: Option<&str>) -> Vec<String> {
652 self.pull_requests
653 .values()
654 .filter(|pr| {
655 pr.repository_name == repo_name && status.map(|s| pr.status == s).unwrap_or(true)
656 })
657 .map(|pr| pr.pull_request_id.clone())
658 .collect()
659 }
660
661 pub fn update_pull_request_status(
662 &mut self,
663 pr_id: &str,
664 status: &str,
665 ) -> Result<PullRequestRecord, CodeCommitError> {
666 let pr = self.pull_requests.get_mut(pr_id).ok_or_else(|| {
667 CodeCommitError::PullRequestDoesNotExist {
668 pr_id: pr_id.to_string(),
669 }
670 })?;
671 pr.status = status.to_string();
672 pr.last_activity_date = Utc::now();
673 Ok(pr.clone())
674 }
675
676 pub fn tag_resource(
679 &mut self,
680 repo_name: &str,
681 tags: std::collections::HashMap<String, String>,
682 ) -> Result<(), CodeCommitError> {
683 let repo = self.repositories.get_mut(repo_name).ok_or_else(|| {
684 CodeCommitError::RepositoryDoesNotExist {
685 name: repo_name.to_string(),
686 }
687 })?;
688 repo.tags.extend(tags);
689 Ok(())
690 }
691
692 pub fn untag_resource(
693 &mut self,
694 repo_name: &str,
695 tag_keys: &[String],
696 ) -> Result<(), CodeCommitError> {
697 let repo = self.repositories.get_mut(repo_name).ok_or_else(|| {
698 CodeCommitError::RepositoryDoesNotExist {
699 name: repo_name.to_string(),
700 }
701 })?;
702 for key in tag_keys {
703 repo.tags.remove(key);
704 }
705 Ok(())
706 }
707
708 pub fn list_tags_for_resource(
709 &self,
710 repo_arn: &str,
711 ) -> Result<std::collections::HashMap<String, String>, CodeCommitError> {
712 let repo = self
714 .repositories
715 .values()
716 .find(|r| r.arn == repo_arn)
717 .ok_or_else(|| CodeCommitError::RepositoryDoesNotExistByArn {
718 arn: repo_arn.to_string(),
719 })?;
720 Ok(repo.tags.clone())
721 }
722
723 fn resolve_specifier(
726 &self,
727 repo_name: &str,
728 specifier: Option<&str>,
729 ) -> Result<String, CodeCommitError> {
730 match specifier {
731 None => {
732 let repo = self.repositories.get(repo_name).ok_or_else(|| {
734 CodeCommitError::RepositoryDoesNotExist {
735 name: repo_name.to_string(),
736 }
737 })?;
738 let branch_name = repo
739 .default_branch
740 .as_deref()
741 .ok_or(CodeCommitError::RepositoryEmpty)?;
742 self.branches
743 .get(repo_name)
744 .and_then(|m| m.get(branch_name))
745 .map(|b| b.commit_id.clone())
746 .ok_or_else(|| CodeCommitError::BranchNotFound {
747 branch_name: branch_name.to_string(),
748 })
749 }
750 Some(spec) => {
751 let branch_name = spec.trim_start_matches("refs/heads/");
753 if let Some(branch) = self
754 .branches
755 .get(repo_name)
756 .and_then(|m| m.get(branch_name))
757 {
758 return Ok(branch.commit_id.clone());
759 }
760 if self
762 .commits
763 .get(repo_name)
764 .map(|m| m.contains_key(spec))
765 .unwrap_or(false)
766 {
767 return Ok(spec.to_string());
768 }
769 Err(CodeCommitError::SpecifierDoesNotResolve {
770 spec: spec.to_string(),
771 })
772 }
773 }
774 }
775}