Skip to main content

fakecloud_codecommit/
service.rs

1//! AWS CodeCommit awsJson1.1 dispatch + operation handlers.
2//!
3//! Implements the full CodeCommit git-repository control plane against a real
4//! content-addressed object store (see [`crate::state`]): repositories, the
5//! commit graph and materialized trees, branches, files, pull requests with
6//! approvals/events/overrides, approval-rule templates and their repository
7//! associations, per-revision pull-request approval rules, comments and
8//! reactions, repository triggers, and resource tagging. Blob/tree/commit ids
9//! are real 40-char SHA-1 hex; clone URLs, ARNs, and repository UUIDs are minted
10//! in exact AWS form. Merges are computed on the stored graph (fast-forward and
11//! non-conflicting three-way/squash resolve; genuinely divergent trees report
12//! the declared `ManualMergeRequiredException`). Everything is real, persisted,
13//! and account-partitioned; there is no live git transport.
14
15use async_trait::async_trait;
16use base64::Engine;
17use chrono::Utc;
18use serde_json::{json, Map, Value};
19use std::sync::Arc;
20use tokio::sync::Mutex as AsyncMutex;
21
22use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
23use fakecloud_persistence::SnapshotStore;
24
25use crate::state::{FileEntry, Repo, SharedCodeCommitState};
26use crate::validate;
27
28use crate::codecommit_helpers::*;
29
30pub const CODECOMMIT_ACTIONS: &[&str] = &[
31    "AssociateApprovalRuleTemplateWithRepository",
32    "BatchAssociateApprovalRuleTemplateWithRepositories",
33    "BatchDescribeMergeConflicts",
34    "BatchDisassociateApprovalRuleTemplateFromRepositories",
35    "BatchGetCommits",
36    "BatchGetRepositories",
37    "CreateApprovalRuleTemplate",
38    "CreateBranch",
39    "CreateCommit",
40    "CreatePullRequest",
41    "CreatePullRequestApprovalRule",
42    "CreateRepository",
43    "CreateUnreferencedMergeCommit",
44    "DeleteApprovalRuleTemplate",
45    "DeleteBranch",
46    "DeleteCommentContent",
47    "DeleteFile",
48    "DeletePullRequestApprovalRule",
49    "DeleteRepository",
50    "DescribeMergeConflicts",
51    "DescribePullRequestEvents",
52    "DisassociateApprovalRuleTemplateFromRepository",
53    "EvaluatePullRequestApprovalRules",
54    "GetApprovalRuleTemplate",
55    "GetBlob",
56    "GetBranch",
57    "GetComment",
58    "GetCommentReactions",
59    "GetCommentsForComparedCommit",
60    "GetCommentsForPullRequest",
61    "GetCommit",
62    "GetDifferences",
63    "GetFile",
64    "GetFolder",
65    "GetMergeCommit",
66    "GetMergeConflicts",
67    "GetMergeOptions",
68    "GetPullRequest",
69    "GetPullRequestApprovalStates",
70    "GetPullRequestOverrideState",
71    "GetRepository",
72    "GetRepositoryTriggers",
73    "ListApprovalRuleTemplates",
74    "ListAssociatedApprovalRuleTemplatesForRepository",
75    "ListBranches",
76    "ListFileCommitHistory",
77    "ListPullRequests",
78    "ListRepositories",
79    "ListRepositoriesForApprovalRuleTemplate",
80    "ListTagsForResource",
81    "MergeBranchesByFastForward",
82    "MergeBranchesBySquash",
83    "MergeBranchesByThreeWay",
84    "MergePullRequestByFastForward",
85    "MergePullRequestBySquash",
86    "MergePullRequestByThreeWay",
87    "OverridePullRequestApprovalRules",
88    "PostCommentForComparedCommit",
89    "PostCommentForPullRequest",
90    "PostCommentReply",
91    "PutCommentReaction",
92    "PutFile",
93    "PutRepositoryTriggers",
94    "TagResource",
95    "TestRepositoryTriggers",
96    "UntagResource",
97    "UpdateApprovalRuleTemplateContent",
98    "UpdateApprovalRuleTemplateDescription",
99    "UpdateApprovalRuleTemplateName",
100    "UpdateComment",
101    "UpdateDefaultBranch",
102    "UpdatePullRequestApprovalRuleContent",
103    "UpdatePullRequestApprovalState",
104    "UpdatePullRequestDescription",
105    "UpdatePullRequestStatus",
106    "UpdatePullRequestTitle",
107    "UpdateRepositoryDescription",
108    "UpdateRepositoryEncryptionKey",
109    "UpdateRepositoryName",
110];
111
112pub struct CodeCommitService {
113    state: SharedCodeCommitState,
114    snapshot_store: Option<Arc<dyn SnapshotStore>>,
115    snapshot_lock: Arc<AsyncMutex<()>>,
116}
117
118impl CodeCommitService {
119    pub fn new(state: SharedCodeCommitState) -> Self {
120        Self {
121            state,
122            snapshot_store: None,
123            snapshot_lock: Arc::new(AsyncMutex::new(())),
124        }
125    }
126
127    pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
128        self.snapshot_store = Some(store);
129        self
130    }
131
132    async fn save(&self) {
133        crate::persistence::save_snapshot(
134            &self.state,
135            self.snapshot_store.clone(),
136            &self.snapshot_lock,
137        )
138        .await;
139    }
140
141    /// Persist hook for the CloudFormation provisioner; `None` in memory mode.
142    pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
143        let store = self.snapshot_store.clone()?;
144        let state = self.state.clone();
145        let lock = self.snapshot_lock.clone();
146        Some(Arc::new(move || {
147            let state = state.clone();
148            let store = store.clone();
149            let lock = lock.clone();
150            Box::pin(async move {
151                crate::persistence::save_snapshot(&state, Some(store), &lock).await;
152            })
153        }))
154    }
155}
156
157/// Whether an action can mutate persisted state (so a snapshot is taken on a
158/// successful response). The read/list/describe/get/evaluate/test operations
159/// never mutate.
160fn is_mutator(action: &str) -> bool {
161    action.starts_with("Create")
162        || action.starts_with("Delete")
163        || action.starts_with("Update")
164        || action.starts_with("Put")
165        || action.starts_with("Merge")
166        || action.starts_with("Post")
167        || action.starts_with("Associate")
168        || action.starts_with("Disassociate")
169        || action.starts_with("BatchAssociate")
170        || action.starts_with("BatchDisassociate")
171        || matches!(
172            action,
173            "TagResource" | "UntagResource" | "OverridePullRequestApprovalRules"
174        )
175}
176
177#[async_trait]
178impl AwsService for CodeCommitService {
179    fn service_name(&self) -> &str {
180        "codecommit"
181    }
182
183    async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
184        let action = req.action.clone();
185        let result = self.dispatch(&action, &req);
186        let should_save =
187            matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) && is_mutator(&action);
188        if should_save {
189            self.save().await;
190        }
191        result
192    }
193
194    fn supported_actions(&self) -> &[&str] {
195        CODECOMMIT_ACTIONS
196    }
197}
198
199impl CodeCommitService {
200    #[allow(clippy::too_many_lines)]
201    fn dispatch(&self, action: &str, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
202        match action {
203            // Repositories
204            "CreateRepository" => self.create_repository(req),
205            "GetRepository" => self.get_repository(req),
206            "DeleteRepository" => self.delete_repository(req),
207            "ListRepositories" => self.list_repositories(req),
208            "BatchGetRepositories" => self.batch_get_repositories(req),
209            "UpdateRepositoryDescription" => self.update_repository_description(req),
210            "UpdateRepositoryName" => self.update_repository_name(req),
211            "UpdateRepositoryEncryptionKey" => self.update_repository_encryption_key(req),
212            // Branches
213            "CreateBranch" => self.create_branch(req),
214            "DeleteBranch" => self.delete_branch(req),
215            "GetBranch" => self.get_branch(req),
216            "ListBranches" => self.list_branches(req),
217            "UpdateDefaultBranch" => self.update_default_branch(req),
218            // Files, blobs, commits
219            "PutFile" => self.put_file(req),
220            "DeleteFile" => self.delete_file(req),
221            "GetFile" => self.get_file(req),
222            "GetFolder" => self.get_folder(req),
223            "GetBlob" => self.get_blob(req),
224            "CreateCommit" => self.create_commit(req),
225            "GetCommit" => self.get_commit(req),
226            "BatchGetCommits" => self.batch_get_commits(req),
227            "GetDifferences" => self.get_differences(req),
228            "ListFileCommitHistory" => self.list_file_commit_history(req),
229            // Merges
230            "MergeBranchesByFastForward" => self.merge_branches_fast_forward(req),
231            "MergeBranchesBySquash" => self.merge_branches_squash(req),
232            "MergeBranchesByThreeWay" => self.merge_branches_three_way(req),
233            "CreateUnreferencedMergeCommit" => self.create_unreferenced_merge_commit(req),
234            "GetMergeCommit" => self.get_merge_commit(req),
235            "GetMergeOptions" => self.get_merge_options(req),
236            "GetMergeConflicts" => self.get_merge_conflicts(req),
237            "DescribeMergeConflicts" => self.describe_merge_conflicts(req),
238            "BatchDescribeMergeConflicts" => self.batch_describe_merge_conflicts(req),
239            // Pull requests
240            "CreatePullRequest" => self.create_pull_request(req),
241            "GetPullRequest" => self.get_pull_request(req),
242            "ListPullRequests" => self.list_pull_requests(req),
243            "UpdatePullRequestTitle" => self.update_pull_request_title(req),
244            "UpdatePullRequestDescription" => self.update_pull_request_description(req),
245            "UpdatePullRequestStatus" => self.update_pull_request_status(req),
246            "DescribePullRequestEvents" => self.describe_pull_request_events(req),
247            "MergePullRequestByFastForward" => self.merge_pull_request_fast_forward(req),
248            "MergePullRequestBySquash" => self.merge_pull_request_squash(req),
249            "MergePullRequestByThreeWay" => self.merge_pull_request_three_way(req),
250            // Pull-request approvals and rules
251            "CreatePullRequestApprovalRule" => self.create_pull_request_approval_rule(req),
252            "DeletePullRequestApprovalRule" => self.delete_pull_request_approval_rule(req),
253            "UpdatePullRequestApprovalRuleContent" => {
254                self.update_pull_request_approval_rule_content(req)
255            }
256            "UpdatePullRequestApprovalState" => self.update_pull_request_approval_state(req),
257            "GetPullRequestApprovalStates" => self.get_pull_request_approval_states(req),
258            "EvaluatePullRequestApprovalRules" => self.evaluate_pull_request_approval_rules(req),
259            "OverridePullRequestApprovalRules" => self.override_pull_request_approval_rules(req),
260            "GetPullRequestOverrideState" => self.get_pull_request_override_state(req),
261            // Approval-rule templates
262            "CreateApprovalRuleTemplate" => self.create_approval_rule_template(req),
263            "GetApprovalRuleTemplate" => self.get_approval_rule_template(req),
264            "DeleteApprovalRuleTemplate" => self.delete_approval_rule_template(req),
265            "ListApprovalRuleTemplates" => self.list_approval_rule_templates(req),
266            "UpdateApprovalRuleTemplateContent" => self.update_approval_rule_template_content(req),
267            "UpdateApprovalRuleTemplateDescription" => {
268                self.update_approval_rule_template_description(req)
269            }
270            "UpdateApprovalRuleTemplateName" => self.update_approval_rule_template_name(req),
271            "AssociateApprovalRuleTemplateWithRepository" => {
272                self.associate_template_with_repository(req)
273            }
274            "DisassociateApprovalRuleTemplateFromRepository" => {
275                self.disassociate_template_from_repository(req)
276            }
277            "BatchAssociateApprovalRuleTemplateWithRepositories" => {
278                self.batch_associate_template(req)
279            }
280            "BatchDisassociateApprovalRuleTemplateFromRepositories" => {
281                self.batch_disassociate_template(req)
282            }
283            "ListAssociatedApprovalRuleTemplatesForRepository" => {
284                self.list_associated_templates_for_repository(req)
285            }
286            "ListRepositoriesForApprovalRuleTemplate" => self.list_repositories_for_template(req),
287            // Comments
288            "PostCommentForComparedCommit" => self.post_comment_for_compared_commit(req),
289            "PostCommentForPullRequest" => self.post_comment_for_pull_request(req),
290            "PostCommentReply" => self.post_comment_reply(req),
291            "GetComment" => self.get_comment(req),
292            "GetCommentsForComparedCommit" => self.get_comments_for_compared_commit(req),
293            "GetCommentsForPullRequest" => self.get_comments_for_pull_request(req),
294            "UpdateComment" => self.update_comment(req),
295            "DeleteCommentContent" => self.delete_comment_content(req),
296            "PutCommentReaction" => self.put_comment_reaction(req),
297            "GetCommentReactions" => self.get_comment_reactions(req),
298            // Triggers
299            "PutRepositoryTriggers" => self.put_repository_triggers(req),
300            "GetRepositoryTriggers" => self.get_repository_triggers(req),
301            "TestRepositoryTriggers" => self.test_repository_triggers(req),
302            // Tagging
303            "TagResource" => self.tag_resource(req),
304            "UntagResource" => self.untag_resource(req),
305            "ListTagsForResource" => self.list_tags_for_resource(req),
306            _ => Err(AwsServiceError::action_not_implemented(
307                self.service_name(),
308                action,
309            )),
310        }
311    }
312}
313
314// ---------------------------------------------------------------------------
315// Free helpers
316// ---------------------------------------------------------------------------
317impl CodeCommitService {
318    fn account(&self, req: &AwsRequest) -> String {
319        req.account_id.clone()
320    }
321
322    // ----- Repositories -----
323
324    fn create_repository(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
325        let b = body(req);
326        let name = require_repository_name(&b)?;
327        if let Some(d) = str_field(&b, "repositoryDescription") {
328            if d.chars().count() > 1000 {
329                return Err(err(
330                    "InvalidRepositoryDescriptionException",
331                    "The repository description is not valid. Length must not exceed 1000 characters.",
332                ));
333            }
334        }
335        if let Some(k) = str_field(&b, "kmsKeyId") {
336            if !validate::valid_kms_key_id(&k) {
337                return Err(err(
338                    "EncryptionKeyInvalidIdException",
339                    "The Amazon Web Services KMS key is not valid.",
340                ));
341            }
342        }
343        let account = self.account(req);
344        let arn = repo_arn(&req.region, &account, &name);
345        let now = Utc::now();
346        let mut guard = self.state.write();
347        let st = guard.get_or_create(&account);
348        if st.repositories.contains_key(&name) {
349            return Err(err(
350                "RepositoryNameExistsException",
351                format!("Repository named {name} already exists."),
352            ));
353        }
354        let kms = str_field(&b, "kmsKeyId").unwrap_or_else(|| {
355            format!("arn:aws:kms:{}:{}:key/{}", req.region, account, new_uuid())
356        });
357        let mut metadata = Map::new();
358        metadata.insert("accountId".into(), json!(account));
359        metadata.insert("repositoryId".into(), json!(new_uuid()));
360        metadata.insert("repositoryName".into(), json!(name));
361        if let Some(d) = str_field(&b, "repositoryDescription") {
362            metadata.insert("repositoryDescription".into(), json!(d));
363        }
364        metadata.insert("lastModifiedDate".into(), ts(now));
365        metadata.insert("creationDate".into(), ts(now));
366        metadata.insert(
367            "cloneUrlHttp".into(),
368            json!(format!(
369                "https://git-codecommit.{}.amazonaws.com/v1/repos/{}",
370                req.region, name
371            )),
372        );
373        metadata.insert(
374            "cloneUrlSsh".into(),
375            json!(format!(
376                "ssh://git-codecommit.{}.amazonaws.com/v1/repos/{}",
377                req.region, name
378            )),
379        );
380        metadata.insert("Arn".into(), json!(arn));
381        metadata.insert("kmsKeyId".into(), json!(kms));
382        let metadata = Value::Object(metadata);
383        let repo = Repo {
384            metadata: metadata.clone(),
385            ..Repo::default()
386        };
387        st.repositories.insert(name.clone(), repo);
388        st.repository_order.push(name.clone());
389        if let Some(tags) = b.get("tags").and_then(Value::as_object) {
390            let map = tags
391                .iter()
392                .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
393                .collect();
394            st.tags.insert(arn, map);
395        }
396        ok(json!({ "repositoryMetadata": metadata }))
397    }
398
399    fn get_repository(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
400        let b = body(req);
401        let name = require_repository_name(&b)?;
402        let account = self.account(req);
403        let guard = self.state.read();
404        let st = guard.get(&account);
405        match st.and_then(|s| s.repositories.get(&name)) {
406            Some(repo) => ok(json!({ "repositoryMetadata": repo.metadata })),
407            None => Err(repo_not_found(&name)),
408        }
409    }
410
411    fn delete_repository(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
412        let b = body(req);
413        let name = require_repository_name(&b)?;
414        let account = self.account(req);
415        let arn = repo_arn(&req.region, &account, &name);
416        let mut guard = self.state.write();
417        let st = guard.get_or_create(&account);
418        // Removing the repository drops its `associated_templates`, which is the
419        // sole record of approval-rule-template associations, so no template
420        // association can outlive the repository.
421        let repository_id = st.repositories.remove(&name).and_then(|r| {
422            r.metadata
423                .get("repositoryId")
424                .and_then(Value::as_str)
425                .map(str::to_string)
426        });
427        st.repository_order.retain(|n| n != &name);
428        st.tags.remove(&arn);
429        match repository_id {
430            Some(id) => ok(json!({ "repositoryId": id })),
431            None => ok(json!({})),
432        }
433    }
434
435    fn list_repositories(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
436        let b = body(req);
437        check_enum(&b, "sortBy", validate::SORT_BY, "InvalidSortByException")?;
438        check_enum(&b, "order", validate::ORDER, "InvalidOrderException")?;
439        let account = self.account(req);
440        let guard = self.state.read();
441        let st = guard.get(&account);
442        let mut pairs = Vec::new();
443        if let Some(s) = st {
444            let mut names: Vec<String> = s.repository_order.clone();
445            if str_field(&b, "sortBy").as_deref() == Some("lastModifiedDate") {
446                names.sort_by_key(|n| {
447                    s.repositories
448                        .get(n)
449                        .and_then(|r| r.metadata.get("lastModifiedDate"))
450                        .and_then(Value::as_f64)
451                        .map(|f| (f * 1000.0) as i64)
452                        .unwrap_or(0)
453                });
454            } else {
455                names.sort();
456            }
457            if str_field(&b, "order").as_deref() == Some("descending") {
458                names.reverse();
459            }
460            for n in names {
461                if let Some(r) = s.repositories.get(&n) {
462                    pairs.push(json!({
463                        "repositoryName": n,
464                        "repositoryId": r.metadata.get("repositoryId").cloned().unwrap_or(Value::Null),
465                    }));
466                }
467            }
468        }
469        ok(json!({ "repositories": pairs }))
470    }
471
472    fn batch_get_repositories(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
473        let b = body(req);
474        let names = b
475            .get("repositoryNames")
476            .and_then(Value::as_array)
477            .ok_or_else(|| {
478                err(
479                    "RepositoryNamesRequiredException",
480                    "A repository names object is required.",
481                )
482            })?;
483        if names.len() > 100 {
484            return Err(err(
485                "MaximumRepositoryNamesExceededException",
486                "The maximum number of repository names for a request (100) has been exceeded.",
487            ));
488        }
489        let account = self.account(req);
490        let guard = self.state.read();
491        let st = guard.get(&account);
492        let mut found = Vec::new();
493        let mut not_found = Vec::new();
494        for n in names {
495            let Some(name) = n.as_str() else { continue };
496            match st.and_then(|s| s.repositories.get(name)) {
497                Some(r) => found.push(r.metadata.clone()),
498                None => not_found.push(json!(name)),
499            }
500        }
501        ok(json!({
502            "repositories": found,
503            "repositoriesNotFound": not_found,
504            "errors": [],
505        }))
506    }
507
508    fn update_repository_description(
509        &self,
510        req: &AwsRequest,
511    ) -> Result<AwsResponse, AwsServiceError> {
512        let b = body(req);
513        let name = require_repository_name(&b)?;
514        if let Some(d) = str_field(&b, "repositoryDescription") {
515            if d.chars().count() > 1000 {
516                return Err(err(
517                    "InvalidRepositoryDescriptionException",
518                    "The repository description is not valid.",
519                ));
520            }
521        }
522        let account = self.account(req);
523        let mut guard = self.state.write();
524        let st = guard.get_or_create(&account);
525        let repo = st
526            .repositories
527            .get_mut(&name)
528            .ok_or_else(|| repo_not_found(&name))?;
529        if let Some(obj) = repo.metadata.as_object_mut() {
530            match str_field(&b, "repositoryDescription") {
531                Some(d) => {
532                    obj.insert("repositoryDescription".into(), json!(d));
533                }
534                None => {
535                    obj.remove("repositoryDescription");
536                }
537            }
538            obj.insert("lastModifiedDate".into(), ts(Utc::now()));
539        }
540        ok(json!({}))
541    }
542
543    fn update_repository_name(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
544        let b = body(req);
545        let old = require(&b, "oldName", "RepositoryNameRequiredException")?;
546        let new = require(&b, "newName", "RepositoryNameRequiredException")?;
547        if !validate::valid_repository_name(&old) || !validate::valid_repository_name(&new) {
548            return Err(err(
549                "InvalidRepositoryNameException",
550                "The repository name is not valid.",
551            ));
552        }
553        let account = self.account(req);
554        let mut guard = self.state.write();
555        let st = guard.get_or_create(&account);
556        if !st.repositories.contains_key(&old) {
557            return Err(repo_not_found(&old));
558        }
559        if old != new && st.repositories.contains_key(&new) {
560            return Err(err(
561                "RepositoryNameExistsException",
562                format!("Repository named {new} already exists."),
563            ));
564        }
565        if old != new {
566            let mut repo = st.repositories.remove(&old).unwrap();
567            let arn = repo_arn(&req.region, &account, &new);
568            if let Some(obj) = repo.metadata.as_object_mut() {
569                obj.insert("repositoryName".into(), json!(new));
570                obj.insert("Arn".into(), json!(arn.clone()));
571                obj.insert(
572                    "cloneUrlHttp".into(),
573                    json!(format!(
574                        "https://git-codecommit.{}.amazonaws.com/v1/repos/{}",
575                        req.region, new
576                    )),
577                );
578                obj.insert(
579                    "cloneUrlSsh".into(),
580                    json!(format!(
581                        "ssh://git-codecommit.{}.amazonaws.com/v1/repos/{}",
582                        req.region, new
583                    )),
584                );
585                obj.insert("lastModifiedDate".into(), ts(Utc::now()));
586            }
587            let old_arn = repo_arn(&req.region, &account, &old);
588            if let Some(tags) = st.tags.remove(&old_arn) {
589                st.tags.insert(arn, tags);
590            }
591            st.repositories.insert(new.clone(), repo);
592            for n in &mut st.repository_order {
593                if n == &old {
594                    *n = new.clone();
595                }
596            }
597        }
598        ok(json!({}))
599    }
600
601    fn update_repository_encryption_key(
602        &self,
603        req: &AwsRequest,
604    ) -> Result<AwsResponse, AwsServiceError> {
605        let b = body(req);
606        let name = require_repository_name(&b)?;
607        let kms = require(&b, "kmsKeyId", "EncryptionKeyRequiredException")?;
608        if !validate::valid_kms_key_id(&kms) {
609            return Err(err(
610                "EncryptionKeyInvalidIdException",
611                "The Amazon Web Services KMS key is not valid.",
612            ));
613        }
614        let account = self.account(req);
615        let mut guard = self.state.write();
616        let st = guard.get_or_create(&account);
617        let repo = st
618            .repositories
619            .get_mut(&name)
620            .ok_or_else(|| repo_not_found(&name))?;
621        let repository_id = repo
622            .metadata
623            .get("repositoryId")
624            .and_then(Value::as_str)
625            .unwrap_or_default()
626            .to_string();
627        let original = repo
628            .metadata
629            .get("kmsKeyId")
630            .and_then(Value::as_str)
631            .unwrap_or_default()
632            .to_string();
633        if let Some(obj) = repo.metadata.as_object_mut() {
634            obj.insert("kmsKeyId".into(), json!(kms));
635            obj.insert("lastModifiedDate".into(), ts(Utc::now()));
636        }
637        ok(json!({
638            "repositoryId": repository_id,
639            "kmsKeyId": kms,
640            "originalKmsKeyId": original,
641        }))
642    }
643
644    // ----- Branches -----
645
646    fn create_branch(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
647        let b = body(req);
648        let name = require_repository_name(&b)?;
649        let branch = require(&b, "branchName", "BranchNameRequiredException")?;
650        if !validate::valid_branch_name(&branch) {
651            return Err(err(
652                "InvalidBranchNameException",
653                "The branch name is not valid.",
654            ));
655        }
656        let commit = require(&b, "commitId", "CommitIdRequiredException")?;
657        if !is_object_id(&commit) {
658            return Err(err(
659                "InvalidCommitIdException",
660                "The commit ID is not valid.",
661            ));
662        }
663        let account = self.account(req);
664        let mut guard = self.state.write();
665        let st = guard.get_or_create(&account);
666        let repo = st
667            .repositories
668            .get_mut(&name)
669            .ok_or_else(|| repo_not_found(&name))?;
670        if repo.branches.contains_key(&branch) {
671            return Err(err(
672                "BranchNameExistsException",
673                format!("Branch name {branch} already exists."),
674            ));
675        }
676        if !repo.commits.contains_key(&commit) {
677            return Err(err(
678                "CommitDoesNotExistException",
679                "The specified commit does not exist.",
680            ));
681        }
682        repo.branches.insert(branch, commit);
683        ok(json!({}))
684    }
685
686    fn delete_branch(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
687        let b = body(req);
688        let name = require_repository_name(&b)?;
689        let branch = require(&b, "branchName", "BranchNameRequiredException")?;
690        if !validate::valid_branch_name(&branch) {
691            return Err(err(
692                "InvalidBranchNameException",
693                "The branch name is not valid.",
694            ));
695        }
696        let account = self.account(req);
697        let mut guard = self.state.write();
698        let st = guard.get_or_create(&account);
699        let repo = st
700            .repositories
701            .get_mut(&name)
702            .ok_or_else(|| repo_not_found(&name))?;
703        let default = repo
704            .metadata
705            .get("defaultBranch")
706            .and_then(Value::as_str)
707            .map(str::to_string);
708        if default.as_deref() == Some(branch.as_str()) {
709            return Err(err(
710                "DefaultBranchCannotBeDeletedException",
711                "The default branch for a repository cannot be deleted.",
712            ));
713        }
714        match repo.branches.remove(&branch) {
715            Some(commit) => ok(json!({
716                "deletedBranch": { "branchName": branch, "commitId": commit }
717            })),
718            None => ok(json!({})),
719        }
720    }
721
722    fn get_branch(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
723        let b = body(req);
724        let name = str_field(&b, "repositoryName").ok_or_else(|| {
725            err(
726                "RepositoryNameRequiredException",
727                "A repository name is required.",
728            )
729        })?;
730        if !validate::valid_repository_name(&name) {
731            return Err(err(
732                "InvalidRepositoryNameException",
733                "The repository name is not valid.",
734            ));
735        }
736        let branch = require(&b, "branchName", "BranchNameRequiredException")?;
737        if !validate::valid_branch_name(&branch) {
738            return Err(err(
739                "InvalidBranchNameException",
740                "The branch name is not valid.",
741            ));
742        }
743        let account = self.account(req);
744        let guard = self.state.read();
745        let st = guard.get(&account);
746        let repo = st
747            .and_then(|s| s.repositories.get(&name))
748            .ok_or_else(|| repo_not_found(&name))?;
749        match repo.branches.get(&branch) {
750            Some(commit) => ok(json!({
751                "branch": { "branchName": branch, "commitId": commit }
752            })),
753            None => Err(err(
754                "BranchDoesNotExistException",
755                "The specified branch does not exist.",
756            )),
757        }
758    }
759
760    fn list_branches(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
761        let b = body(req);
762        let name = require_repository_name(&b)?;
763        let account = self.account(req);
764        let guard = self.state.read();
765        let st = guard.get(&account);
766        let repo = st
767            .and_then(|s| s.repositories.get(&name))
768            .ok_or_else(|| repo_not_found(&name))?;
769        let branches: Vec<Value> = repo.branches.keys().map(|k| json!(k)).collect();
770        ok(json!({ "branches": branches }))
771    }
772
773    fn update_default_branch(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
774        let b = body(req);
775        let name = require_repository_name(&b)?;
776        let branch = require(&b, "defaultBranchName", "BranchNameRequiredException")?;
777        if !validate::valid_branch_name(&branch) {
778            return Err(err(
779                "InvalidBranchNameException",
780                "The branch name is not valid.",
781            ));
782        }
783        let account = self.account(req);
784        let mut guard = self.state.write();
785        let st = guard.get_or_create(&account);
786        let repo = st
787            .repositories
788            .get_mut(&name)
789            .ok_or_else(|| repo_not_found(&name))?;
790        if !repo.branches.contains_key(&branch) {
791            return Err(err(
792                "BranchDoesNotExistException",
793                "The specified branch does not exist.",
794            ));
795        }
796        if let Some(obj) = repo.metadata.as_object_mut() {
797            obj.insert("defaultBranch".into(), json!(branch));
798            obj.insert("lastModifiedDate".into(), ts(Utc::now()));
799        }
800        ok(json!({}))
801    }
802
803    // ----- Files / blobs / commits -----
804
805    fn put_file(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
806        let b = body(req);
807        let name = require_repository_name(&b)?;
808        let branch = require(&b, "branchName", "BranchNameRequiredException")?;
809        if !validate::valid_branch_name(&branch) {
810            return Err(err(
811                "InvalidBranchNameException",
812                "The branch name is not valid.",
813            ));
814        }
815        let path = require(&b, "filePath", "PathRequiredException")?;
816        let content_b64 = require(&b, "fileContent", "FileContentRequiredException")?;
817        check_enum(
818            &b,
819            "fileMode",
820            validate::FILE_MODE,
821            "InvalidFileModeException",
822        )?;
823        let mode = str_field(&b, "fileMode").unwrap_or_else(|| "NORMAL".to_string());
824        let content = base64::engine::general_purpose::STANDARD
825            .decode(content_b64.as_bytes())
826            .unwrap_or_else(|_| content_b64.clone().into_bytes());
827        let account = self.account(req);
828        let mut guard = self.state.write();
829        let st = guard.get_or_create(&account);
830        let repo = st
831            .repositories
832            .get_mut(&name)
833            .ok_or_else(|| repo_not_found(&name))?;
834
835        let parent_commit = str_field(&b, "parentCommitId");
836        let mut tree = if let Some(tip) = repo.branches.get(&branch).cloned() {
837            // Existing branch: parent commit id must match the tip.
838            match &parent_commit {
839                None => {
840                    return Err(err(
841                        "ParentCommitIdRequiredException",
842                        "A parent commit ID is required.",
843                    ))
844                }
845                Some(p) if *p != tip => {
846                    return Err(err(
847                        "ParentCommitIdOutdatedException",
848                        "The parent commit ID is not the tip of the branch.",
849                    ))
850                }
851                _ => {}
852            }
853            repo.trees.get(&tip).cloned().unwrap_or_default()
854        } else {
855            std::collections::BTreeMap::new()
856        };
857
858        let bid = blob_id_for(&content);
859        // SameFileContentException when the path already holds this exact blob.
860        if tree.get(&path).map(|e| &e.blob_id) == Some(&bid)
861            && tree.get(&path).map(|e| e.mode.as_str()) == Some(file_mode_to_git(&mode))
862        {
863            return Err(err(
864                "SameFileContentException",
865                "The file was not added or updated because the content of the file is exactly the same as the content of that file in the repository and branch that you specified.",
866            ));
867        }
868        repo.blobs.insert(bid.clone(), content_b64.clone());
869        tree.insert(
870            path.clone(),
871            FileEntry {
872                blob_id: bid.clone(),
873                mode: file_mode_to_git(&mode).to_string(),
874            },
875        );
876        let tree_id = tree_id_for(&tree);
877        let commit_id = fresh_object_id();
878        let parents: Vec<String> = parent_commit.into_iter().collect();
879        let commit = build_commit(&commit_id, &tree_id, &parents, &b, req);
880        repo.commits.insert(commit_id.clone(), commit);
881        repo.trees.insert(commit_id.clone(), tree);
882        repo.branches.insert(branch.clone(), commit_id.clone());
883        set_default_if_absent(repo);
884        // Advancing this branch invalidates approvals on any open PR sourced from it.
885        refresh_source_revisions(st, &name, &branch);
886        ok(json!({ "commitId": commit_id, "blobId": bid, "treeId": tree_id }))
887    }
888
889    fn delete_file(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
890        let b = body(req);
891        let name = require_repository_name(&b)?;
892        let branch = require(&b, "branchName", "BranchNameRequiredException")?;
893        if !validate::valid_branch_name(&branch) {
894            return Err(err(
895                "InvalidBranchNameException",
896                "The branch name is not valid.",
897            ));
898        }
899        let path = require(&b, "filePath", "PathRequiredException")?;
900        let parent = require(&b, "parentCommitId", "ParentCommitIdRequiredException")?;
901        let account = self.account(req);
902        let mut guard = self.state.write();
903        let st = guard.get_or_create(&account);
904        let repo = st
905            .repositories
906            .get_mut(&name)
907            .ok_or_else(|| repo_not_found(&name))?;
908        let tip = repo.branches.get(&branch).cloned().ok_or_else(|| {
909            err(
910                "BranchDoesNotExistException",
911                "The specified branch does not exist.",
912            )
913        })?;
914        if parent != tip {
915            return Err(err(
916                "ParentCommitIdOutdatedException",
917                "The parent commit ID is not the tip of the branch.",
918            ));
919        }
920        let mut tree = repo.trees.get(&tip).cloned().unwrap_or_default();
921        if tree.remove(&path).is_none() {
922            return Err(err(
923                "FileDoesNotExistException",
924                "The specified file does not exist.",
925            ));
926        }
927        let tree_id = tree_id_for(&tree);
928        let commit_id = fresh_object_id();
929        let commit = build_commit(&commit_id, &tree_id, &[tip], &b, req);
930        repo.commits.insert(commit_id.clone(), commit);
931        repo.trees.insert(commit_id.clone(), tree);
932        repo.branches.insert(branch.clone(), commit_id.clone());
933        refresh_source_revisions(st, &name, &branch);
934        ok(json!({
935            "commitId": commit_id,
936            "blobId": blob_id_for(&[]),
937            "treeId": tree_id,
938            "filePath": path,
939        }))
940    }
941
942    fn create_commit(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
943        let b = body(req);
944        let name = require_repository_name(&b)?;
945        let branch = require(&b, "branchName", "BranchNameRequiredException")?;
946        if !validate::valid_branch_name(&branch) {
947            return Err(err(
948                "InvalidBranchNameException",
949                "The branch name is not valid.",
950            ));
951        }
952        let account = self.account(req);
953        let mut guard = self.state.write();
954        let st = guard.get_or_create(&account);
955        let repo = st
956            .repositories
957            .get_mut(&name)
958            .ok_or_else(|| repo_not_found(&name))?;
959
960        let parent_commit = str_field(&b, "parentCommitId");
961        let mut tree = if let Some(tip) = repo.branches.get(&branch).cloned() {
962            match &parent_commit {
963                None => {
964                    return Err(err(
965                        "ParentCommitIdRequiredException",
966                        "A parent commit ID is required.",
967                    ))
968                }
969                Some(p) if *p != tip => {
970                    return Err(err(
971                        "ParentCommitIdOutdatedException",
972                        "The parent commit ID is not the tip of the branch.",
973                    ))
974                }
975                _ => {}
976            }
977            repo.trees.get(&tip).cloned().unwrap_or_default()
978        } else {
979            std::collections::BTreeMap::new()
980        };
981
982        let mut files_added = Vec::new();
983        let mut files_updated = Vec::new();
984        let mut files_deleted = Vec::new();
985
986        if let Some(puts) = b.get("putFiles").and_then(Value::as_array) {
987            for pf in puts {
988                let Some(path) = pf.get("filePath").and_then(Value::as_str) else {
989                    return Err(err("PathRequiredException", "A file path is required."));
990                };
991                let mode = pf
992                    .get("fileMode")
993                    .and_then(Value::as_str)
994                    .unwrap_or("NORMAL");
995                let content_b64 = pf
996                    .get("fileContent")
997                    .and_then(Value::as_str)
998                    .map(str::to_string)
999                    .unwrap_or_default();
1000                let content = base64::engine::general_purpose::STANDARD
1001                    .decode(content_b64.as_bytes())
1002                    .unwrap_or_else(|_| content_b64.clone().into_bytes());
1003                let bid = blob_id_for(&content);
1004                repo.blobs.insert(bid.clone(), content_b64.clone());
1005                let existed = tree.contains_key(path);
1006                tree.insert(
1007                    path.to_string(),
1008                    FileEntry {
1009                        blob_id: bid.clone(),
1010                        mode: file_mode_to_git(mode).to_string(),
1011                    },
1012                );
1013                let meta = json!({ "absolutePath": path, "blobId": bid, "fileMode": git_mode_to_file(file_mode_to_git(mode)) });
1014                if existed {
1015                    files_updated.push(meta);
1016                } else {
1017                    files_added.push(meta);
1018                }
1019            }
1020        }
1021        if let Some(dels) = b.get("deleteFiles").and_then(Value::as_array) {
1022            for df in dels {
1023                if let Some(path) = df.get("filePath").and_then(Value::as_str) {
1024                    if tree.remove(path).is_some() {
1025                        files_deleted.push(json!({ "absolutePath": path }));
1026                    }
1027                }
1028            }
1029        }
1030        if let Some(modes) = b.get("setFileModes").and_then(Value::as_array) {
1031            for sm in modes {
1032                if let (Some(path), Some(mode)) = (
1033                    sm.get("filePath").and_then(Value::as_str),
1034                    sm.get("fileMode").and_then(Value::as_str),
1035                ) {
1036                    if let Some(e) = tree.get_mut(path) {
1037                        e.mode = file_mode_to_git(mode).to_string();
1038                    }
1039                }
1040            }
1041        }
1042
1043        if files_added.is_empty()
1044            && files_updated.is_empty()
1045            && files_deleted.is_empty()
1046            && parent_commit.is_some()
1047        {
1048            return Err(err(
1049                "NoChangeException",
1050                "The commit cannot be created because no changes will be made to the repository as a result of this commit.",
1051            ));
1052        }
1053
1054        let tree_id = tree_id_for(&tree);
1055        let commit_id = fresh_object_id();
1056        let parents: Vec<String> = parent_commit.into_iter().collect();
1057        let commit = build_commit(&commit_id, &tree_id, &parents, &b, req);
1058        repo.commits.insert(commit_id.clone(), commit);
1059        repo.trees.insert(commit_id.clone(), tree);
1060        repo.branches.insert(branch.clone(), commit_id.clone());
1061        set_default_if_absent(repo);
1062        refresh_source_revisions(st, &name, &branch);
1063        ok(json!({
1064            "commitId": commit_id,
1065            "treeId": tree_id,
1066            "filesAdded": files_added,
1067            "filesUpdated": files_updated,
1068            "filesDeleted": files_deleted,
1069        }))
1070    }
1071
1072    fn get_commit(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1073        let b = body(req);
1074        let name = require_repository_name(&b)?;
1075        let commit = require(&b, "commitId", "CommitIdRequiredException")?;
1076        if !is_object_id(&commit) {
1077            return Err(err(
1078                "InvalidCommitIdException",
1079                "The commit ID is not valid.",
1080            ));
1081        }
1082        let account = self.account(req);
1083        let guard = self.state.read();
1084        let st = guard.get(&account);
1085        let repo = st
1086            .and_then(|s| s.repositories.get(&name))
1087            .ok_or_else(|| repo_not_found(&name))?;
1088        match repo.commits.get(&commit) {
1089            Some(c) => ok(json!({ "commit": c })),
1090            None => Err(err(
1091                "CommitIdDoesNotExistException",
1092                "The specified commit ID does not exist.",
1093            )),
1094        }
1095    }
1096
1097    fn batch_get_commits(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1098        let b = body(req);
1099        let name = require_repository_name(&b)?;
1100        let ids = b
1101            .get("commitIds")
1102            .and_then(Value::as_array)
1103            .ok_or_else(|| {
1104                err(
1105                    "CommitIdsListRequiredException",
1106                    "A list of commit IDs is required.",
1107                )
1108            })?;
1109        let account = self.account(req);
1110        let guard = self.state.read();
1111        let st = guard.get(&account);
1112        let repo = st
1113            .and_then(|s| s.repositories.get(&name))
1114            .ok_or_else(|| repo_not_found(&name))?;
1115        let mut commits = Vec::new();
1116        let mut errors = Vec::new();
1117        for id in ids {
1118            let Some(cid) = id.as_str() else { continue };
1119            match repo.commits.get(cid) {
1120                Some(c) => commits.push(c.clone()),
1121                None => errors.push(json!({
1122                    "commitId": cid,
1123                    "errorCode": "CommitDoesNotExist",
1124                    "errorMessage": "The specified commit does not exist.",
1125                })),
1126            }
1127        }
1128        ok(json!({ "commits": commits, "errors": errors }))
1129    }
1130
1131    fn get_blob(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1132        let b = body(req);
1133        let name = require_repository_name(&b)?;
1134        let blob = require(&b, "blobId", "BlobIdRequiredException")?;
1135        if !is_object_id(&blob) {
1136            return Err(err("InvalidBlobIdException", "The blob ID is not valid."));
1137        }
1138        let account = self.account(req);
1139        let guard = self.state.read();
1140        let st = guard.get(&account);
1141        let repo = st
1142            .and_then(|s| s.repositories.get(&name))
1143            .ok_or_else(|| repo_not_found(&name))?;
1144        match repo.blobs.get(&blob) {
1145            Some(content_b64) => ok(json!({ "content": content_b64 })),
1146            None => Err(err(
1147                "BlobIdDoesNotExistException",
1148                "The specified blob does not exist.",
1149            )),
1150        }
1151    }
1152
1153    fn get_file(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1154        let b = body(req);
1155        let name = require_repository_name(&b)?;
1156        let path = require(&b, "filePath", "PathRequiredException")?;
1157        let account = self.account(req);
1158        let guard = self.state.read();
1159        let st = guard.get(&account);
1160        let repo = st
1161            .and_then(|s| s.repositories.get(&name))
1162            .ok_or_else(|| repo_not_found(&name))?;
1163        let commit_id = resolve_commit(repo, &b, "commitSpecifier")?;
1164        let tree = repo.trees.get(&commit_id).cloned().unwrap_or_default();
1165        let entry = tree.get(&path).ok_or_else(|| {
1166            err(
1167                "FileDoesNotExistException",
1168                "The specified file does not exist.",
1169            )
1170        })?;
1171        let content_b64 = repo.blobs.get(&entry.blob_id).cloned().unwrap_or_default();
1172        let size = base64::engine::general_purpose::STANDARD
1173            .decode(content_b64.as_bytes())
1174            .map(|d| d.len())
1175            .unwrap_or(content_b64.len());
1176        ok(json!({
1177            "commitId": commit_id,
1178            "blobId": entry.blob_id,
1179            "filePath": path,
1180            "fileMode": git_mode_to_file(&entry.mode),
1181            "fileSize": size,
1182            "fileContent": content_b64,
1183        }))
1184    }
1185
1186    fn get_folder(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1187        let b = body(req);
1188        let name = require_repository_name(&b)?;
1189        let folder = require(&b, "folderPath", "PathRequiredException")?;
1190        let account = self.account(req);
1191        let guard = self.state.read();
1192        let st = guard.get(&account);
1193        let repo = st
1194            .and_then(|s| s.repositories.get(&name))
1195            .ok_or_else(|| repo_not_found(&name))?;
1196        let commit_id = resolve_commit(repo, &b, "commitSpecifier")?;
1197        let tree = repo.trees.get(&commit_id).cloned().unwrap_or_default();
1198        let prefix = if folder == "/" || folder.is_empty() {
1199            String::new()
1200        } else {
1201            format!("{}/", folder.trim_end_matches('/'))
1202        };
1203        let tree_id = repo
1204            .commits
1205            .get(&commit_id)
1206            .and_then(|c| c.get("treeId"))
1207            .and_then(Value::as_str)
1208            .unwrap_or_default()
1209            .to_string();
1210        let mut files = Vec::new();
1211        let mut subfolders = std::collections::BTreeSet::new();
1212        let mut symlinks = Vec::new();
1213        for (path, entry) in &tree {
1214            if !path.starts_with(&prefix) {
1215                continue;
1216            }
1217            let rel = &path[prefix.len()..];
1218            if let Some(idx) = rel.find('/') {
1219                subfolders.insert(rel[..idx].to_string());
1220            } else {
1221                let file = json!({
1222                    "blobId": entry.blob_id,
1223                    "absolutePath": path,
1224                    "relativePath": rel,
1225                    "fileMode": git_mode_to_file(&entry.mode),
1226                });
1227                if entry.mode == "120000" {
1228                    symlinks.push(file);
1229                } else {
1230                    files.push(file);
1231                }
1232            }
1233        }
1234        let sub: Vec<Value> = subfolders
1235            .into_iter()
1236            .map(|s| {
1237                json!({
1238                    "treeId": tree_id,
1239                    "absolutePath": format!("{prefix}{s}"),
1240                    "relativePath": s,
1241                })
1242            })
1243            .collect();
1244        ok(json!({
1245            "commitId": commit_id,
1246            "folderPath": folder,
1247            "treeId": tree_id,
1248            "subFolders": sub,
1249            "files": files,
1250            "symbolicLinks": symlinks,
1251            "subModules": [],
1252        }))
1253    }
1254
1255    fn get_differences(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1256        let b = body(req);
1257        let name = require_repository_name(&b)?;
1258        let account = self.account(req);
1259        let guard = self.state.read();
1260        let st = guard.get(&account);
1261        let repo = st
1262            .and_then(|s| s.repositories.get(&name))
1263            .ok_or_else(|| repo_not_found(&name))?;
1264        let after = resolve_commit(repo, &b, "afterCommitSpecifier")?;
1265        let after_tree = repo.trees.get(&after).cloned().unwrap_or_default();
1266        let before_tree = if b.get("beforeCommitSpecifier").is_some() {
1267            let before = resolve_commit(repo, &b, "beforeCommitSpecifier")?;
1268            repo.trees.get(&before).cloned().unwrap_or_default()
1269        } else {
1270            std::collections::BTreeMap::new()
1271        };
1272        let mut diffs = Vec::new();
1273        for (path, entry) in &after_tree {
1274            match before_tree.get(path) {
1275                None => diffs.push(json!({
1276                    "afterBlob": { "blobId": entry.blob_id, "path": path, "mode": entry.mode },
1277                    "changeType": "A",
1278                })),
1279                Some(bentry) if bentry.blob_id != entry.blob_id => diffs.push(json!({
1280                    "beforeBlob": { "blobId": bentry.blob_id, "path": path, "mode": bentry.mode },
1281                    "afterBlob": { "blobId": entry.blob_id, "path": path, "mode": entry.mode },
1282                    "changeType": "M",
1283                })),
1284                _ => {}
1285            }
1286        }
1287        for (path, bentry) in &before_tree {
1288            if !after_tree.contains_key(path) {
1289                diffs.push(json!({
1290                    "beforeBlob": { "blobId": bentry.blob_id, "path": path, "mode": bentry.mode },
1291                    "changeType": "D",
1292                }));
1293            }
1294        }
1295        ok(json!({ "differences": diffs }))
1296    }
1297
1298    fn list_file_commit_history(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1299        let b = body(req);
1300        let name = require_repository_name(&b)?;
1301        let _path = require(&b, "filePath", "PathRequiredException")?;
1302        let account = self.account(req);
1303        let guard = self.state.read();
1304        let st = guard.get(&account);
1305        let repo = st
1306            .and_then(|s| s.repositories.get(&name))
1307            .ok_or_else(|| repo_not_found(&name))?;
1308        let commit_id = resolve_commit(repo, &b, "commitSpecifier")?;
1309        // Walk the first-parent chain from the resolved commit.
1310        let mut dag = Vec::new();
1311        let mut cur = Some(commit_id);
1312        while let Some(cid) = cur {
1313            let Some(c) = repo.commits.get(&cid) else {
1314                break;
1315            };
1316            let parents = c
1317                .get("parents")
1318                .and_then(Value::as_array)
1319                .cloned()
1320                .unwrap_or_default();
1321            dag.push(json!({
1322                "commit": cid,
1323                "parents": parents.iter().filter_map(|p| p.as_str()).collect::<Vec<_>>(),
1324            }));
1325            cur = parents.first().and_then(Value::as_str).map(str::to_string);
1326        }
1327        ok(json!({ "revisionDag": dag }))
1328    }
1329
1330    // ----- Merges -----
1331
1332    fn merge_branches_fast_forward(
1333        &self,
1334        req: &AwsRequest,
1335    ) -> Result<AwsResponse, AwsServiceError> {
1336        let b = body(req);
1337        let name = require_repository_name(&b)?;
1338        require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1339        require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1340        let account = self.account(req);
1341        let mut guard = self.state.write();
1342        let st = guard.get_or_create(&account);
1343        let repo = st
1344            .repositories
1345            .get_mut(&name)
1346            .ok_or_else(|| repo_not_found(&name))?;
1347        let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1348        let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1349        if !is_ancestor(repo, &dest, &source) {
1350            return Err(err(
1351                "ManualMergeRequiredException",
1352                "The fast-forward merge cannot be performed because the destination is not an ancestor of the source.",
1353            ));
1354        }
1355        let tree_id = repo
1356            .commits
1357            .get(&source)
1358            .and_then(|c| c.get("treeId"))
1359            .and_then(Value::as_str)
1360            .unwrap_or_default()
1361            .to_string();
1362        if let Some(target) = str_field(&b, "targetBranch") {
1363            repo.branches.insert(target.clone(), source.clone());
1364            refresh_source_revisions(st, &name, &target);
1365        }
1366        ok(json!({ "commitId": source, "treeId": tree_id }))
1367    }
1368
1369    fn merge_branches_squash(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1370        self.merge_branches_create(req, "SQUASH_MERGE")
1371    }
1372
1373    fn merge_branches_three_way(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1374        self.merge_branches_create(req, "THREE_WAY_MERGE")
1375    }
1376
1377    fn merge_branches_create(
1378        &self,
1379        req: &AwsRequest,
1380        merge_option: &str,
1381    ) -> Result<AwsResponse, AwsServiceError> {
1382        let b = body(req);
1383        let name = require_repository_name(&b)?;
1384        require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1385        require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1386        validate_merge_conflict_enums(&b)?;
1387        let account = self.account(req);
1388        let mut guard = self.state.write();
1389        let st = guard.get_or_create(&account);
1390        let repo = st
1391            .repositories
1392            .get_mut(&name)
1393            .ok_or_else(|| repo_not_found(&name))?;
1394        let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1395        let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1396        let strategy = str_field(&b, "conflictResolutionStrategy").unwrap_or_default();
1397        let merged = merge_trees(repo, &source, &dest, &strategy).ok_or_else(|| {
1398            err(
1399                "ManualMergeRequiredException",
1400                "The merge cannot be completed because there are merge conflicts that must be resolved manually.",
1401            )
1402        })?;
1403        let tree_id = tree_id_for(&merged);
1404        let commit_id = fresh_object_id();
1405        let parents = merge_parents(merge_option, &dest, &source);
1406        let commit = build_commit(&commit_id, &tree_id, &parents, &b, req);
1407        repo.commits.insert(commit_id.clone(), commit);
1408        repo.trees.insert(commit_id.clone(), merged);
1409        if let Some(target) = str_field(&b, "targetBranch") {
1410            repo.branches.insert(target.clone(), commit_id.clone());
1411            refresh_source_revisions(st, &name, &target);
1412        }
1413        ok(json!({ "commitId": commit_id, "treeId": tree_id }))
1414    }
1415
1416    fn create_unreferenced_merge_commit(
1417        &self,
1418        req: &AwsRequest,
1419    ) -> Result<AwsResponse, AwsServiceError> {
1420        let b = body(req);
1421        let name = require_repository_name(&b)?;
1422        require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1423        require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1424        let merge_option = require(&b, "mergeOption", "MergeOptionRequiredException")?;
1425        if !validate::is_enum(validate::MERGE_OPTION, &merge_option) {
1426            return Err(err(
1427                "InvalidMergeOptionException",
1428                "The merge option is not valid.",
1429            ));
1430        }
1431        validate_merge_conflict_enums(&b)?;
1432        let account = self.account(req);
1433        let mut guard = self.state.write();
1434        let st = guard.get_or_create(&account);
1435        let repo = st
1436            .repositories
1437            .get_mut(&name)
1438            .ok_or_else(|| repo_not_found(&name))?;
1439        let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1440        let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1441        let strategy = str_field(&b, "conflictResolutionStrategy").unwrap_or_default();
1442        let merged = merge_trees(repo, &source, &dest, &strategy).ok_or_else(|| {
1443            err(
1444                "ManualMergeRequiredException",
1445                "The merge cannot be completed because there are merge conflicts that must be resolved manually.",
1446            )
1447        })?;
1448        let tree_id = tree_id_for(&merged);
1449        let commit_id = fresh_object_id();
1450        let parents = merge_parents(&merge_option, &dest, &source);
1451        let commit = build_commit(&commit_id, &tree_id, &parents, &b, req);
1452        repo.commits.insert(commit_id.clone(), commit);
1453        repo.trees.insert(commit_id.clone(), merged);
1454        ok(json!({ "commitId": commit_id, "treeId": tree_id }))
1455    }
1456
1457    fn get_merge_commit(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1458        let b = body(req);
1459        let name = require_repository_name(&b)?;
1460        require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1461        require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1462        let account = self.account(req);
1463        let guard = self.state.read();
1464        let st = guard.get(&account);
1465        let repo = st
1466            .and_then(|s| s.repositories.get(&name))
1467            .ok_or_else(|| repo_not_found(&name))?;
1468        let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1469        let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1470        let base = merge_base(repo, &source, &dest);
1471        ok(json!({
1472            "sourceCommitId": source,
1473            "destinationCommitId": dest,
1474            "baseCommitId": base,
1475        }))
1476    }
1477
1478    fn get_merge_options(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1479        let b = body(req);
1480        let name = require_repository_name(&b)?;
1481        require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1482        require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1483        let account = self.account(req);
1484        let guard = self.state.read();
1485        let st = guard.get(&account);
1486        let repo = st
1487            .and_then(|s| s.repositories.get(&name))
1488            .ok_or_else(|| repo_not_found(&name))?;
1489        let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1490        let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1491        let base = merge_base(repo, &source, &dest);
1492        let mut options = vec![json!("SQUASH_MERGE"), json!("THREE_WAY_MERGE")];
1493        if is_ancestor(repo, &dest, &source) {
1494            options.insert(0, json!("FAST_FORWARD_MERGE"));
1495        }
1496        ok(json!({
1497            "mergeOptions": options,
1498            "sourceCommitId": source,
1499            "destinationCommitId": dest,
1500            "baseCommitId": base,
1501        }))
1502    }
1503
1504    fn get_merge_conflicts(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1505        let b = body(req);
1506        let name = require_repository_name(&b)?;
1507        require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1508        require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1509        let merge_option = require(&b, "mergeOption", "MergeOptionRequiredException")?;
1510        if !validate::is_enum(validate::MERGE_OPTION, &merge_option) {
1511            return Err(err(
1512                "InvalidMergeOptionException",
1513                "The merge option is not valid.",
1514            ));
1515        }
1516        let account = self.account(req);
1517        let guard = self.state.read();
1518        let st = guard.get(&account);
1519        let repo = st
1520            .and_then(|s| s.repositories.get(&name))
1521            .ok_or_else(|| repo_not_found(&name))?;
1522        let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1523        let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1524        let empty = std::collections::BTreeMap::new();
1525        let (base, base_tree, src_tree, dst_tree) = merge_inputs(repo, &source, &dest, &empty);
1526        let conflicts = conflicting_paths_in(base_tree, src_tree, dst_tree);
1527        let mergeable = conflicts.is_empty();
1528        let metadata: Vec<Value> = conflicts
1529            .iter()
1530            .map(|p| conflict_metadata(repo, &source, &dest, base_tree, src_tree, dst_tree, p).0)
1531            .collect();
1532        ok(json!({
1533            "mergeable": mergeable,
1534            "destinationCommitId": dest,
1535            "sourceCommitId": source,
1536            "baseCommitId": base,
1537            "conflictMetadataList": metadata,
1538        }))
1539    }
1540
1541    fn describe_merge_conflicts(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1542        let b = body(req);
1543        let name = require_repository_name(&b)?;
1544        require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1545        require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1546        let merge_option = require(&b, "mergeOption", "MergeOptionRequiredException")?;
1547        if !validate::is_enum(validate::MERGE_OPTION, &merge_option) {
1548            return Err(err(
1549                "InvalidMergeOptionException",
1550                "The merge option is not valid.",
1551            ));
1552        }
1553        let path = require(&b, "filePath", "PathRequiredException")?;
1554        let account = self.account(req);
1555        let guard = self.state.read();
1556        let st = guard.get(&account);
1557        let repo = st
1558            .and_then(|s| s.repositories.get(&name))
1559            .ok_or_else(|| repo_not_found(&name))?;
1560        let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1561        let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1562        let empty = std::collections::BTreeMap::new();
1563        let (base, base_tree, src_tree, dst_tree) = merge_inputs(repo, &source, &dest, &empty);
1564        if base_tree.get(&path).is_none()
1565            && src_tree.get(&path).is_none()
1566            && dst_tree.get(&path).is_none()
1567        {
1568            return Err(err(
1569                "FileDoesNotExistException",
1570                "The specified file does not exist.",
1571            ));
1572        }
1573        let (metadata, hunks) =
1574            conflict_metadata(repo, &source, &dest, base_tree, src_tree, dst_tree, &path);
1575        ok(json!({
1576            "conflictMetadata": metadata,
1577            "mergeHunks": hunks,
1578            "destinationCommitId": dest,
1579            "sourceCommitId": source,
1580            "baseCommitId": base,
1581        }))
1582    }
1583
1584    fn batch_describe_merge_conflicts(
1585        &self,
1586        req: &AwsRequest,
1587    ) -> Result<AwsResponse, AwsServiceError> {
1588        let b = body(req);
1589        let name = require_repository_name(&b)?;
1590        require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1591        require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1592        let merge_option = require(&b, "mergeOption", "MergeOptionRequiredException")?;
1593        if !validate::is_enum(validate::MERGE_OPTION, &merge_option) {
1594            return Err(err(
1595                "InvalidMergeOptionException",
1596                "The merge option is not valid.",
1597            ));
1598        }
1599        let account = self.account(req);
1600        let guard = self.state.read();
1601        let st = guard.get(&account);
1602        let repo = st
1603            .and_then(|s| s.repositories.get(&name))
1604            .ok_or_else(|| repo_not_found(&name))?;
1605        let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1606        let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1607        let max_files = b
1608            .get("maxConflictFiles")
1609            .and_then(Value::as_i64)
1610            .filter(|n| *n >= 0)
1611            .map_or(usize::MAX, |n| n as usize);
1612        let max_hunks = b
1613            .get("maxMergeHunks")
1614            .and_then(Value::as_i64)
1615            .filter(|n| *n >= 0)
1616            .map_or(usize::MAX, |n| n as usize);
1617        let empty = std::collections::BTreeMap::new();
1618        let (base, base_tree, src_tree, dst_tree) = merge_inputs(repo, &source, &dest, &empty);
1619        let mut conflicts = Vec::new();
1620        for path in conflicting_paths_in(base_tree, src_tree, dst_tree) {
1621            if conflicts.len() >= max_files {
1622                break;
1623            }
1624            let (metadata, mut hunks) =
1625                conflict_metadata(repo, &source, &dest, base_tree, src_tree, dst_tree, &path);
1626            hunks.truncate(max_hunks);
1627            conflicts.push(json!({
1628                "conflictMetadata": metadata,
1629                "mergeHunks": hunks,
1630            }));
1631        }
1632        ok(json!({
1633            "conflicts": conflicts,
1634            "errors": [],
1635            "destinationCommitId": dest,
1636            "sourceCommitId": source,
1637            "baseCommitId": base,
1638        }))
1639    }
1640
1641    // ----- Pull requests -----
1642
1643    fn create_pull_request(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1644        let b = body(req);
1645        let title = match str_field(&b, "title") {
1646            Some(t) => t,
1647            None => {
1648                return Err(err(
1649                    "TitleRequiredException",
1650                    "A pull request title is required.",
1651                ))
1652            }
1653        };
1654        if title.chars().count() > 150 {
1655            return Err(err("InvalidTitleException", "The title is not valid."));
1656        }
1657        let targets = b
1658            .get("targets")
1659            .and_then(Value::as_array)
1660            .filter(|t| !t.is_empty())
1661            .ok_or_else(|| {
1662                err(
1663                    "TargetsRequiredException",
1664                    "Pull request targets are required.",
1665                )
1666            })?
1667            .clone();
1668        if let Some(d) = str_field(&b, "description") {
1669            if d.chars().count() > 10240 {
1670                return Err(err(
1671                    "InvalidDescriptionException",
1672                    "The description is not valid.",
1673                ));
1674            }
1675        }
1676        let account = self.account(req);
1677        let author = caller_arn(req);
1678        let mut guard = self.state.write();
1679        let st = guard.get_or_create(&account);
1680        let now = Utc::now();
1681        let mut pr_targets = Vec::new();
1682        let mut approval_rules = Vec::new();
1683        for t in &targets {
1684            let repo_name = t
1685                .get("repositoryName")
1686                .and_then(Value::as_str)
1687                .ok_or_else(|| {
1688                    err(
1689                        "RepositoryNameRequiredException",
1690                        "A repository name is required.",
1691                    )
1692                })?;
1693            let repo = st
1694                .repositories
1695                .get(repo_name)
1696                .ok_or_else(|| repo_not_found(repo_name))?;
1697            let source_ref = t
1698                .get("sourceReference")
1699                .and_then(Value::as_str)
1700                .ok_or_else(|| {
1701                    err(
1702                        "ReferenceNameRequiredException",
1703                        "A source reference is required.",
1704                    )
1705                })?;
1706            let dest_ref = t
1707                .get("destinationReference")
1708                .and_then(Value::as_str)
1709                .map(str::to_string)
1710                .or_else(|| {
1711                    repo.metadata
1712                        .get("defaultBranch")
1713                        .and_then(Value::as_str)
1714                        .map(str::to_string)
1715                })
1716                .unwrap_or_else(|| "main".to_string());
1717            if source_ref == dest_ref {
1718                return Err(err(
1719                    "SourceAndDestinationAreSameException",
1720                    "The source reference and destination reference are the same. You must specify different references for the source and destination.",
1721                ));
1722            }
1723            let source_commit = repo.branches.get(source_ref).cloned().ok_or_else(|| {
1724                err(
1725                    "ReferenceDoesNotExistException",
1726                    "The specified reference does not exist.",
1727                )
1728            })?;
1729            // The destination reference must also resolve to a real branch.
1730            let dest_commit = repo.branches.get(&dest_ref).cloned().ok_or_else(|| {
1731                err(
1732                    "ReferenceDoesNotExistException",
1733                    "The specified reference does not exist.",
1734                )
1735            })?;
1736            let base = merge_base(repo, &source_commit, &dest_commit);
1737            pr_targets.push(json!({
1738                "repositoryName": repo_name,
1739                "sourceReference": source_ref,
1740                "destinationReference": dest_ref,
1741                "sourceCommit": source_commit,
1742                "destinationCommit": dest_commit,
1743                "mergeBase": base,
1744                "mergeMetadata": { "isMerged": false },
1745            }));
1746            // Auto-populate approval rules from every approval-rule template
1747            // associated with the target repository (exactly as CodeCommit does).
1748            for tmpl_name in &repo.associated_templates {
1749                if approval_rules.iter().any(|r: &Value| {
1750                    r.get("approvalRuleName").and_then(Value::as_str) == Some(tmpl_name.as_str())
1751                }) {
1752                    continue;
1753                }
1754                if let Some(tmpl) = st.templates.get(tmpl_name) {
1755                    let content = tmpl
1756                        .get("approvalRuleTemplateContent")
1757                        .and_then(Value::as_str)
1758                        .unwrap_or_default()
1759                        .to_string();
1760                    approval_rules.push(json!({
1761                        "approvalRuleId": new_uuid(),
1762                        "approvalRuleName": tmpl_name,
1763                        "approvalRuleContent": content,
1764                        "ruleContentSha256": sha256_hex(content.as_bytes()),
1765                        "lastModifiedDate": ts(now),
1766                        "creationDate": ts(now),
1767                        "lastModifiedUser": author,
1768                        "originApprovalRuleTemplate": {
1769                            "approvalRuleTemplateId": tmpl
1770                                .get("approvalRuleTemplateId")
1771                                .cloned()
1772                                .unwrap_or(Value::Null),
1773                            "approvalRuleTemplateName": tmpl_name,
1774                        },
1775                    }));
1776                }
1777            }
1778        }
1779        st.pull_request_counter += 1;
1780        let pr_id = st.pull_request_counter.to_string();
1781        let revision_id = fresh_object_id();
1782        let pr = json!({
1783            "pullRequestId": pr_id,
1784            "title": title,
1785            "description": str_field(&b, "description").unwrap_or_default(),
1786            "lastActivityDate": ts(now),
1787            "creationDate": ts(now),
1788            "pullRequestStatus": "OPEN",
1789            "authorArn": author,
1790            "pullRequestTargets": pr_targets,
1791            "clientRequestToken": str_field(&b, "clientRequestToken").unwrap_or_default(),
1792            "revisionId": revision_id,
1793            "approvalRules": approval_rules,
1794        });
1795        st.pull_requests.insert(pr_id.clone(), pr.clone());
1796        st.pull_request_order.push(pr_id.clone());
1797        st.pull_request_events.insert(
1798            pr_id.clone(),
1799            vec![pr_event(
1800                &pr_id,
1801                "PULL_REQUEST_CREATED",
1802                &author,
1803                now,
1804                json!({}),
1805            )],
1806        );
1807        ok(json!({ "pullRequest": pr }))
1808    }
1809
1810    fn get_pull_request(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1811        let b = body(req);
1812        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
1813        let account = self.account(req);
1814        let guard = self.state.read();
1815        let st = guard.get(&account);
1816        // An open PR's target commits/mergeBase are recomputed from live branch
1817        // tips so a read always reflects what a merge would actually use.
1818        match st.and_then(|s| s.pull_requests.get(&id).map(|pr| refresh_pr_targets(s, pr))) {
1819            Some(pr) => ok(json!({ "pullRequest": pr })),
1820            None => Err(pr_not_found()),
1821        }
1822    }
1823
1824    fn list_pull_requests(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1825        let b = body(req);
1826        let name = require_repository_name(&b)?;
1827        check_enum(
1828            &b,
1829            "pullRequestStatus",
1830            validate::PULL_REQUEST_STATUS,
1831            "InvalidPullRequestStatusException",
1832        )?;
1833        let author_filter = str_field(&b, "authorArn");
1834        let status_filter = str_field(&b, "pullRequestStatus");
1835        let account = self.account(req);
1836        let guard = self.state.read();
1837        let st = guard.get(&account);
1838        let s = st.ok_or_else(|| repo_not_found(&name))?;
1839        if !s.repositories.contains_key(&name) {
1840            return Err(repo_not_found(&name));
1841        }
1842        let mut ids = Vec::new();
1843        for pid in &s.pull_request_order {
1844            let Some(pr) = s.pull_requests.get(pid) else {
1845                continue;
1846            };
1847            let in_repo = pr
1848                .get("pullRequestTargets")
1849                .and_then(Value::as_array)
1850                .map(|ts| {
1851                    ts.iter().any(|t| {
1852                        t.get("repositoryName").and_then(Value::as_str) == Some(name.as_str())
1853                    })
1854                })
1855                .unwrap_or(false);
1856            if !in_repo {
1857                continue;
1858            }
1859            if let Some(a) = &author_filter {
1860                if pr.get("authorArn").and_then(Value::as_str) != Some(a.as_str()) {
1861                    continue;
1862                }
1863            }
1864            if let Some(s2) = &status_filter {
1865                if pr.get("pullRequestStatus").and_then(Value::as_str) != Some(s2.as_str()) {
1866                    continue;
1867                }
1868            }
1869            ids.push(json!(pid));
1870        }
1871        ok(json!({ "pullRequestIds": ids }))
1872    }
1873
1874    fn update_pull_request_title(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1875        let b = body(req);
1876        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
1877        let title = match str_field(&b, "title") {
1878            Some(t) => t,
1879            None => {
1880                return Err(err(
1881                    "TitleRequiredException",
1882                    "A pull request title is required.",
1883                ))
1884            }
1885        };
1886        if title.chars().count() > 150 {
1887            return Err(err("InvalidTitleException", "The title is not valid."));
1888        }
1889        self.mutate_pr(req, &id, |pr| {
1890            pr["title"] = json!(title);
1891            pr["lastActivityDate"] = ts(Utc::now());
1892            Ok(())
1893        })
1894    }
1895
1896    fn update_pull_request_description(
1897        &self,
1898        req: &AwsRequest,
1899    ) -> Result<AwsResponse, AwsServiceError> {
1900        let b = body(req);
1901        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
1902        let desc = str_field(&b, "description").unwrap_or_default();
1903        if desc.chars().count() > 10240 {
1904            return Err(err(
1905                "InvalidDescriptionException",
1906                "The description is not valid.",
1907            ));
1908        }
1909        self.mutate_pr(req, &id, |pr| {
1910            pr["description"] = json!(desc);
1911            pr["lastActivityDate"] = ts(Utc::now());
1912            Ok(())
1913        })
1914    }
1915
1916    fn update_pull_request_status(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1917        let b = body(req);
1918        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
1919        let status = require(
1920            &b,
1921            "pullRequestStatus",
1922            "PullRequestStatusRequiredException",
1923        )?;
1924        if !validate::is_enum(validate::PULL_REQUEST_STATUS, &status) {
1925            return Err(err(
1926                "InvalidPullRequestStatusException",
1927                "The pull request status is not valid.",
1928            ));
1929        }
1930        self.mutate_pr(req, &id, |pr| {
1931            let current = pr.get("pullRequestStatus").and_then(Value::as_str).unwrap_or("OPEN");
1932            // The only valid transition is OPEN -> CLOSED. Reopening a closed PR
1933            // (CLOSED -> *), re-closing (CLOSED -> CLOSED), and a no-op
1934            // OPEN -> OPEN are all rejected, matching CodeCommit.
1935            if !(current == "OPEN" && status == "CLOSED") {
1936                return Err(err(
1937                    "InvalidPullRequestStatusUpdateException",
1938                    "The pull request status update is not valid. The only valid update is from OPEN to CLOSED.",
1939                ));
1940            }
1941            pr["pullRequestStatus"] = json!(status);
1942            pr["lastActivityDate"] = ts(Utc::now());
1943            Ok(())
1944        })
1945    }
1946
1947    fn describe_pull_request_events(
1948        &self,
1949        req: &AwsRequest,
1950    ) -> Result<AwsResponse, AwsServiceError> {
1951        let b = body(req);
1952        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
1953        check_enum(
1954            &b,
1955            "pullRequestEventType",
1956            validate::PULL_REQUEST_EVENT_TYPE,
1957            "InvalidPullRequestEventTypeException",
1958        )?;
1959        let account = self.account(req);
1960        let guard = self.state.read();
1961        let st = guard.get(&account);
1962        let s = st.ok_or_else(pr_not_found)?;
1963        if !s.pull_requests.contains_key(&id) {
1964            return Err(pr_not_found());
1965        }
1966        let events = s.pull_request_events.get(&id).cloned().unwrap_or_default();
1967        ok(json!({ "pullRequestEvents": events }))
1968    }
1969
1970    fn merge_pull_request_fast_forward(
1971        &self,
1972        req: &AwsRequest,
1973    ) -> Result<AwsResponse, AwsServiceError> {
1974        self.merge_pull_request(req, "FAST_FORWARD_MERGE")
1975    }
1976
1977    fn merge_pull_request_squash(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1978        self.merge_pull_request(req, "SQUASH_MERGE")
1979    }
1980
1981    fn merge_pull_request_three_way(
1982        &self,
1983        req: &AwsRequest,
1984    ) -> Result<AwsResponse, AwsServiceError> {
1985        self.merge_pull_request(req, "THREE_WAY_MERGE")
1986    }
1987
1988    fn merge_pull_request(
1989        &self,
1990        req: &AwsRequest,
1991        merge_option: &str,
1992    ) -> Result<AwsResponse, AwsServiceError> {
1993        let b = body(req);
1994        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
1995        let name = require_repository_name(&b)?;
1996        validate_merge_conflict_enums(&b)?;
1997        let author = caller_arn(req);
1998        let account = self.account(req);
1999        let mut guard = self.state.write();
2000        let st = guard.get_or_create(&account);
2001        if !st.repositories.contains_key(&name) {
2002            return Err(repo_not_found(&name));
2003        }
2004        let now = Utc::now();
2005
2006        // Read the pull request's target and rules without holding a mutable
2007        // borrow across the object-store mutation below.
2008        let (source_ref, dest_ref, revision, rules) = {
2009            let pr = st.pull_requests.get(&id).ok_or_else(pr_not_found)?;
2010            if pr.get("pullRequestStatus").and_then(Value::as_str) == Some("CLOSED") {
2011                return Err(err(
2012                    "PullRequestAlreadyClosedException",
2013                    "The pull request status cannot be updated because it is already closed.",
2014                ));
2015            }
2016            let target = pr
2017                .get("pullRequestTargets")
2018                .and_then(Value::as_array)
2019                .and_then(|ts| {
2020                    ts.iter().find(|t| {
2021                        t.get("repositoryName").and_then(Value::as_str) == Some(name.as_str())
2022                    })
2023                })
2024                .ok_or_else(|| {
2025                    err(
2026                        "RepositoryNotAssociatedWithPullRequestException",
2027                        "The repository is not associated with the pull request.",
2028                    )
2029                })?;
2030            let source_ref = target
2031                .get("sourceReference")
2032                .and_then(Value::as_str)
2033                .unwrap_or_default()
2034                .to_string();
2035            let dest_ref = target
2036                .get("destinationReference")
2037                .and_then(Value::as_str)
2038                .unwrap_or_default()
2039                .to_string();
2040            let revision = pr
2041                .get("revisionId")
2042                .and_then(Value::as_str)
2043                .unwrap_or_default()
2044                .to_string();
2045            let rules = pr
2046                .get("approvalRules")
2047                .and_then(Value::as_array)
2048                .cloned()
2049                .unwrap_or_default();
2050            (source_ref, dest_ref, revision, rules)
2051        };
2052
2053        // Enforce approval rules (unless overridden), exactly as AWS does before
2054        // allowing a merge.
2055        let overridden = st
2056            .pull_request_overrides
2057            .get(&id)
2058            .and_then(|m| m.get(&revision))
2059            .is_some();
2060        if !overridden && !rules.is_empty() {
2061            let approvers: std::collections::BTreeSet<String> = st
2062                .pull_request_approvals
2063                .get(&id)
2064                .and_then(|m| m.get(&revision))
2065                .map(|m| {
2066                    m.iter()
2067                        .filter(|(_, v)| v.as_str() == "APPROVE")
2068                        .map(|(arn, _)| arn.clone())
2069                        .collect()
2070                })
2071                .unwrap_or_default();
2072            let all_satisfied = rules.iter().all(|r| {
2073                let content = r
2074                    .get("approvalRuleContent")
2075                    .and_then(Value::as_str)
2076                    .unwrap_or_default();
2077                rule_satisfied(content, &approvers)
2078            });
2079            if !all_satisfied {
2080                return Err(err(
2081                    "PullRequestApprovalRulesNotSatisfiedException",
2082                    "The pull request cannot be merged because one or more approval rules have not been satisfied.",
2083                ));
2084            }
2085        }
2086
2087        // Resolve the current tips and perform the merge against the object
2088        // store, advancing the destination branch to the resulting commit.
2089        let strategy = str_field(&b, "conflictResolutionStrategy").unwrap_or_default();
2090        let repo = st.repositories.get_mut(&name).expect("repo checked above");
2091        let source_tip = repo.branches.get(&source_ref).cloned().ok_or_else(|| {
2092            err(
2093                "ReferenceDoesNotExistException",
2094                "The specified reference does not exist.",
2095            )
2096        })?;
2097        let dest_tip = repo.branches.get(&dest_ref).cloned().ok_or_else(|| {
2098            err(
2099                "ReferenceDoesNotExistException",
2100                "The specified reference does not exist.",
2101            )
2102        })?;
2103        // Concurrency check: if the caller pins a source tip it must still match.
2104        if let Some(pinned) = str_field(&b, "sourceCommitId") {
2105            if pinned != source_tip {
2106                return Err(err(
2107                    "TipOfSourceReferenceIsDifferentException",
2108                    "The tip of the source reference is different from the specified commit ID.",
2109                ));
2110            }
2111        }
2112        let merge_commit_id = match merge_option {
2113            "FAST_FORWARD_MERGE" => {
2114                if !is_ancestor(repo, &dest_tip, &source_tip) {
2115                    return Err(err(
2116                        "ManualMergeRequiredException",
2117                        "The fast-forward merge cannot be performed because the destination is not an ancestor of the source.",
2118                    ));
2119                }
2120                repo.branches.insert(dest_ref.clone(), source_tip.clone());
2121                source_tip.clone()
2122            }
2123            _ => {
2124                let merged =
2125                    merge_trees(repo, &source_tip, &dest_tip, &strategy).ok_or_else(|| {
2126                        err(
2127                            "ManualMergeRequiredException",
2128                            "The merge cannot be completed because there are merge conflicts that must be resolved manually.",
2129                        )
2130                    })?;
2131                let tree_id = tree_id_for(&merged);
2132                let commit_id = fresh_object_id();
2133                let parents = merge_parents(merge_option, &dest_tip, &source_tip);
2134                let commit = build_commit(&commit_id, &tree_id, &parents, &b, req);
2135                repo.commits.insert(commit_id.clone(), commit);
2136                repo.trees.insert(commit_id.clone(), merged);
2137                repo.branches.insert(dest_ref.clone(), commit_id.clone());
2138                commit_id
2139            }
2140        };
2141
2142        // Advancing the destination branch invalidates approvals on any other
2143        // open PR sourced from it.
2144        refresh_source_revisions(st, &name, &dest_ref);
2145
2146        // Close the pull request and record the real merge metadata.
2147        let pr = st.pull_requests.get_mut(&id).expect("pr checked above");
2148        pr["pullRequestStatus"] = json!("CLOSED");
2149        pr["lastActivityDate"] = ts(now);
2150        if let Some(targets) = pr
2151            .get_mut("pullRequestTargets")
2152            .and_then(Value::as_array_mut)
2153        {
2154            for t in targets.iter_mut() {
2155                if t.get("repositoryName").and_then(Value::as_str) == Some(name.as_str()) {
2156                    t["mergeMetadata"] = json!({
2157                        "isMerged": true,
2158                        "mergedBy": author,
2159                        "mergeCommitId": merge_commit_id,
2160                        "mergeOption": merge_option,
2161                    });
2162                }
2163            }
2164        }
2165        let pr = pr.clone();
2166        st.pull_request_events
2167            .entry(id.clone())
2168            .or_default()
2169            .push(pr_event(
2170                &id,
2171                "PULL_REQUEST_MERGE_STATE_CHANGED",
2172                &author,
2173                now,
2174                json!({}),
2175            ));
2176        ok(json!({ "pullRequest": pr }))
2177    }
2178
2179    // ----- Pull-request approval rules / states -----
2180
2181    fn create_pull_request_approval_rule(
2182        &self,
2183        req: &AwsRequest,
2184    ) -> Result<AwsResponse, AwsServiceError> {
2185        let b = body(req);
2186        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2187        let rule_name = require(&b, "approvalRuleName", "ApprovalRuleNameRequiredException")?;
2188        let content = require(
2189            &b,
2190            "approvalRuleContent",
2191            "ApprovalRuleContentRequiredException",
2192        )?;
2193        if rule_name.chars().count() > 100 {
2194            return Err(err(
2195                "InvalidApprovalRuleNameException",
2196                "The approval rule name is not valid.",
2197            ));
2198        }
2199        if content.chars().count() > 3000 {
2200            return Err(err(
2201                "InvalidApprovalRuleContentException",
2202                "The approval rule content is not valid.",
2203            ));
2204        }
2205        let content = normalize_rule_content(&content);
2206        let now = Utc::now();
2207        let user = caller_arn(req);
2208        let account = self.account(req);
2209        let mut guard = self.state.write();
2210        let st = guard.get_or_create(&account);
2211        let pr = st.pull_requests.get_mut(&id).ok_or_else(pr_not_found)?;
2212        if pr.get("pullRequestStatus").and_then(Value::as_str) == Some("CLOSED") {
2213            return Err(err(
2214                "PullRequestAlreadyClosedException",
2215                "The pull request is already closed.",
2216            ));
2217        }
2218        let rules = pr
2219            .get_mut("approvalRules")
2220            .and_then(Value::as_array_mut)
2221            .ok_or_else(pr_not_found)?;
2222        if rules
2223            .iter()
2224            .any(|r| r.get("approvalRuleName").and_then(Value::as_str) == Some(rule_name.as_str()))
2225        {
2226            return Err(err(
2227                "ApprovalRuleNameAlreadyExistsException",
2228                "An approval rule with that name already exists.",
2229            ));
2230        }
2231        let rule = json!({
2232            "approvalRuleId": new_uuid(),
2233            "approvalRuleName": rule_name,
2234            "approvalRuleContent": content,
2235            "ruleContentSha256": sha256_hex(content.as_bytes()),
2236            "lastModifiedDate": ts(now),
2237            "creationDate": ts(now),
2238            "lastModifiedUser": user,
2239        });
2240        rules.push(rule.clone());
2241        ok(json!({ "approvalRule": rule }))
2242    }
2243
2244    fn delete_pull_request_approval_rule(
2245        &self,
2246        req: &AwsRequest,
2247    ) -> Result<AwsResponse, AwsServiceError> {
2248        let b = body(req);
2249        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2250        let rule_name = require(&b, "approvalRuleName", "ApprovalRuleNameRequiredException")?;
2251        let account = self.account(req);
2252        let mut guard = self.state.write();
2253        let st = guard.get_or_create(&account);
2254        let pr = st.pull_requests.get_mut(&id).ok_or_else(pr_not_found)?;
2255        let mut deleted_id = String::new();
2256        if let Some(rules) = pr.get_mut("approvalRules").and_then(Value::as_array_mut) {
2257            if let Some(pos) = rules.iter().position(|r| {
2258                r.get("approvalRuleName").and_then(Value::as_str) == Some(rule_name.as_str())
2259            }) {
2260                deleted_id = rules[pos]
2261                    .get("approvalRuleId")
2262                    .and_then(Value::as_str)
2263                    .unwrap_or_default()
2264                    .to_string();
2265                rules.remove(pos);
2266            }
2267        }
2268        ok(json!({ "approvalRuleId": deleted_id }))
2269    }
2270
2271    fn update_pull_request_approval_rule_content(
2272        &self,
2273        req: &AwsRequest,
2274    ) -> Result<AwsResponse, AwsServiceError> {
2275        let b = body(req);
2276        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2277        let rule_name = require(&b, "approvalRuleName", "ApprovalRuleNameRequiredException")?;
2278        let content = require(&b, "newRuleContent", "ApprovalRuleContentRequiredException")?;
2279        if content.chars().count() > 3000 {
2280            return Err(err(
2281                "InvalidApprovalRuleContentException",
2282                "The approval rule content is not valid.",
2283            ));
2284        }
2285        let content = normalize_rule_content(&content);
2286        let now = Utc::now();
2287        let user = caller_arn(req);
2288        let account = self.account(req);
2289        let mut guard = self.state.write();
2290        let st = guard.get_or_create(&account);
2291        let pr = st.pull_requests.get_mut(&id).ok_or_else(pr_not_found)?;
2292        let rules = pr
2293            .get_mut("approvalRules")
2294            .and_then(Value::as_array_mut)
2295            .ok_or_else(pr_not_found)?;
2296        let rule = rules
2297            .iter_mut()
2298            .find(|r| r.get("approvalRuleName").and_then(Value::as_str) == Some(rule_name.as_str()))
2299            .ok_or_else(|| {
2300                err(
2301                    "ApprovalRuleDoesNotExistException",
2302                    "The specified approval rule does not exist.",
2303                )
2304            })?;
2305        rule["approvalRuleContent"] = json!(content);
2306        rule["ruleContentSha256"] = json!(sha256_hex(content.as_bytes()));
2307        rule["lastModifiedDate"] = ts(now);
2308        rule["lastModifiedUser"] = json!(user);
2309        let rule = rule.clone();
2310        ok(json!({ "approvalRule": rule }))
2311    }
2312
2313    fn update_pull_request_approval_state(
2314        &self,
2315        req: &AwsRequest,
2316    ) -> Result<AwsResponse, AwsServiceError> {
2317        let b = body(req);
2318        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2319        let revision = require(&b, "revisionId", "RevisionIdRequiredException")?;
2320        let state = require(&b, "approvalState", "ApprovalStateRequiredException")?;
2321        if !validate::is_enum(validate::APPROVAL_STATE, &state) {
2322            return Err(err(
2323                "InvalidApprovalStateException",
2324                "The approval state is not valid.",
2325            ));
2326        }
2327        let user = caller_arn(req);
2328        let account = self.account(req);
2329        let mut guard = self.state.write();
2330        let st = guard.get_or_create(&account);
2331        let pr = st.pull_requests.get(&id).ok_or_else(pr_not_found)?;
2332        if pr.get("pullRequestStatus").and_then(Value::as_str) == Some("CLOSED") {
2333            return Err(err(
2334                "PullRequestAlreadyClosedException",
2335                "The pull request is already closed.",
2336            ));
2337        }
2338        let current_revision = pr
2339            .get("revisionId")
2340            .and_then(Value::as_str)
2341            .unwrap_or_default()
2342            .to_string();
2343        if revision != current_revision {
2344            return Err(err(
2345                "RevisionNotCurrentException",
2346                "The revision ID is not the current revision of the pull request.",
2347            ));
2348        }
2349        if pr.get("authorArn").and_then(Value::as_str) == Some(user.as_str()) {
2350            return Err(err(
2351                "PullRequestCannotBeApprovedByAuthorException",
2352                "The pull request cannot be approved by the author of the pull request.",
2353            ));
2354        }
2355        st.pull_request_approvals
2356            .entry(id)
2357            .or_default()
2358            .entry(revision)
2359            .or_default()
2360            .insert(user, state);
2361        ok(json!({}))
2362    }
2363
2364    fn get_pull_request_approval_states(
2365        &self,
2366        req: &AwsRequest,
2367    ) -> Result<AwsResponse, AwsServiceError> {
2368        let b = body(req);
2369        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2370        let revision = require(&b, "revisionId", "RevisionIdRequiredException")?;
2371        let account = self.account(req);
2372        let guard = self.state.read();
2373        let st = guard.get(&account);
2374        let s = st.ok_or_else(pr_not_found)?;
2375        if !s.pull_requests.contains_key(&id) {
2376            return Err(pr_not_found());
2377        }
2378        let approvals: Vec<Value> = s
2379            .pull_request_approvals
2380            .get(&id)
2381            .and_then(|m| m.get(&revision))
2382            .map(|m| {
2383                m.iter()
2384                    .filter(|(_, v)| v.as_str() == "APPROVE")
2385                    .map(|(arn, v)| json!({ "userArn": arn, "approvalState": v }))
2386                    .collect()
2387            })
2388            .unwrap_or_default();
2389        ok(json!({ "approvals": approvals }))
2390    }
2391
2392    fn evaluate_pull_request_approval_rules(
2393        &self,
2394        req: &AwsRequest,
2395    ) -> Result<AwsResponse, AwsServiceError> {
2396        let b = body(req);
2397        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2398        let revision = require(&b, "revisionId", "RevisionIdRequiredException")?;
2399        let account = self.account(req);
2400        let guard = self.state.read();
2401        let st = guard.get(&account);
2402        let s = st.ok_or_else(pr_not_found)?;
2403        let pr = s.pull_requests.get(&id).ok_or_else(pr_not_found)?;
2404        let current_revision = pr
2405            .get("revisionId")
2406            .and_then(Value::as_str)
2407            .unwrap_or_default();
2408        if revision != current_revision {
2409            return Err(err(
2410                "RevisionNotCurrentException",
2411                "The revision ID is not the current revision of the pull request.",
2412            ));
2413        }
2414        let overridden = s
2415            .pull_request_overrides
2416            .get(&id)
2417            .and_then(|m| m.get(&revision))
2418            .is_some();
2419        let rules = pr
2420            .get("approvalRules")
2421            .and_then(Value::as_array)
2422            .cloned()
2423            .unwrap_or_default();
2424        // Distinct reviewers who approved the current revision.
2425        let approvers: std::collections::BTreeSet<String> = s
2426            .pull_request_approvals
2427            .get(&id)
2428            .and_then(|m| m.get(&revision))
2429            .map(|m| {
2430                m.iter()
2431                    .filter(|(_, v)| v.as_str() == "APPROVE")
2432                    .map(|(arn, _)| arn.clone())
2433                    .collect()
2434            })
2435            .unwrap_or_default();
2436        let mut satisfied_names = Vec::new();
2437        let mut not_satisfied_names = Vec::new();
2438        for r in &rules {
2439            let Some(name) = r.get("approvalRuleName").cloned() else {
2440                continue;
2441            };
2442            let content = r
2443                .get("approvalRuleContent")
2444                .and_then(Value::as_str)
2445                .unwrap_or_default();
2446            // An override satisfies every rule; otherwise honor the rule's
2447            // NumberOfApprovalsNeeded and approver pool.
2448            if overridden || rule_satisfied(content, &approvers) {
2449                satisfied_names.push(name);
2450            } else {
2451                not_satisfied_names.push(name);
2452            }
2453        }
2454        let approved = not_satisfied_names.is_empty();
2455        ok(json!({
2456            "evaluation": {
2457                "approved": approved,
2458                "overridden": overridden,
2459                "approvalRulesSatisfied": satisfied_names,
2460                "approvalRulesNotSatisfied": not_satisfied_names,
2461            }
2462        }))
2463    }
2464
2465    fn override_pull_request_approval_rules(
2466        &self,
2467        req: &AwsRequest,
2468    ) -> Result<AwsResponse, AwsServiceError> {
2469        let b = body(req);
2470        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2471        let revision = require(&b, "revisionId", "RevisionIdRequiredException")?;
2472        let status = require(&b, "overrideStatus", "OverrideStatusRequiredException")?;
2473        if !validate::is_enum(validate::OVERRIDE_STATUS, &status) {
2474            return Err(err(
2475                "InvalidOverrideStatusException",
2476                "The override status is not valid.",
2477            ));
2478        }
2479        let user = caller_arn(req);
2480        let account = self.account(req);
2481        let mut guard = self.state.write();
2482        let st = guard.get_or_create(&account);
2483        {
2484            let pr = st.pull_requests.get(&id).ok_or_else(pr_not_found)?;
2485            let current_revision = pr
2486                .get("revisionId")
2487                .and_then(Value::as_str)
2488                .unwrap_or_default();
2489            if revision != current_revision {
2490                return Err(err(
2491                    "RevisionNotCurrentException",
2492                    "The revision ID is not the current revision of the pull request.",
2493                ));
2494            }
2495        }
2496        let overrides = st.pull_request_overrides.entry(id).or_default();
2497        if status == "OVERRIDE" {
2498            if overrides.contains_key(&revision) {
2499                return Err(err(
2500                    "OverrideAlreadySetException",
2501                    "The override status is already set.",
2502                ));
2503            }
2504            overrides.insert(revision, user);
2505        } else {
2506            overrides.remove(&revision);
2507        }
2508        ok(json!({}))
2509    }
2510
2511    fn get_pull_request_override_state(
2512        &self,
2513        req: &AwsRequest,
2514    ) -> Result<AwsResponse, AwsServiceError> {
2515        let b = body(req);
2516        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2517        let revision = require(&b, "revisionId", "RevisionIdRequiredException")?;
2518        let account = self.account(req);
2519        let guard = self.state.read();
2520        let st = guard.get(&account);
2521        let s = st.ok_or_else(pr_not_found)?;
2522        if !s.pull_requests.contains_key(&id) {
2523            return Err(pr_not_found());
2524        }
2525        match s
2526            .pull_request_overrides
2527            .get(&id)
2528            .and_then(|m| m.get(&revision))
2529        {
2530            Some(arn) => ok(json!({ "overridden": true, "overrider": arn })),
2531            None => ok(json!({ "overridden": false })),
2532        }
2533    }
2534
2535    // ----- Approval-rule templates -----
2536
2537    fn create_approval_rule_template(
2538        &self,
2539        req: &AwsRequest,
2540    ) -> Result<AwsResponse, AwsServiceError> {
2541        let b = body(req);
2542        let name = require(
2543            &b,
2544            "approvalRuleTemplateName",
2545            "ApprovalRuleTemplateNameRequiredException",
2546        )?;
2547        let content = require(
2548            &b,
2549            "approvalRuleTemplateContent",
2550            "ApprovalRuleTemplateContentRequiredException",
2551        )?;
2552        if name.chars().count() > 100 {
2553            return Err(err(
2554                "InvalidApprovalRuleTemplateNameException",
2555                "The approval rule template name is not valid.",
2556            ));
2557        }
2558        if content.chars().count() > 3000 {
2559            return Err(err(
2560                "InvalidApprovalRuleTemplateContentException",
2561                "The approval rule template content is not valid.",
2562            ));
2563        }
2564        let content = normalize_rule_content(&content);
2565        if let Some(d) = str_field(&b, "approvalRuleTemplateDescription") {
2566            if d.chars().count() > 1000 {
2567                return Err(err(
2568                    "InvalidApprovalRuleTemplateDescriptionException",
2569                    "The approval rule template description is not valid.",
2570                ));
2571            }
2572        }
2573        let now = Utc::now();
2574        let user = caller_arn(req);
2575        let account = self.account(req);
2576        let mut guard = self.state.write();
2577        let st = guard.get_or_create(&account);
2578        if st.templates.contains_key(&name) {
2579            return Err(err(
2580                "ApprovalRuleTemplateNameAlreadyExistsException",
2581                format!("An approval rule template with the name {name} already exists."),
2582            ));
2583        }
2584        let mut tmpl = Map::new();
2585        tmpl.insert("approvalRuleTemplateId".into(), json!(new_uuid()));
2586        tmpl.insert("approvalRuleTemplateName".into(), json!(name));
2587        if let Some(d) = str_field(&b, "approvalRuleTemplateDescription") {
2588            tmpl.insert("approvalRuleTemplateDescription".into(), json!(d));
2589        }
2590        tmpl.insert("approvalRuleTemplateContent".into(), json!(content));
2591        tmpl.insert(
2592            "ruleContentSha256".into(),
2593            json!(sha256_hex(content.as_bytes())),
2594        );
2595        tmpl.insert("lastModifiedDate".into(), ts(now));
2596        tmpl.insert("creationDate".into(), ts(now));
2597        tmpl.insert("lastModifiedUser".into(), json!(user));
2598        let tmpl = Value::Object(tmpl);
2599        st.templates.insert(name.clone(), tmpl.clone());
2600        st.template_order.push(name);
2601        ok(json!({ "approvalRuleTemplate": tmpl }))
2602    }
2603
2604    fn get_approval_rule_template(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
2605        let b = body(req);
2606        let name = require(
2607            &b,
2608            "approvalRuleTemplateName",
2609            "ApprovalRuleTemplateNameRequiredException",
2610        )?;
2611        let account = self.account(req);
2612        let guard = self.state.read();
2613        let st = guard.get(&account);
2614        match st.and_then(|s| s.templates.get(&name)) {
2615            Some(t) => ok(json!({ "approvalRuleTemplate": t })),
2616            None => Err(template_not_found()),
2617        }
2618    }
2619
2620    fn delete_approval_rule_template(
2621        &self,
2622        req: &AwsRequest,
2623    ) -> Result<AwsResponse, AwsServiceError> {
2624        let b = body(req);
2625        let name = require(
2626            &b,
2627            "approvalRuleTemplateName",
2628            "ApprovalRuleTemplateNameRequiredException",
2629        )?;
2630        if name.chars().count() > 100 {
2631            return Err(err(
2632                "InvalidApprovalRuleTemplateNameException",
2633                "The approval rule template name is not valid.",
2634            ));
2635        }
2636        let account = self.account(req);
2637        let mut guard = self.state.write();
2638        let st = guard.get_or_create(&account);
2639        let id = st
2640            .templates
2641            .remove(&name)
2642            .and_then(|t| {
2643                t.get("approvalRuleTemplateId")
2644                    .and_then(Value::as_str)
2645                    .map(str::to_string)
2646            })
2647            .unwrap_or_default();
2648        st.template_order.retain(|n| n != &name);
2649        for repo in st.repositories.values_mut() {
2650            repo.associated_templates.retain(|t| t != &name);
2651        }
2652        ok(json!({ "approvalRuleTemplateId": id }))
2653    }
2654
2655    fn list_approval_rule_templates(
2656        &self,
2657        req: &AwsRequest,
2658    ) -> Result<AwsResponse, AwsServiceError> {
2659        let account = self.account(req);
2660        let guard = self.state.read();
2661        let st = guard.get(&account);
2662        let names: Vec<Value> = st
2663            .map(|s| s.template_order.iter().map(|n| json!(n)).collect())
2664            .unwrap_or_default();
2665        ok(json!({ "approvalRuleTemplateNames": names }))
2666    }
2667
2668    fn update_approval_rule_template_content(
2669        &self,
2670        req: &AwsRequest,
2671    ) -> Result<AwsResponse, AwsServiceError> {
2672        let b = body(req);
2673        let name = require(
2674            &b,
2675            "approvalRuleTemplateName",
2676            "ApprovalRuleTemplateNameRequiredException",
2677        )?;
2678        let content = require(
2679            &b,
2680            "newRuleContent",
2681            "ApprovalRuleTemplateContentRequiredException",
2682        )?;
2683        if content.chars().count() > 3000 {
2684            return Err(err(
2685                "InvalidApprovalRuleTemplateContentException",
2686                "The approval rule template content is not valid.",
2687            ));
2688        }
2689        let content = normalize_rule_content(&content);
2690        self.mutate_template(req, &name, |t| {
2691            t["approvalRuleTemplateContent"] = json!(content);
2692            t["ruleContentSha256"] = json!(sha256_hex(content.as_bytes()));
2693            t["lastModifiedDate"] = ts(Utc::now());
2694            Ok(())
2695        })
2696    }
2697
2698    fn update_approval_rule_template_description(
2699        &self,
2700        req: &AwsRequest,
2701    ) -> Result<AwsResponse, AwsServiceError> {
2702        let b = body(req);
2703        let name = require(
2704            &b,
2705            "approvalRuleTemplateName",
2706            "ApprovalRuleTemplateNameRequiredException",
2707        )?;
2708        let desc = require(
2709            &b,
2710            "approvalRuleTemplateDescription",
2711            "InvalidApprovalRuleTemplateDescriptionException",
2712        )?;
2713        if desc.chars().count() > 1000 {
2714            return Err(err(
2715                "InvalidApprovalRuleTemplateDescriptionException",
2716                "The approval rule template description is not valid.",
2717            ));
2718        }
2719        self.mutate_template(req, &name, |t| {
2720            t["approvalRuleTemplateDescription"] = json!(desc);
2721            t["lastModifiedDate"] = ts(Utc::now());
2722            Ok(())
2723        })
2724    }
2725
2726    fn update_approval_rule_template_name(
2727        &self,
2728        req: &AwsRequest,
2729    ) -> Result<AwsResponse, AwsServiceError> {
2730        let b = body(req);
2731        let old = require(
2732            &b,
2733            "oldApprovalRuleTemplateName",
2734            "ApprovalRuleTemplateNameRequiredException",
2735        )?;
2736        let new = require(
2737            &b,
2738            "newApprovalRuleTemplateName",
2739            "ApprovalRuleTemplateNameRequiredException",
2740        )?;
2741        if new.chars().count() > 100 {
2742            return Err(err(
2743                "InvalidApprovalRuleTemplateNameException",
2744                "The approval rule template name is not valid.",
2745            ));
2746        }
2747        let account = self.account(req);
2748        let mut guard = self.state.write();
2749        let st = guard.get_or_create(&account);
2750        if !st.templates.contains_key(&old) {
2751            return Err(template_not_found());
2752        }
2753        if old != new && st.templates.contains_key(&new) {
2754            return Err(err(
2755                "ApprovalRuleTemplateNameAlreadyExistsException",
2756                format!("An approval rule template with the name {new} already exists."),
2757            ));
2758        }
2759        let mut tmpl = st.templates.remove(&old).unwrap();
2760        tmpl["approvalRuleTemplateName"] = json!(new);
2761        tmpl["lastModifiedDate"] = ts(Utc::now());
2762        for n in &mut st.template_order {
2763            if n == &old {
2764                *n = new.clone();
2765            }
2766        }
2767        for repo in st.repositories.values_mut() {
2768            for t in &mut repo.associated_templates {
2769                if t == &old {
2770                    *t = new.clone();
2771                }
2772            }
2773        }
2774        st.templates.insert(new, tmpl.clone());
2775        ok(json!({ "approvalRuleTemplate": tmpl }))
2776    }
2777
2778    fn associate_template_with_repository(
2779        &self,
2780        req: &AwsRequest,
2781    ) -> Result<AwsResponse, AwsServiceError> {
2782        let b = body(req);
2783        let tmpl = require(
2784            &b,
2785            "approvalRuleTemplateName",
2786            "ApprovalRuleTemplateNameRequiredException",
2787        )?;
2788        let name = require_repository_name(&b)?;
2789        let account = self.account(req);
2790        let mut guard = self.state.write();
2791        let st = guard.get_or_create(&account);
2792        if !st.templates.contains_key(&tmpl) {
2793            return Err(template_not_found());
2794        }
2795        let repo = st
2796            .repositories
2797            .get_mut(&name)
2798            .ok_or_else(|| repo_not_found(&name))?;
2799        if !repo.associated_templates.contains(&tmpl) {
2800            repo.associated_templates.push(tmpl);
2801        }
2802        ok(json!({}))
2803    }
2804
2805    fn disassociate_template_from_repository(
2806        &self,
2807        req: &AwsRequest,
2808    ) -> Result<AwsResponse, AwsServiceError> {
2809        let b = body(req);
2810        let tmpl = require(
2811            &b,
2812            "approvalRuleTemplateName",
2813            "ApprovalRuleTemplateNameRequiredException",
2814        )?;
2815        let name = require_repository_name(&b)?;
2816        let account = self.account(req);
2817        let mut guard = self.state.write();
2818        let st = guard.get_or_create(&account);
2819        if !st.templates.contains_key(&tmpl) {
2820            return Err(template_not_found());
2821        }
2822        let repo = st
2823            .repositories
2824            .get_mut(&name)
2825            .ok_or_else(|| repo_not_found(&name))?;
2826        repo.associated_templates.retain(|t| t != &tmpl);
2827        ok(json!({}))
2828    }
2829
2830    fn batch_associate_template(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
2831        let b = body(req);
2832        let tmpl = require(
2833            &b,
2834            "approvalRuleTemplateName",
2835            "ApprovalRuleTemplateNameRequiredException",
2836        )?;
2837        let names = b
2838            .get("repositoryNames")
2839            .and_then(Value::as_array)
2840            .ok_or_else(|| {
2841                err(
2842                    "RepositoryNamesRequiredException",
2843                    "Repository names are required.",
2844                )
2845            })?
2846            .clone();
2847        let account = self.account(req);
2848        let mut guard = self.state.write();
2849        let st = guard.get_or_create(&account);
2850        if !st.templates.contains_key(&tmpl) {
2851            return Err(template_not_found());
2852        }
2853        let mut associated = Vec::new();
2854        let mut errors = Vec::new();
2855        for n in &names {
2856            let Some(rn) = n.as_str() else { continue };
2857            match st.repositories.get_mut(rn) {
2858                Some(repo) => {
2859                    if !repo.associated_templates.contains(&tmpl) {
2860                        repo.associated_templates.push(tmpl.clone());
2861                    }
2862                    associated.push(json!(rn));
2863                }
2864                None => errors.push(json!({
2865                    "repositoryName": rn,
2866                    "errorCode": "RepositoryDoesNotExist",
2867                    "errorMessage": format!("The repository {rn} does not exist."),
2868                })),
2869            }
2870        }
2871        ok(json!({ "associatedRepositoryNames": associated, "errors": errors }))
2872    }
2873
2874    fn batch_disassociate_template(
2875        &self,
2876        req: &AwsRequest,
2877    ) -> Result<AwsResponse, AwsServiceError> {
2878        let b = body(req);
2879        let tmpl = require(
2880            &b,
2881            "approvalRuleTemplateName",
2882            "ApprovalRuleTemplateNameRequiredException",
2883        )?;
2884        let names = b
2885            .get("repositoryNames")
2886            .and_then(Value::as_array)
2887            .ok_or_else(|| {
2888                err(
2889                    "RepositoryNamesRequiredException",
2890                    "Repository names are required.",
2891                )
2892            })?
2893            .clone();
2894        let account = self.account(req);
2895        let mut guard = self.state.write();
2896        let st = guard.get_or_create(&account);
2897        if !st.templates.contains_key(&tmpl) {
2898            return Err(template_not_found());
2899        }
2900        let mut disassociated = Vec::new();
2901        let mut errors = Vec::new();
2902        for n in &names {
2903            let Some(rn) = n.as_str() else { continue };
2904            match st.repositories.get_mut(rn) {
2905                Some(repo) => {
2906                    repo.associated_templates.retain(|t| t != &tmpl);
2907                    disassociated.push(json!(rn));
2908                }
2909                None => errors.push(json!({
2910                    "repositoryName": rn,
2911                    "errorCode": "RepositoryDoesNotExist",
2912                    "errorMessage": format!("The repository {rn} does not exist."),
2913                })),
2914            }
2915        }
2916        ok(json!({ "disassociatedRepositoryNames": disassociated, "errors": errors }))
2917    }
2918
2919    fn list_associated_templates_for_repository(
2920        &self,
2921        req: &AwsRequest,
2922    ) -> Result<AwsResponse, AwsServiceError> {
2923        let b = body(req);
2924        let name = require_repository_name(&b)?;
2925        let account = self.account(req);
2926        let guard = self.state.read();
2927        let st = guard.get(&account);
2928        let repo = st
2929            .and_then(|s| s.repositories.get(&name))
2930            .ok_or_else(|| repo_not_found(&name))?;
2931        let names: Vec<Value> = repo.associated_templates.iter().map(|t| json!(t)).collect();
2932        ok(json!({ "approvalRuleTemplateNames": names }))
2933    }
2934
2935    fn list_repositories_for_template(
2936        &self,
2937        req: &AwsRequest,
2938    ) -> Result<AwsResponse, AwsServiceError> {
2939        let b = body(req);
2940        let tmpl = require(
2941            &b,
2942            "approvalRuleTemplateName",
2943            "ApprovalRuleTemplateNameRequiredException",
2944        )?;
2945        let account = self.account(req);
2946        let guard = self.state.read();
2947        let st = guard.get(&account);
2948        let s = st.ok_or_else(template_not_found)?;
2949        if !s.templates.contains_key(&tmpl) {
2950            return Err(template_not_found());
2951        }
2952        let names: Vec<Value> = s
2953            .repository_order
2954            .iter()
2955            .filter(|n| {
2956                s.repositories
2957                    .get(*n)
2958                    .map(|r| r.associated_templates.contains(&tmpl))
2959                    .unwrap_or(false)
2960            })
2961            .map(|n| json!(n))
2962            .collect();
2963        ok(json!({ "repositoryNames": names }))
2964    }
2965
2966    // ----- Comments -----
2967
2968    fn post_comment_for_compared_commit(
2969        &self,
2970        req: &AwsRequest,
2971    ) -> Result<AwsResponse, AwsServiceError> {
2972        let b = body(req);
2973        let name = require_repository_name(&b)?;
2974        let after = require(&b, "afterCommitId", "CommitIdRequiredException")?;
2975        let content = require(&b, "content", "CommentContentRequiredException")?;
2976        if content.chars().count() > 10240 {
2977            return Err(err(
2978                "CommentContentSizeLimitExceededException",
2979                "The comment is too large. Comments are limited to 10,240 characters.",
2980            ));
2981        }
2982        let before = str_field(&b, "beforeCommitId");
2983        if before.as_deref() == Some(after.as_str()) {
2984            return Err(err(
2985                "BeforeCommitIdAndAfterCommitIdAreSameException",
2986                "The before commit ID and the after commit ID are the same.",
2987            ));
2988        }
2989        let account = self.account(req);
2990        let mut guard = self.state.write();
2991        let st = guard.get_or_create(&account);
2992        if !st.repositories.contains_key(&name) {
2993            return Err(repo_not_found(&name));
2994        }
2995        let now = Utc::now();
2996        let author = caller_arn(req);
2997        let comment_id = new_uuid();
2998        let location = b.get("location").cloned();
2999        let mut stored = comment_value(&comment_id, &content, &author, now, None);
3000        if let Some(obj) = stored.as_object_mut() {
3001            obj.insert("_ctxRepositoryName".into(), json!(name));
3002            obj.insert("_ctxAfterCommitId".into(), json!(after));
3003            if let Some(bc) = &before {
3004                obj.insert("_ctxBeforeCommitId".into(), json!(bc));
3005            }
3006            if let Some(loc) = &location {
3007                obj.insert("_ctxLocation".into(), loc.clone());
3008            }
3009        }
3010        st.comments.insert(comment_id.clone(), stored.clone());
3011        st.comment_order.push(comment_id);
3012        let public = public_comment(&stored);
3013        let mut out = Map::new();
3014        out.insert("repositoryName".into(), json!(name));
3015        if let Some(bc) = before {
3016            out.insert("beforeCommitId".into(), json!(bc));
3017        }
3018        out.insert("afterCommitId".into(), json!(after));
3019        if let Some(loc) = location {
3020            out.insert("location".into(), loc);
3021        }
3022        out.insert("comment".into(), public);
3023        ok(Value::Object(out))
3024    }
3025
3026    fn post_comment_for_pull_request(
3027        &self,
3028        req: &AwsRequest,
3029    ) -> Result<AwsResponse, AwsServiceError> {
3030        let b = body(req);
3031        let pr_id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
3032        let name = require_repository_name(&b)?;
3033        let before = require(&b, "beforeCommitId", "CommitIdRequiredException")?;
3034        let after = require(&b, "afterCommitId", "CommitIdRequiredException")?;
3035        let content = require(&b, "content", "CommentContentRequiredException")?;
3036        if content.chars().count() > 10240 {
3037            return Err(err(
3038                "CommentContentSizeLimitExceededException",
3039                "The comment is too large.",
3040            ));
3041        }
3042        let account = self.account(req);
3043        let mut guard = self.state.write();
3044        let st = guard.get_or_create(&account);
3045        if !st.repositories.contains_key(&name) {
3046            return Err(repo_not_found(&name));
3047        }
3048        if !st.pull_requests.contains_key(&pr_id) {
3049            return Err(pr_not_found());
3050        }
3051        let now = Utc::now();
3052        let author = caller_arn(req);
3053        let comment_id = new_uuid();
3054        let location = b.get("location").cloned();
3055        let mut stored = comment_value(&comment_id, &content, &author, now, None);
3056        if let Some(obj) = stored.as_object_mut() {
3057            obj.insert("_ctxRepositoryName".into(), json!(name));
3058            obj.insert("_ctxPullRequestId".into(), json!(pr_id));
3059            obj.insert("_ctxBeforeCommitId".into(), json!(before));
3060            obj.insert("_ctxAfterCommitId".into(), json!(after));
3061            if let Some(loc) = &location {
3062                obj.insert("_ctxLocation".into(), loc.clone());
3063            }
3064        }
3065        st.comments.insert(comment_id.clone(), stored.clone());
3066        st.comment_order.push(comment_id);
3067        let public = public_comment(&stored);
3068        let mut out = Map::new();
3069        out.insert("repositoryName".into(), json!(name));
3070        out.insert("pullRequestId".into(), json!(pr_id));
3071        out.insert("beforeCommitId".into(), json!(before));
3072        out.insert("afterCommitId".into(), json!(after));
3073        if let Some(loc) = location {
3074            out.insert("location".into(), loc);
3075        }
3076        out.insert("comment".into(), public);
3077        ok(Value::Object(out))
3078    }
3079
3080    fn post_comment_reply(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3081        let b = body(req);
3082        let in_reply_to = require(&b, "inReplyTo", "CommentIdRequiredException")?;
3083        let content = require(&b, "content", "CommentContentRequiredException")?;
3084        if content.chars().count() > 10240 {
3085            return Err(err(
3086                "CommentContentSizeLimitExceededException",
3087                "The comment is too large.",
3088            ));
3089        }
3090        let account = self.account(req);
3091        let mut guard = self.state.write();
3092        let st = guard.get_or_create(&account);
3093        let parent = st.comments.get(&in_reply_to).cloned().ok_or_else(|| {
3094            err(
3095                "CommentDoesNotExistException",
3096                "The specified comment does not exist.",
3097            )
3098        })?;
3099        let now = Utc::now();
3100        let author = caller_arn(req);
3101        let comment_id = new_uuid();
3102        let mut stored = comment_value(&comment_id, &content, &author, now, Some(&in_reply_to));
3103        // Inherit the parent's thread context.
3104        if let (Some(obj), Some(pobj)) = (stored.as_object_mut(), parent.as_object()) {
3105            for (k, v) in pobj {
3106                if k.starts_with("_ctx") {
3107                    obj.insert(k.clone(), v.clone());
3108                }
3109            }
3110        }
3111        st.comments.insert(comment_id.clone(), stored.clone());
3112        st.comment_order.push(comment_id);
3113        ok(json!({ "comment": public_comment(&stored) }))
3114    }
3115
3116    fn get_comment(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3117        let b = body(req);
3118        let id = require(&b, "commentId", "CommentIdRequiredException")?;
3119        let account = self.account(req);
3120        let guard = self.state.read();
3121        let st = guard.get(&account);
3122        let comment = st.and_then(|s| s.comments.get(&id)).ok_or_else(|| {
3123            err(
3124                "CommentDoesNotExistException",
3125                "The specified comment does not exist.",
3126            )
3127        })?;
3128        if comment.get("deleted").and_then(Value::as_bool) == Some(true) {
3129            return Err(err(
3130                "CommentDeletedException",
3131                "This comment has already been deleted.",
3132            ));
3133        }
3134        ok(json!({ "comment": public_comment(comment) }))
3135    }
3136
3137    fn get_comments_for_compared_commit(
3138        &self,
3139        req: &AwsRequest,
3140    ) -> Result<AwsResponse, AwsServiceError> {
3141        let b = body(req);
3142        let name = require_repository_name(&b)?;
3143        let after = require(&b, "afterCommitId", "CommitIdRequiredException")?;
3144        let before = str_field(&b, "beforeCommitId");
3145        let account = self.account(req);
3146        let guard = self.state.read();
3147        let st = guard.get(&account);
3148        let s = st.ok_or_else(|| repo_not_found(&name))?;
3149        if !s.repositories.contains_key(&name) {
3150            return Err(repo_not_found(&name));
3151        }
3152        let mut threads: Vec<Value> = Vec::new();
3153        for cid in &s.comment_order {
3154            let Some(c) = s.comments.get(cid) else {
3155                continue;
3156            };
3157            if c.get("_ctxPullRequestId").is_some() {
3158                continue;
3159            }
3160            if c.get("_ctxRepositoryName").and_then(Value::as_str) != Some(name.as_str()) {
3161                continue;
3162            }
3163            if c.get("_ctxAfterCommitId").and_then(Value::as_str) != Some(after.as_str()) {
3164                continue;
3165            }
3166            if let Some(bc) = &before {
3167                if c.get("_ctxBeforeCommitId").and_then(Value::as_str) != Some(bc.as_str()) {
3168                    continue;
3169                }
3170            }
3171            threads.push(json!({
3172                "repositoryName": name,
3173                "beforeCommitId": before,
3174                "afterCommitId": after,
3175                "location": c.get("_ctxLocation").cloned().unwrap_or(Value::Null),
3176                "comments": [public_comment(c)],
3177            }));
3178        }
3179        ok(json!({ "commentsForComparedCommitData": threads }))
3180    }
3181
3182    fn get_comments_for_pull_request(
3183        &self,
3184        req: &AwsRequest,
3185    ) -> Result<AwsResponse, AwsServiceError> {
3186        let b = body(req);
3187        let pr_id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
3188        let account = self.account(req);
3189        let guard = self.state.read();
3190        let st = guard.get(&account);
3191        let s = st.ok_or_else(pr_not_found)?;
3192        if !s.pull_requests.contains_key(&pr_id) {
3193            return Err(pr_not_found());
3194        }
3195        let mut threads: Vec<Value> = Vec::new();
3196        for cid in &s.comment_order {
3197            let Some(c) = s.comments.get(cid) else {
3198                continue;
3199            };
3200            if c.get("_ctxPullRequestId").and_then(Value::as_str) != Some(pr_id.as_str()) {
3201                continue;
3202            }
3203            threads.push(json!({
3204                "pullRequestId": pr_id,
3205                "repositoryName": c.get("_ctxRepositoryName").cloned().unwrap_or(Value::Null),
3206                "beforeCommitId": c.get("_ctxBeforeCommitId").cloned().unwrap_or(Value::Null),
3207                "afterCommitId": c.get("_ctxAfterCommitId").cloned().unwrap_or(Value::Null),
3208                "location": c.get("_ctxLocation").cloned().unwrap_or(Value::Null),
3209                "comments": [public_comment(c)],
3210            }));
3211        }
3212        ok(json!({ "commentsForPullRequestData": threads }))
3213    }
3214
3215    fn update_comment(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3216        let b = body(req);
3217        let id = require(&b, "commentId", "CommentIdRequiredException")?;
3218        let content = require(&b, "content", "CommentContentRequiredException")?;
3219        if content.chars().count() > 10240 {
3220            return Err(err(
3221                "CommentContentSizeLimitExceededException",
3222                "The comment is too large.",
3223            ));
3224        }
3225        let caller = caller_arn(req);
3226        let account = self.account(req);
3227        let mut guard = self.state.write();
3228        let st = guard.get_or_create(&account);
3229        let comment = st.comments.get_mut(&id).ok_or_else(|| {
3230            err(
3231                "CommentDoesNotExistException",
3232                "The specified comment does not exist.",
3233            )
3234        })?;
3235        if comment.get("deleted").and_then(Value::as_bool) == Some(true) {
3236            return Err(err(
3237                "CommentDeletedException",
3238                "This comment has already been deleted.",
3239            ));
3240        }
3241        if comment.get("authorArn").and_then(Value::as_str) != Some(caller.as_str()) {
3242            return Err(err(
3243                "CommentNotCreatedByCallerException",
3244                "You cannot modify a comment that was not created by you.",
3245            ));
3246        }
3247        comment["content"] = json!(content);
3248        comment["lastModifiedDate"] = ts(Utc::now());
3249        ok(json!({ "comment": public_comment(comment) }))
3250    }
3251
3252    fn delete_comment_content(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3253        let b = body(req);
3254        let id = require(&b, "commentId", "CommentIdRequiredException")?;
3255        let account = self.account(req);
3256        let mut guard = self.state.write();
3257        let st = guard.get_or_create(&account);
3258        let comment = st.comments.get_mut(&id).ok_or_else(|| {
3259            err(
3260                "CommentDoesNotExistException",
3261                "The specified comment does not exist.",
3262            )
3263        })?;
3264        comment["deleted"] = json!(true);
3265        comment["content"] = json!("");
3266        comment["lastModifiedDate"] = ts(Utc::now());
3267        ok(json!({ "comment": public_comment(comment) }))
3268    }
3269
3270    fn put_comment_reaction(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3271        let b = body(req);
3272        let id = require(&b, "commentId", "CommentIdRequiredException")?;
3273        let reaction = require(&b, "reactionValue", "ReactionValueRequiredException")?;
3274        if !validate::REACTION_EMOJIS.contains(&reaction.as_str())
3275            && !reaction.starts_with(":")
3276            && !reaction.starts_with("\\u")
3277        {
3278            return Err(err(
3279                "InvalidReactionValueException",
3280                "The reaction value is not valid.",
3281            ));
3282        }
3283        let user = caller_arn(req);
3284        let account = self.account(req);
3285        let mut guard = self.state.write();
3286        let st = guard.get_or_create(&account);
3287        let comment = st.comments.get_mut(&id).ok_or_else(|| {
3288            err(
3289                "CommentDoesNotExistException",
3290                "The specified comment does not exist.",
3291            )
3292        })?;
3293        if comment.get("deleted").and_then(Value::as_bool) == Some(true) {
3294            return Err(err(
3295                "CommentDeletedException",
3296                "This comment has already been deleted.",
3297            ));
3298        }
3299        let reactions = comment
3300            .as_object_mut()
3301            .unwrap()
3302            .entry("_ctxReactions".to_string())
3303            .or_insert_with(|| json!({}));
3304        if let Some(obj) = reactions.as_object_mut() {
3305            let users = obj.entry(reaction).or_insert_with(|| json!([]));
3306            if let Some(arr) = users.as_array_mut() {
3307                if !arr.iter().any(|u| u.as_str() == Some(user.as_str())) {
3308                    arr.push(json!(user));
3309                }
3310            }
3311        }
3312        ok(json!({}))
3313    }
3314
3315    fn get_comment_reactions(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3316        let b = body(req);
3317        let id = require(&b, "commentId", "CommentIdRequiredException")?;
3318        let account = self.account(req);
3319        let guard = self.state.read();
3320        let st = guard.get(&account);
3321        let comment = st.and_then(|s| s.comments.get(&id)).ok_or_else(|| {
3322            err(
3323                "CommentDoesNotExistException",
3324                "The specified comment does not exist.",
3325            )
3326        })?;
3327        let mut reactions = Vec::new();
3328        if let Some(obj) = comment.get("_ctxReactions").and_then(Value::as_object) {
3329            for (emoji, users) in obj {
3330                let user_list: Vec<&str> = users
3331                    .as_array()
3332                    .map(|a| a.iter().filter_map(Value::as_str).collect())
3333                    .unwrap_or_default();
3334                reactions.push(json!({
3335                    "reaction": {
3336                        "emoji": emoji,
3337                        "shortCode": emoji,
3338                        "unicode": "",
3339                    },
3340                    "reactionUsers": user_list,
3341                    "reactionsFromDeletedUsersCount": 0,
3342                }));
3343            }
3344        }
3345        ok(json!({ "reactionsForComment": reactions }))
3346    }
3347
3348    // ----- Triggers -----
3349
3350    fn put_repository_triggers(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3351        let b = body(req);
3352        let name = require_repository_name(&b)?;
3353        let triggers = b
3354            .get("triggers")
3355            .and_then(Value::as_array)
3356            .ok_or_else(|| {
3357                err(
3358                    "RepositoryTriggersListRequiredException",
3359                    "The list of triggers for the repository is required.",
3360                )
3361            })?
3362            .clone();
3363        for t in &triggers {
3364            let tname = t.get("name").and_then(Value::as_str);
3365            if tname.map(str::is_empty).unwrap_or(true) {
3366                return Err(err(
3367                    "RepositoryTriggerNameRequiredException",
3368                    "A name for the trigger is required.",
3369                ));
3370            }
3371            let dest = t.get("destinationArn").and_then(Value::as_str);
3372            if dest.map(str::is_empty).unwrap_or(true) {
3373                return Err(err(
3374                    "RepositoryTriggerDestinationArnRequiredException",
3375                    "A destination ARN for the target service for the trigger is required.",
3376                ));
3377            }
3378            let events = t.get("events").and_then(Value::as_array);
3379            match events {
3380                None => {
3381                    return Err(err(
3382                        "RepositoryTriggerEventsListRequiredException",
3383                        "At least one event for the trigger is required.",
3384                    ))
3385                }
3386                Some(evs) => {
3387                    for e in evs {
3388                        if let Some(ev) = e.as_str() {
3389                            if !validate::is_enum(validate::REPOSITORY_TRIGGER_EVENT, ev) {
3390                                return Err(err(
3391                                    "InvalidRepositoryTriggerEventsException",
3392                                    "One or more of the trigger events is not valid.",
3393                                ));
3394                            }
3395                        }
3396                    }
3397                }
3398            }
3399        }
3400        let account = self.account(req);
3401        let mut guard = self.state.write();
3402        let st = guard.get_or_create(&account);
3403        let repo = st
3404            .repositories
3405            .get_mut(&name)
3406            .ok_or_else(|| repo_not_found(&name))?;
3407        let config_id = new_uuid();
3408        repo.triggers = triggers;
3409        repo.triggers_config_id = config_id.clone();
3410        ok(json!({ "configurationId": config_id }))
3411    }
3412
3413    fn get_repository_triggers(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3414        let b = body(req);
3415        let name = require_repository_name(&b)?;
3416        let account = self.account(req);
3417        let guard = self.state.read();
3418        let st = guard.get(&account);
3419        let repo = st
3420            .and_then(|s| s.repositories.get(&name))
3421            .ok_or_else(|| repo_not_found(&name))?;
3422        let config_id = if repo.triggers_config_id.is_empty() {
3423            new_uuid()
3424        } else {
3425            repo.triggers_config_id.clone()
3426        };
3427        ok(json!({
3428            "configurationId": config_id,
3429            "triggers": repo.triggers,
3430        }))
3431    }
3432
3433    fn test_repository_triggers(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3434        let b = body(req);
3435        let name = require_repository_name(&b)?;
3436        let triggers = b
3437            .get("triggers")
3438            .and_then(Value::as_array)
3439            .ok_or_else(|| {
3440                err(
3441                    "RepositoryTriggersListRequiredException",
3442                    "The list of triggers is required.",
3443                )
3444            })?
3445            .clone();
3446        let account = self.account(req);
3447        let guard = self.state.read();
3448        let st = guard.get(&account);
3449        let repo = st
3450            .and_then(|s| s.repositories.get(&name))
3451            .ok_or_else(|| repo_not_found(&name))?;
3452        // Enforce the same structural requirements as PutRepositoryTriggers.
3453        for t in &triggers {
3454            if t.get("name")
3455                .and_then(Value::as_str)
3456                .map(str::is_empty)
3457                .unwrap_or(true)
3458            {
3459                return Err(err(
3460                    "RepositoryTriggerNameRequiredException",
3461                    "A name for the trigger is required.",
3462                ));
3463            }
3464            if t.get("destinationArn")
3465                .and_then(Value::as_str)
3466                .map(str::is_empty)
3467                .unwrap_or(true)
3468            {
3469                return Err(err(
3470                    "RepositoryTriggerDestinationArnRequiredException",
3471                    "A destination ARN for the target service for the trigger is required.",
3472                ));
3473            }
3474            if t.get("events")
3475                .and_then(Value::as_array)
3476                .map(|e| e.is_empty())
3477                .unwrap_or(true)
3478            {
3479                return Err(err(
3480                    "RepositoryTriggerEventsListRequiredException",
3481                    "At least one event for the trigger is required.",
3482                ));
3483            }
3484        }
3485        // A test "executes" each trigger: its destination must resolve to a
3486        // supported delivery target (SNS topic / Lambda function in this account
3487        // and region), its events must be valid, and any branch it is scoped to
3488        // must exist. Triggers that cannot be delivered are reported as failures
3489        // with a real message rather than a blanket success.
3490        let mut successful = Vec::new();
3491        let mut failed = Vec::new();
3492        for t in &triggers {
3493            let tname = t.get("name").and_then(Value::as_str).unwrap_or_default();
3494            let dest = t
3495                .get("destinationArn")
3496                .and_then(Value::as_str)
3497                .unwrap_or_default();
3498            let mut failure: Option<String> = None;
3499            if !trigger_destination_ok(dest, &account, &req.region) {
3500                failure = Some(format!(
3501                    "Unable to deliver to destination {dest}. A repository trigger destination must be an Amazon SNS topic or an AWS Lambda function in the same account ({account}) and region ({}).",
3502                    req.region
3503                ));
3504            }
3505            if failure.is_none() {
3506                if let Some(evs) = t.get("events").and_then(Value::as_array) {
3507                    for e in evs {
3508                        if let Some(ev) = e.as_str() {
3509                            if !validate::is_enum(validate::REPOSITORY_TRIGGER_EVENT, ev) {
3510                                failure = Some(format!("The trigger event {ev} is not valid."));
3511                                break;
3512                            }
3513                        }
3514                    }
3515                }
3516            }
3517            if failure.is_none() {
3518                if let Some(branches) = t.get("branches").and_then(Value::as_array) {
3519                    for br in branches {
3520                        if let Some(bn) = br.as_str() {
3521                            if !repo.branches.contains_key(bn) {
3522                                failure = Some(format!(
3523                                    "The branch {bn} specified for the trigger does not exist in the repository."
3524                                ));
3525                                break;
3526                            }
3527                        }
3528                    }
3529                }
3530            }
3531            match failure {
3532                Some(msg) => {
3533                    failed.push(json!({ "trigger": tname, "failureMessage": msg }));
3534                }
3535                None => successful.push(json!(tname)),
3536            }
3537        }
3538        ok(json!({
3539            "successfulExecutions": successful,
3540            "failedExecutions": failed,
3541        }))
3542    }
3543
3544    // ----- Tagging -----
3545
3546    fn tag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3547        let b = body(req);
3548        let arn = require(&b, "resourceArn", "ResourceArnRequiredException")?;
3549        if !is_codecommit_arn(&arn) {
3550            return Err(err(
3551                "InvalidResourceArnException",
3552                "The resource ARN is not valid.",
3553            ));
3554        }
3555        let tags = b
3556            .get("tags")
3557            .and_then(Value::as_object)
3558            .ok_or_else(|| err("TagsMapRequiredException", "A map of tags is required."))?;
3559        let account = self.account(req);
3560        let mut guard = self.state.write();
3561        let st = guard.get_or_create(&account);
3562        if let Some(rn) = repo_name_from_arn(&arn) {
3563            if !st.repositories.contains_key(&rn) {
3564                return Err(repo_not_found(&rn));
3565            }
3566        }
3567        let entry = st.tags.entry(arn).or_default();
3568        for (k, v) in tags {
3569            if let Some(v) = v.as_str() {
3570                entry.insert(k.clone(), v.to_string());
3571            }
3572        }
3573        ok(json!({}))
3574    }
3575
3576    fn untag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3577        let b = body(req);
3578        let arn = require(&b, "resourceArn", "ResourceArnRequiredException")?;
3579        if !is_codecommit_arn(&arn) {
3580            return Err(err(
3581                "InvalidResourceArnException",
3582                "The resource ARN is not valid.",
3583            ));
3584        }
3585        let keys = b
3586            .get("tagKeys")
3587            .and_then(Value::as_array)
3588            .ok_or_else(|| {
3589                err(
3590                    "TagKeysListRequiredException",
3591                    "A list of tag keys is required.",
3592                )
3593            })?
3594            .clone();
3595        let account = self.account(req);
3596        let mut guard = self.state.write();
3597        let st = guard.get_or_create(&account);
3598        if let Some(rn) = repo_name_from_arn(&arn) {
3599            if !st.repositories.contains_key(&rn) {
3600                return Err(repo_not_found(&rn));
3601            }
3602        }
3603        if let Some(entry) = st.tags.get_mut(&arn) {
3604            for k in &keys {
3605                if let Some(k) = k.as_str() {
3606                    entry.remove(k);
3607                }
3608            }
3609        }
3610        ok(json!({}))
3611    }
3612
3613    fn list_tags_for_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3614        let b = body(req);
3615        let arn = require(&b, "resourceArn", "ResourceArnRequiredException")?;
3616        if !is_codecommit_arn(&arn) {
3617            return Err(err(
3618                "InvalidResourceArnException",
3619                "The resource ARN is not valid.",
3620            ));
3621        }
3622        let account = self.account(req);
3623        let guard = self.state.read();
3624        let st = guard.get(&account);
3625        if let Some(rn) = repo_name_from_arn(&arn) {
3626            if !st
3627                .map(|s| s.repositories.contains_key(&rn))
3628                .unwrap_or(false)
3629            {
3630                return Err(repo_not_found(&rn));
3631            }
3632        }
3633        let tags = st
3634            .and_then(|s| s.tags.get(&arn))
3635            .map(|m| {
3636                let mut obj = Map::new();
3637                for (k, v) in m {
3638                    obj.insert(k.clone(), json!(v));
3639                }
3640                Value::Object(obj)
3641            })
3642            .unwrap_or_else(|| json!({}));
3643        ok(json!({ "tags": tags }))
3644    }
3645
3646    // ----- Shared mutation helpers -----
3647
3648    fn mutate_pr(
3649        &self,
3650        req: &AwsRequest,
3651        id: &str,
3652        f: impl FnOnce(&mut Value) -> Result<(), AwsServiceError>,
3653    ) -> Result<AwsResponse, AwsServiceError> {
3654        let account = self.account(req);
3655        let mut guard = self.state.write();
3656        let st = guard.get_or_create(&account);
3657        let pr = st.pull_requests.get_mut(id).ok_or_else(pr_not_found)?;
3658        f(pr)?;
3659        let pr = pr.clone();
3660        ok(json!({ "pullRequest": pr }))
3661    }
3662
3663    fn mutate_template(
3664        &self,
3665        req: &AwsRequest,
3666        name: &str,
3667        f: impl FnOnce(&mut Value) -> Result<(), AwsServiceError>,
3668    ) -> Result<AwsResponse, AwsServiceError> {
3669        let account = self.account(req);
3670        let mut guard = self.state.write();
3671        let st = guard.get_or_create(&account);
3672        let t = st.templates.get_mut(name).ok_or_else(template_not_found)?;
3673        f(t)?;
3674        let t = t.clone();
3675        ok(json!({ "approvalRuleTemplate": t }))
3676    }
3677}
3678
3679// ---------------------------------------------------------------------------
3680// Error constructors for the service-wide "does not exist" responses.
3681// ---------------------------------------------------------------------------
3682
3683#[cfg(test)]
3684mod normalize_tests {
3685    use crate::codecommit_helpers::normalize_rule_content;
3686
3687    #[test]
3688    fn compacts_and_preserves_member_order() {
3689        let input = "  {\n\t  \"Version\": \"2018-11-08\",\n\t  \"DestinationReferences\": [\"refs/heads/master\"],\n\t  \"Statements\": []\n  }\n";
3690        let out = normalize_rule_content(input);
3691        assert_eq!(
3692            out,
3693            "{\"Version\":\"2018-11-08\",\"DestinationReferences\":[\"refs/heads/master\"],\"Statements\":[]}"
3694        );
3695    }
3696}
3697
3698#[cfg(test)]
3699mod engine_tests {
3700    use super::*;
3701
3702    fn entry(id: &str) -> FileEntry {
3703        FileEntry {
3704            blob_id: id.to_string(),
3705            mode: "100644".to_string(),
3706        }
3707    }
3708
3709    fn commit(parents: &[&str]) -> Value {
3710        json!({ "parents": parents, "treeId": "t" })
3711    }
3712
3713    // Defect 3: merge_base is the lowest common ancestor, not any ancestor.
3714    #[test]
3715    fn merge_base_is_lowest_common_ancestor() {
3716        // root <- X <- A ; and B is a merge commit whose parents are [X, root].
3717        // The common ancestors of A and B are {X, root}; the LCA is X. A
3718        // depth-first walk that returns the first common ancestor would wrongly
3719        // report root.
3720        let mut repo = Repo::default();
3721        repo.commits.insert("root".into(), commit(&[]));
3722        repo.commits.insert("X".into(), commit(&["root"]));
3723        repo.commits.insert("A".into(), commit(&["X"]));
3724        repo.commits.insert("B".into(), commit(&["X", "root"]));
3725        assert_eq!(merge_base(&repo, "A", "B"), Some("X".to_string()));
3726        assert_eq!(merge_base(&repo, "B", "A"), Some("X".to_string()));
3727    }
3728
3729    #[test]
3730    fn merge_base_of_a_diamond_is_the_fork_point() {
3731        // root <- L <- A ; root <- R <- B. LCA(A, B) == root.
3732        let mut repo = Repo::default();
3733        repo.commits.insert("root".into(), commit(&[]));
3734        repo.commits.insert("L".into(), commit(&["root"]));
3735        repo.commits.insert("R".into(), commit(&["root"]));
3736        repo.commits.insert("A".into(), commit(&["L"]));
3737        repo.commits.insert("B".into(), commit(&["R"]));
3738        assert_eq!(merge_base(&repo, "A", "B"), Some("root".to_string()));
3739    }
3740
3741    // Defect 2: a file deleted on the source branch is removed by the merge.
3742    #[test]
3743    fn three_way_merge_propagates_source_deletion() {
3744        let mut repo = Repo::default();
3745        repo.commits.insert("base".into(), commit(&[]));
3746        repo.commits.insert("src".into(), commit(&["base"]));
3747        repo.commits.insert("dst".into(), commit(&["base"]));
3748        // base has a.txt + b.txt.
3749        let mut base = Tree::new();
3750        base.insert("a.txt".into(), entry("a1"));
3751        base.insert("b.txt".into(), entry("b1"));
3752        repo.trees.insert("base".into(), base);
3753        // source deletes b.txt.
3754        let mut src = Tree::new();
3755        src.insert("a.txt".into(), entry("a1"));
3756        repo.trees.insert("src".into(), src);
3757        // destination is unchanged w.r.t. b.txt but adds c.txt.
3758        let mut dst = Tree::new();
3759        dst.insert("a.txt".into(), entry("a1"));
3760        dst.insert("b.txt".into(), entry("b1"));
3761        dst.insert("c.txt".into(), entry("c1"));
3762        repo.trees.insert("dst".into(), dst);
3763
3764        let merged = merge_trees(&repo, "src", "dst", "").expect("mergeable");
3765        assert!(!merged.contains_key("b.txt"), "source deletion must win");
3766        assert!(merged.contains_key("a.txt"));
3767        assert!(merged.contains_key("c.txt"));
3768    }
3769
3770    #[test]
3771    fn three_way_merge_flags_true_conflict() {
3772        let mut repo = Repo::default();
3773        repo.commits.insert("base".into(), commit(&[]));
3774        repo.commits.insert("src".into(), commit(&["base"]));
3775        repo.commits.insert("dst".into(), commit(&["base"]));
3776        let mut base = Tree::new();
3777        base.insert("a.txt".into(), entry("a1"));
3778        repo.trees.insert("base".into(), base);
3779        let mut src = Tree::new();
3780        src.insert("a.txt".into(), entry("a-src"));
3781        repo.trees.insert("src".into(), src);
3782        let mut dst = Tree::new();
3783        dst.insert("a.txt".into(), entry("a-dst"));
3784        repo.trees.insert("dst".into(), dst);
3785        // Both sides changed a.txt differently -> unresolved conflict.
3786        assert!(merge_trees(&repo, "src", "dst", "").is_none());
3787        let empty = Tree::new();
3788        let (_, bt, st, dt) = merge_inputs(&repo, "src", "dst", &empty);
3789        assert_eq!(conflicting_paths_in(bt, st, dt), vec!["a.txt"]);
3790        // A strategy resolves it deterministically.
3791        let merged = merge_trees(&repo, "src", "dst", "ACCEPT_SOURCE").expect("resolved");
3792        assert_eq!(merged.get("a.txt").unwrap().blob_id, "a-src");
3793    }
3794}
3795
3796#[cfg(test)]
3797mod handler_tests {
3798    use super::*;
3799    use bytes::Bytes;
3800    use fakecloud_core::auth::{Principal, PrincipalType};
3801    use fakecloud_core::multi_account::MultiAccountState;
3802    use http::{HeaderMap, Method};
3803    use parking_lot::RwLock;
3804    use std::collections::HashMap;
3805
3806    fn svc() -> CodeCommitService {
3807        CodeCommitService::new(Arc::new(RwLock::new(MultiAccountState::new(
3808            "000000000000",
3809            "us-east-1",
3810            "",
3811        ))))
3812    }
3813
3814    fn req_as(action: &str, body: Value, arn: Option<&str>) -> AwsRequest {
3815        let principal = arn.map(|a| Principal {
3816            arn: a.to_string(),
3817            user_id: "AIDATEST".to_string(),
3818            account_id: "000000000000".to_string(),
3819            principal_type: PrincipalType::User,
3820            source_identity: None,
3821            tags: None,
3822        });
3823        AwsRequest {
3824            service: "codecommit".into(),
3825            action: action.into(),
3826            region: "us-east-1".into(),
3827            account_id: "000000000000".into(),
3828            request_id: "req".into(),
3829            headers: HeaderMap::new(),
3830            query_params: HashMap::new(),
3831            body: Bytes::from(serde_json::to_vec(&body).unwrap()),
3832            body_stream: parking_lot::Mutex::new(None),
3833            path_segments: vec![],
3834            raw_path: String::new(),
3835            raw_query: String::new(),
3836            method: Method::POST,
3837            is_query_protocol: false,
3838            access_key_id: None,
3839            principal,
3840        }
3841    }
3842
3843    fn call(s: &CodeCommitService, action: &str, body: Value) -> Value {
3844        let resp = s
3845            .dispatch(action, &req_as(action, body, None))
3846            .expect("op ok");
3847        serde_json::from_slice(resp.body.expect_bytes()).unwrap()
3848    }
3849
3850    fn call_as(s: &CodeCommitService, action: &str, body: Value, arn: &str) -> Value {
3851        let resp = s
3852            .dispatch(action, &req_as(action, body, Some(arn)))
3853            .expect("op ok");
3854        serde_json::from_slice(resp.body.expect_bytes()).unwrap()
3855    }
3856
3857    fn call_err(s: &CodeCommitService, action: &str, body: Value) -> AwsServiceError {
3858        s.dispatch(action, &req_as(action, body, None))
3859            .err()
3860            .expect("op err")
3861    }
3862
3863    fn b64(s: &str) -> String {
3864        base64::engine::general_purpose::STANDARD.encode(s.as_bytes())
3865    }
3866
3867    /// Create a repository and return nothing; the caller uses "repo".
3868    fn make_repo(s: &CodeCommitService) {
3869        call(s, "CreateRepository", json!({ "repositoryName": "repo" }));
3870    }
3871
3872    /// Put a file on a branch, returning the new commit id.
3873    fn put(
3874        s: &CodeCommitService,
3875        branch: &str,
3876        path: &str,
3877        content: &str,
3878        parent: Option<&str>,
3879    ) -> String {
3880        let mut body = json!({
3881            "repositoryName": "repo",
3882            "branchName": branch,
3883            "filePath": path,
3884            "fileContent": b64(content),
3885        });
3886        if let Some(p) = parent {
3887            body["parentCommitId"] = json!(p);
3888        }
3889        call(s, "PutFile", body)["commitId"]
3890            .as_str()
3891            .unwrap()
3892            .to_string()
3893    }
3894
3895    /// A repo with `main` and `feature` diverging non-conflictingly from a
3896    /// shared base commit. Returns (source_tip, dest_tip).
3897    fn diverged_repo(s: &CodeCommitService) -> (String, String) {
3898        make_repo(s);
3899        let c1 = put(s, "main", "a.txt", "base", None);
3900        call(
3901            s,
3902            "CreateBranch",
3903            json!({ "repositoryName": "repo", "branchName": "feature", "commitId": c1 }),
3904        );
3905        let feature_tip = put(s, "feature", "b.txt", "from-feature", Some(&c1));
3906        let main_tip = put(s, "main", "c.txt", "from-main", Some(&c1));
3907        (feature_tip, main_tip)
3908    }
3909
3910    fn create_pr(s: &CodeCommitService, dest: &str) -> Value {
3911        call(
3912            s,
3913            "CreatePullRequest",
3914            json!({
3915                "title": "PR",
3916                "targets": [{
3917                    "repositoryName": "repo",
3918                    "sourceReference": "feature",
3919                    "destinationReference": dest,
3920                }],
3921            }),
3922        )["pullRequest"]
3923            .clone()
3924    }
3925
3926    // Defect 1: MergePullRequestByThreeWay advances the destination branch and
3927    // the returned mergeCommitId is a real, retrievable commit.
3928    #[test]
3929    fn merge_pr_three_way_advances_branch_and_writes_commit() {
3930        let s = svc();
3931        diverged_repo(&s);
3932        let pr = create_pr(&s, "main");
3933        let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
3934
3935        let out = call(
3936            &s,
3937            "MergePullRequestByThreeWay",
3938            json!({ "pullRequestId": pr_id, "repositoryName": "repo" }),
3939        );
3940        let target = &out["pullRequest"]["pullRequestTargets"][0];
3941        assert_eq!(target["mergeMetadata"]["isMerged"], json!(true));
3942        let merge_commit = target["mergeMetadata"]["mergeCommitId"]
3943            .as_str()
3944            .unwrap()
3945            .to_string();
3946        assert!(is_object_id(&merge_commit));
3947        assert_eq!(out["pullRequest"]["pullRequestStatus"], json!("CLOSED"));
3948
3949        // Destination branch now points at the merge commit.
3950        let branch = call(
3951            &s,
3952            "GetBranch",
3953            json!({ "repositoryName": "repo", "branchName": "main" }),
3954        );
3955        assert_eq!(branch["branch"]["commitId"], json!(merge_commit));
3956
3957        // GetCommit on the merge commit succeeds and it has two parents.
3958        let got = call(
3959            &s,
3960            "GetCommit",
3961            json!({ "repositoryName": "repo", "commitId": merge_commit }),
3962        );
3963        assert_eq!(got["commit"]["parents"].as_array().unwrap().len(), 2);
3964    }
3965
3966    // Defect 1/2: a squash merge that deletes a file on source removes it from
3967    // the merged destination tree, and the squash commit has a single parent.
3968    #[test]
3969    fn merge_pr_squash_deletes_file_and_has_one_parent() {
3970        let s = svc();
3971        make_repo(&s);
3972        let c1 = put(&s, "main", "a.txt", "a", None);
3973        let c2 = put(&s, "main", "b.txt", "b", Some(&c1));
3974        call(
3975            &s,
3976            "CreateBranch",
3977            json!({ "repositoryName": "repo", "branchName": "feature", "commitId": c2 }),
3978        );
3979        // feature deletes b.txt; main advances independently.
3980        call(
3981            &s,
3982            "DeleteFile",
3983            json!({ "repositoryName": "repo", "branchName": "feature", "filePath": "b.txt", "parentCommitId": c2 }),
3984        );
3985        put(&s, "main", "c.txt", "c", Some(&c2));
3986
3987        let pr = create_pr(&s, "main");
3988        let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
3989        let out = call(
3990            &s,
3991            "MergePullRequestBySquash",
3992            json!({ "pullRequestId": pr_id, "repositoryName": "repo" }),
3993        );
3994        let merge_commit = out["pullRequest"]["pullRequestTargets"][0]["mergeMetadata"]
3995            ["mergeCommitId"]
3996            .as_str()
3997            .unwrap()
3998            .to_string();
3999        // Squash => single parent.
4000        let got = call(
4001            &s,
4002            "GetCommit",
4003            json!({ "repositoryName": "repo", "commitId": merge_commit }),
4004        );
4005        assert_eq!(got["commit"]["parents"].as_array().unwrap().len(), 1);
4006        // b.txt is gone on the destination branch after merge.
4007        let err = call_err(
4008            &s,
4009            "GetFile",
4010            json!({ "repositoryName": "repo", "commitSpecifier": "main", "filePath": "b.txt" }),
4011        );
4012        assert_eq!(err.code(), "FileDoesNotExistException");
4013    }
4014
4015    // Defect 4: MergeBranchesBySquash yields a single-parent commit.
4016    #[test]
4017    fn merge_branches_squash_single_parent() {
4018        let s = svc();
4019        let (feature_tip, main_tip) = diverged_repo(&s);
4020        let out = call(
4021            &s,
4022            "MergeBranchesBySquash",
4023            json!({
4024                "repositoryName": "repo",
4025                "sourceCommitSpecifier": feature_tip,
4026                "destinationCommitSpecifier": main_tip,
4027                "targetBranch": "main",
4028            }),
4029        );
4030        let cid = out["commitId"].as_str().unwrap().to_string();
4031        let got = call(
4032            &s,
4033            "GetCommit",
4034            json!({ "repositoryName": "repo", "commitId": cid }),
4035        );
4036        assert_eq!(got["commit"]["parents"].as_array().unwrap().len(), 1);
4037    }
4038
4039    // Defect 5 + 6: an associated 2-approval template is auto-applied on PR
4040    // create and is NOT satisfied by a single approval.
4041    #[test]
4042    fn template_applied_and_two_approvals_needed() {
4043        let s = svc();
4044        let content = "{\"Version\":\"2018-11-08\",\"Statements\":[{\"Type\":\"Approvers\",\"NumberOfApprovalsNeeded\":2,\"ApprovalPoolMembers\":[]}]}";
4045        call(
4046            &s,
4047            "CreateApprovalRuleTemplate",
4048            json!({ "approvalRuleTemplateName": "two", "approvalRuleTemplateContent": content }),
4049        );
4050        diverged_repo(&s);
4051        call(
4052            &s,
4053            "AssociateApprovalRuleTemplateWithRepository",
4054            json!({ "approvalRuleTemplateName": "two", "repositoryName": "repo" }),
4055        );
4056        let pr = create_pr(&s, "main");
4057        let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4058        let revision = pr["revisionId"].as_str().unwrap().to_string();
4059
4060        // The template's rule is materialized on the PR with template origin.
4061        let rules = pr["approvalRules"].as_array().unwrap();
4062        assert_eq!(rules.len(), 1);
4063        assert_eq!(rules[0]["approvalRuleName"], json!("two"));
4064        assert!(rules[0]["originApprovalRuleTemplate"].is_object());
4065
4066        // One approval is not enough for a rule needing two.
4067        call_as(
4068            &s,
4069            "UpdatePullRequestApprovalState",
4070            json!({ "pullRequestId": pr_id, "revisionId": revision, "approvalState": "APPROVE" }),
4071            "arn:aws:iam::000000000000:user/reviewer",
4072        );
4073        let eval = call(
4074            &s,
4075            "EvaluatePullRequestApprovalRules",
4076            json!({ "pullRequestId": pr_id, "revisionId": revision }),
4077        );
4078        assert_eq!(eval["evaluation"]["approved"], json!(false));
4079        assert_eq!(
4080            eval["evaluation"]["approvalRulesNotSatisfied"],
4081            json!(["two"])
4082        );
4083    }
4084
4085    // Defect 7: a closed pull request cannot be reopened.
4086    #[test]
4087    fn closed_pr_cannot_reopen() {
4088        let s = svc();
4089        diverged_repo(&s);
4090        let pr = create_pr(&s, "main");
4091        let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4092        call(
4093            &s,
4094            "UpdatePullRequestStatus",
4095            json!({ "pullRequestId": pr_id, "pullRequestStatus": "CLOSED" }),
4096        );
4097        let err = call_err(
4098            &s,
4099            "UpdatePullRequestStatus",
4100            json!({ "pullRequestId": pr_id, "pullRequestStatus": "OPEN" }),
4101        );
4102        assert_eq!(err.code(), "InvalidPullRequestStatusUpdateException");
4103    }
4104
4105    // Defect 8: a nonexistent destinationReference is rejected before create.
4106    #[test]
4107    fn create_pr_rejects_missing_destination() {
4108        let s = svc();
4109        diverged_repo(&s);
4110        let err = call_err(
4111            &s,
4112            "CreatePullRequest",
4113            json!({
4114                "title": "PR",
4115                "targets": [{
4116                    "repositoryName": "repo",
4117                    "sourceReference": "feature",
4118                    "destinationReference": "does-not-exist",
4119                }],
4120            }),
4121        );
4122        assert_eq!(err.code(), "ReferenceDoesNotExistException");
4123    }
4124
4125    // Defect 9 + 10: Describe / BatchDescribe report a real content conflict.
4126    #[test]
4127    fn describe_and_batch_report_conflict() {
4128        let s = svc();
4129        make_repo(&s);
4130        let c1 = put(&s, "main", "a.txt", "base-content", None);
4131        call(
4132            &s,
4133            "CreateBranch",
4134            json!({ "repositoryName": "repo", "branchName": "feature", "commitId": c1 }),
4135        );
4136        put(&s, "feature", "a.txt", "feature-side", Some(&c1));
4137        put(&s, "main", "a.txt", "main-side", Some(&c1));
4138
4139        let base_body = json!({
4140            "repositoryName": "repo",
4141            "sourceCommitSpecifier": "feature",
4142            "destinationCommitSpecifier": "main",
4143            "mergeOption": "THREE_WAY_MERGE",
4144        });
4145
4146        let mut d = base_body.clone();
4147        d["filePath"] = json!("a.txt");
4148        let desc = call(&s, "DescribeMergeConflicts", d);
4149        assert_eq!(desc["conflictMetadata"]["numberOfConflicts"], json!(1));
4150        assert_eq!(desc["conflictMetadata"]["contentConflict"], json!(true));
4151        assert_eq!(desc["mergeHunks"][0]["isConflict"], json!(true));
4152
4153        let batch = call(&s, "BatchDescribeMergeConflicts", base_body.clone());
4154        let conflicts = batch["conflicts"].as_array().unwrap();
4155        assert_eq!(conflicts.len(), 1);
4156        assert_eq!(conflicts[0]["conflictMetadata"]["filePath"], json!("a.txt"));
4157
4158        // GetMergeConflicts agrees: not mergeable, a.txt listed.
4159        let gmc = call(&s, "GetMergeConflicts", base_body);
4160        assert_eq!(gmc["mergeable"], json!(false));
4161        assert_eq!(gmc["conflictMetadataList"][0]["filePath"], json!("a.txt"));
4162    }
4163
4164    // Defect 1: a fast-forward PR merge that is not fast-forwardable is rejected
4165    // and leaves the PR open.
4166    #[test]
4167    fn merge_pr_fast_forward_non_ff_rejected() {
4168        let s = svc();
4169        diverged_repo(&s);
4170        let pr = create_pr(&s, "main");
4171        let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4172        let err = call_err(
4173            &s,
4174            "MergePullRequestByFastForward",
4175            json!({ "pullRequestId": pr_id, "repositoryName": "repo" }),
4176        );
4177        assert_eq!(err.code(), "ManualMergeRequiredException");
4178        // PR remains open.
4179        let got = call(&s, "GetPullRequest", json!({ "pullRequestId": pr_id }));
4180        assert_eq!(got["pullRequest"]["pullRequestStatus"], json!("OPEN"));
4181    }
4182
4183    fn branch_tip(s: &CodeCommitService, branch: &str) -> String {
4184        call(
4185            s,
4186            "GetBranch",
4187            json!({ "repositoryName": "repo", "branchName": branch }),
4188        )["branch"]["commitId"]
4189            .as_str()
4190            .unwrap()
4191            .to_string()
4192    }
4193
4194    fn one_approval_rule() -> &'static str {
4195        "{\"Version\":\"2018-11-08\",\"Statements\":[{\"Type\":\"Approvers\",\"NumberOfApprovalsNeeded\":1,\"ApprovalPoolMembers\":[]}]}"
4196    }
4197
4198    // Round-2 defect 1: a new commit on the PR's source branch mints a new
4199    // revisionId and invalidates approvals cast against the prior revision.
4200    #[test]
4201    fn new_source_commit_bumps_revision_and_invalidates_approvals() {
4202        let s = svc();
4203        let (feature_tip, _) = diverged_repo(&s);
4204        let pr = create_pr(&s, "main");
4205        let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4206        let rev1 = pr["revisionId"].as_str().unwrap().to_string();
4207        call(
4208            &s,
4209            "CreatePullRequestApprovalRule",
4210            json!({
4211                "pullRequestId": pr_id,
4212                "approvalRuleName": "r",
4213                "approvalRuleContent": one_approval_rule(),
4214            }),
4215        );
4216        // One approval at rev1 satisfies the 1-approval rule.
4217        call_as(
4218            &s,
4219            "UpdatePullRequestApprovalState",
4220            json!({ "pullRequestId": pr_id, "revisionId": rev1, "approvalState": "APPROVE" }),
4221            "arn:aws:iam::000000000000:user/reviewer",
4222        );
4223        let eval1 = call(
4224            &s,
4225            "EvaluatePullRequestApprovalRules",
4226            json!({ "pullRequestId": pr_id, "revisionId": rev1 }),
4227        );
4228        assert_eq!(eval1["evaluation"]["approved"], json!(true));
4229
4230        // A new commit on the source branch bumps the revisionId...
4231        put(&s, "feature", "d.txt", "more", Some(&feature_tip));
4232        let got = call(&s, "GetPullRequest", json!({ "pullRequestId": pr_id }));
4233        let rev2 = got["pullRequest"]["revisionId"]
4234            .as_str()
4235            .unwrap()
4236            .to_string();
4237        assert_ne!(rev1, rev2, "source advance must re-mint the revisionId");
4238
4239        // ...and the old approval no longer counts against the new revision.
4240        let eval2 = call(
4241            &s,
4242            "EvaluatePullRequestApprovalRules",
4243            json!({ "pullRequestId": pr_id, "revisionId": rev2 }),
4244        );
4245        assert_eq!(eval2["evaluation"]["approved"], json!(false));
4246    }
4247
4248    // Round-2 defect 4: GetPullRequest reflects the advanced source tip.
4249    #[test]
4250    fn get_pull_request_reflects_advanced_source_tip() {
4251        let s = svc();
4252        let (feature_tip, _) = diverged_repo(&s);
4253        let pr = create_pr(&s, "main");
4254        let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4255        assert_eq!(
4256            pr["pullRequestTargets"][0]["sourceCommit"],
4257            json!(feature_tip)
4258        );
4259        put(&s, "feature", "e.txt", "x", Some(&feature_tip));
4260        let new_tip = branch_tip(&s, "feature");
4261        let got = call(&s, "GetPullRequest", json!({ "pullRequestId": pr_id }));
4262        assert_eq!(
4263            got["pullRequest"]["pullRequestTargets"][0]["sourceCommit"],
4264            json!(new_tip)
4265        );
4266    }
4267
4268    // Round-2 defect 2: CreateUnreferencedMergeCommit SQUASH -> single parent.
4269    #[test]
4270    fn create_unreferenced_squash_single_parent() {
4271        let s = svc();
4272        diverged_repo(&s);
4273        let out = call(
4274            &s,
4275            "CreateUnreferencedMergeCommit",
4276            json!({
4277                "repositoryName": "repo",
4278                "sourceCommitSpecifier": "feature",
4279                "destinationCommitSpecifier": "main",
4280                "mergeOption": "SQUASH_MERGE",
4281            }),
4282        );
4283        let cid = out["commitId"].as_str().unwrap().to_string();
4284        let got = call(
4285            &s,
4286            "GetCommit",
4287            json!({ "repositoryName": "repo", "commitId": cid }),
4288        );
4289        assert_eq!(got["commit"]["parents"].as_array().unwrap().len(), 1);
4290    }
4291
4292    // Round-2 defect 3: an invalid conflictResolutionStrategy is rejected on both
4293    // MergePullRequestBy* and CreateUnreferencedMergeCommit.
4294    #[test]
4295    fn invalid_conflict_strategy_rejected() {
4296        let s = svc();
4297        diverged_repo(&s);
4298        let pr = create_pr(&s, "main");
4299        let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4300        let e1 = call_err(
4301            &s,
4302            "MergePullRequestByThreeWay",
4303            json!({
4304                "pullRequestId": pr_id,
4305                "repositoryName": "repo",
4306                "conflictResolutionStrategy": "BOGUS",
4307            }),
4308        );
4309        assert_eq!(e1.code(), "InvalidConflictResolutionStrategyException");
4310        let e2 = call_err(
4311            &s,
4312            "CreateUnreferencedMergeCommit",
4313            json!({
4314                "repositoryName": "repo",
4315                "sourceCommitSpecifier": "feature",
4316                "destinationCommitSpecifier": "main",
4317                "mergeOption": "THREE_WAY_MERGE",
4318                "conflictResolutionStrategy": "BOGUS",
4319            }),
4320        );
4321        assert_eq!(e2.code(), "InvalidConflictResolutionStrategyException");
4322    }
4323
4324    // Round-2 defect 5: OPEN -> OPEN status update is rejected.
4325    #[test]
4326    fn open_to_open_status_rejected() {
4327        let s = svc();
4328        diverged_repo(&s);
4329        let pr = create_pr(&s, "main");
4330        let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4331        let err = call_err(
4332            &s,
4333            "UpdatePullRequestStatus",
4334            json!({ "pullRequestId": pr_id, "pullRequestStatus": "OPEN" }),
4335        );
4336        assert_eq!(err.code(), "InvalidPullRequestStatusUpdateException");
4337    }
4338
4339    // Round-2 defect 6: an unresolvable trigger destination is reported as a
4340    // failed execution, while a valid SNS destination succeeds.
4341    #[test]
4342    fn test_triggers_reports_unresolvable_destination() {
4343        let s = svc();
4344        make_repo(&s);
4345        let out = call(
4346            &s,
4347            "TestRepositoryTriggers",
4348            json!({
4349                "repositoryName": "repo",
4350                "triggers": [
4351                    {
4352                        "name": "ok",
4353                        "destinationArn": "arn:aws:sns:us-east-1:000000000000:topic",
4354                        "events": ["all"],
4355                    },
4356                    {
4357                        "name": "bad",
4358                        "destinationArn": "arn:aws:s3:::some-bucket",
4359                        "events": ["all"],
4360                    },
4361                ],
4362            }),
4363        );
4364        assert_eq!(out["successfulExecutions"], json!(["ok"]));
4365        let failed = out["failedExecutions"].as_array().unwrap();
4366        assert_eq!(failed.len(), 1);
4367        assert_eq!(failed[0]["trigger"], json!("bad"));
4368        assert!(failed[0]["failureMessage"].as_str().unwrap().contains("s3"));
4369    }
4370
4371    // Round-2 defect 7: deleting a repository drops its template associations.
4372    #[test]
4373    fn delete_repository_clears_template_association() {
4374        let s = svc();
4375        make_repo(&s);
4376        call(
4377            &s,
4378            "CreateApprovalRuleTemplate",
4379            json!({
4380                "approvalRuleTemplateName": "t",
4381                "approvalRuleTemplateContent": one_approval_rule(),
4382            }),
4383        );
4384        call(
4385            &s,
4386            "AssociateApprovalRuleTemplateWithRepository",
4387            json!({ "approvalRuleTemplateName": "t", "repositoryName": "repo" }),
4388        );
4389        let before = call(
4390            &s,
4391            "ListRepositoriesForApprovalRuleTemplate",
4392            json!({ "approvalRuleTemplateName": "t" }),
4393        );
4394        assert_eq!(before["repositoryNames"], json!(["repo"]));
4395        call(&s, "DeleteRepository", json!({ "repositoryName": "repo" }));
4396        let after = call(
4397            &s,
4398            "ListRepositoriesForApprovalRuleTemplate",
4399            json!({ "approvalRuleTemplateName": "t" }),
4400        );
4401        assert_eq!(after["repositoryNames"], json!([]));
4402    }
4403}