Skip to main content

winterbaume_codecommit/
state.rs

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    /// branches keyed by repo_name -> branch_name -> Branch
11    pub branches: HashMap<String, HashMap<String, Branch>>,
12    /// commits keyed by repo_name -> commit_id -> CommitRecord
13    pub commits: HashMap<String, HashMap<String, CommitRecord>>,
14    /// files keyed by repo_name -> commit_id -> file_path -> FileEntry
15    pub files: HashMap<String, HashMap<String, HashMap<String, FileEntry>>>,
16    /// pull requests keyed by pull_request_id
17    pub pull_requests: HashMap<String, PullRequestRecord>,
18    /// next pull request number per repo
19    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        // AWS DeleteRepository is idempotent: if the repository does not exist,
112        // a null repository ID is returned rather than an error.
113        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        // Migrate associated branch/commit/file state
170        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    // ---- Branches ----
183
184    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        // Prevent deleting default branch
257        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        // Branch must exist
282        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    // ---- Commits ----
299
300    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)>, // (path, mode)
309        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        // Determine parent files to inherit
322        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        // Build new file set
333        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        // Advance the branch
372        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            // Auto-create branch if it doesn't exist
377            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            // Set default branch if none
385            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    // ---- Files ----
414
415    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        // Resolve specifier (branch name or commit id) to a commit id
428        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        // Collect direct children that are files (no additional slash after prefix)
475        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        // Added or modified
569        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        // Deleted
583        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    // ---- Pull Requests ----
593
594    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        // Resolve source/dest commit IDs from branch names
609        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    // ---- Tags ----
677
678    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        // Find repo by ARN
713        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    // ---- Helper ----
724
725    fn resolve_specifier(
726        &self,
727        repo_name: &str,
728        specifier: Option<&str>,
729    ) -> Result<String, CodeCommitError> {
730        match specifier {
731            None => {
732                // Use default branch
733                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                // Try branch first, then treat as commit id
752                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                // Treat as a commit ID directly
761                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}