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")?.to_string();
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        // The blob id of `path` in a given commit's materialized tree, if present.
1310        let blob_at = |cid: &str| -> Option<String> {
1311            repo.trees
1312                .get(cid)
1313                .and_then(|t| t.get(&path))
1314                .map(|e| e.blob_id.clone())
1315        };
1316        // Walk the first-parent chain, keeping only the commits that actually
1317        // TOUCHED `filePath` (its blob differs from the first parent's, covering
1318        // add/modify/delete). Previously every commit was returned regardless of
1319        // the path (bug-hunt).
1320        let mut dag = Vec::new();
1321        let mut cur = Some(commit_id);
1322        while let Some(cid) = cur {
1323            let Some(c) = repo.commits.get(&cid) else {
1324                break;
1325            };
1326            let parents = c
1327                .get("parents")
1328                .and_then(Value::as_array)
1329                .cloned()
1330                .unwrap_or_default();
1331            let first_parent = parents.first().and_then(Value::as_str);
1332            let cur_blob = blob_at(&cid);
1333            let parent_blob = first_parent.and_then(blob_at);
1334            let touched = match first_parent {
1335                // Root commit: touched iff the file exists in it.
1336                None => cur_blob.is_some(),
1337                // Otherwise the blob changed (add/modify/delete).
1338                Some(_) => cur_blob != parent_blob,
1339            };
1340            if touched {
1341                dag.push(json!({
1342                    "commit": cid,
1343                    "parents": parents.iter().filter_map(|p| p.as_str()).collect::<Vec<_>>(),
1344                }));
1345            }
1346            cur = first_parent.map(str::to_string);
1347        }
1348        ok(json!({ "revisionDag": dag }))
1349    }
1350
1351    // ----- Merges -----
1352
1353    fn merge_branches_fast_forward(
1354        &self,
1355        req: &AwsRequest,
1356    ) -> Result<AwsResponse, AwsServiceError> {
1357        let b = body(req);
1358        let name = require_repository_name(&b)?;
1359        require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1360        require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1361        let account = self.account(req);
1362        let mut guard = self.state.write();
1363        let st = guard.get_or_create(&account);
1364        let repo = st
1365            .repositories
1366            .get_mut(&name)
1367            .ok_or_else(|| repo_not_found(&name))?;
1368        let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1369        let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1370        if !is_ancestor(repo, &dest, &source) {
1371            return Err(err(
1372                "ManualMergeRequiredException",
1373                "The fast-forward merge cannot be performed because the destination is not an ancestor of the source.",
1374            ));
1375        }
1376        let tree_id = repo
1377            .commits
1378            .get(&source)
1379            .and_then(|c| c.get("treeId"))
1380            .and_then(Value::as_str)
1381            .unwrap_or_default()
1382            .to_string();
1383        if let Some(target) = str_field(&b, "targetBranch") {
1384            repo.branches.insert(target.clone(), source.clone());
1385            refresh_source_revisions(st, &name, &target);
1386        }
1387        ok(json!({ "commitId": source, "treeId": tree_id }))
1388    }
1389
1390    fn merge_branches_squash(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1391        self.merge_branches_create(req, "SQUASH_MERGE")
1392    }
1393
1394    fn merge_branches_three_way(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1395        self.merge_branches_create(req, "THREE_WAY_MERGE")
1396    }
1397
1398    fn merge_branches_create(
1399        &self,
1400        req: &AwsRequest,
1401        merge_option: &str,
1402    ) -> Result<AwsResponse, AwsServiceError> {
1403        let b = body(req);
1404        let name = require_repository_name(&b)?;
1405        require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1406        require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1407        validate_merge_conflict_enums(&b)?;
1408        let account = self.account(req);
1409        let mut guard = self.state.write();
1410        let st = guard.get_or_create(&account);
1411        let repo = st
1412            .repositories
1413            .get_mut(&name)
1414            .ok_or_else(|| repo_not_found(&name))?;
1415        let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1416        let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1417        let strategy = str_field(&b, "conflictResolutionStrategy").unwrap_or_default();
1418        let merged = merge_trees(repo, &source, &dest, &strategy).ok_or_else(|| {
1419            err(
1420                "ManualMergeRequiredException",
1421                "The merge cannot be completed because there are merge conflicts that must be resolved manually.",
1422            )
1423        })?;
1424        let tree_id = tree_id_for(&merged);
1425        let commit_id = fresh_object_id();
1426        let parents = merge_parents(merge_option, &dest, &source);
1427        let commit = build_commit(&commit_id, &tree_id, &parents, &b, req);
1428        repo.commits.insert(commit_id.clone(), commit);
1429        repo.trees.insert(commit_id.clone(), merged);
1430        if let Some(target) = str_field(&b, "targetBranch") {
1431            repo.branches.insert(target.clone(), commit_id.clone());
1432            refresh_source_revisions(st, &name, &target);
1433        }
1434        ok(json!({ "commitId": commit_id, "treeId": tree_id }))
1435    }
1436
1437    fn create_unreferenced_merge_commit(
1438        &self,
1439        req: &AwsRequest,
1440    ) -> Result<AwsResponse, AwsServiceError> {
1441        let b = body(req);
1442        let name = require_repository_name(&b)?;
1443        require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1444        require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1445        let merge_option = require(&b, "mergeOption", "MergeOptionRequiredException")?;
1446        if !validate::is_enum(validate::MERGE_OPTION, &merge_option) {
1447            return Err(err(
1448                "InvalidMergeOptionException",
1449                "The merge option is not valid.",
1450            ));
1451        }
1452        validate_merge_conflict_enums(&b)?;
1453        let account = self.account(req);
1454        let mut guard = self.state.write();
1455        let st = guard.get_or_create(&account);
1456        let repo = st
1457            .repositories
1458            .get_mut(&name)
1459            .ok_or_else(|| repo_not_found(&name))?;
1460        let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1461        let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1462        let strategy = str_field(&b, "conflictResolutionStrategy").unwrap_or_default();
1463        let merged = merge_trees(repo, &source, &dest, &strategy).ok_or_else(|| {
1464            err(
1465                "ManualMergeRequiredException",
1466                "The merge cannot be completed because there are merge conflicts that must be resolved manually.",
1467            )
1468        })?;
1469        let tree_id = tree_id_for(&merged);
1470        let commit_id = fresh_object_id();
1471        let parents = merge_parents(&merge_option, &dest, &source);
1472        let commit = build_commit(&commit_id, &tree_id, &parents, &b, req);
1473        repo.commits.insert(commit_id.clone(), commit);
1474        repo.trees.insert(commit_id.clone(), merged);
1475        ok(json!({ "commitId": commit_id, "treeId": tree_id }))
1476    }
1477
1478    fn get_merge_commit(&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        ok(json!({
1493            "sourceCommitId": source,
1494            "destinationCommitId": dest,
1495            "baseCommitId": base,
1496        }))
1497    }
1498
1499    fn get_merge_options(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1500        let b = body(req);
1501        let name = require_repository_name(&b)?;
1502        require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1503        require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1504        let account = self.account(req);
1505        let guard = self.state.read();
1506        let st = guard.get(&account);
1507        let repo = st
1508            .and_then(|s| s.repositories.get(&name))
1509            .ok_or_else(|| repo_not_found(&name))?;
1510        let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1511        let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1512        let base = merge_base(repo, &source, &dest);
1513        let mut options = vec![json!("SQUASH_MERGE"), json!("THREE_WAY_MERGE")];
1514        if is_ancestor(repo, &dest, &source) {
1515            options.insert(0, json!("FAST_FORWARD_MERGE"));
1516        }
1517        ok(json!({
1518            "mergeOptions": options,
1519            "sourceCommitId": source,
1520            "destinationCommitId": dest,
1521            "baseCommitId": base,
1522        }))
1523    }
1524
1525    fn get_merge_conflicts(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1526        let b = body(req);
1527        let name = require_repository_name(&b)?;
1528        require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1529        require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1530        let merge_option = require(&b, "mergeOption", "MergeOptionRequiredException")?;
1531        if !validate::is_enum(validate::MERGE_OPTION, &merge_option) {
1532            return Err(err(
1533                "InvalidMergeOptionException",
1534                "The merge option is not valid.",
1535            ));
1536        }
1537        let account = self.account(req);
1538        let guard = self.state.read();
1539        let st = guard.get(&account);
1540        let repo = st
1541            .and_then(|s| s.repositories.get(&name))
1542            .ok_or_else(|| repo_not_found(&name))?;
1543        let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1544        let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1545        let empty = std::collections::BTreeMap::new();
1546        let (base, base_tree, src_tree, dst_tree) = merge_inputs(repo, &source, &dest, &empty);
1547        let conflicts = conflicting_paths_in(base_tree, src_tree, dst_tree);
1548        let mergeable = conflicts.is_empty();
1549        let metadata: Vec<Value> = conflicts
1550            .iter()
1551            .map(|p| conflict_metadata(repo, &source, &dest, base_tree, src_tree, dst_tree, p).0)
1552            .collect();
1553        ok(json!({
1554            "mergeable": mergeable,
1555            "destinationCommitId": dest,
1556            "sourceCommitId": source,
1557            "baseCommitId": base,
1558            "conflictMetadataList": metadata,
1559        }))
1560    }
1561
1562    fn describe_merge_conflicts(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1563        let b = body(req);
1564        let name = require_repository_name(&b)?;
1565        require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1566        require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1567        let merge_option = require(&b, "mergeOption", "MergeOptionRequiredException")?;
1568        if !validate::is_enum(validate::MERGE_OPTION, &merge_option) {
1569            return Err(err(
1570                "InvalidMergeOptionException",
1571                "The merge option is not valid.",
1572            ));
1573        }
1574        let path = require(&b, "filePath", "PathRequiredException")?;
1575        let account = self.account(req);
1576        let guard = self.state.read();
1577        let st = guard.get(&account);
1578        let repo = st
1579            .and_then(|s| s.repositories.get(&name))
1580            .ok_or_else(|| repo_not_found(&name))?;
1581        let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1582        let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1583        let empty = std::collections::BTreeMap::new();
1584        let (base, base_tree, src_tree, dst_tree) = merge_inputs(repo, &source, &dest, &empty);
1585        if base_tree.get(&path).is_none()
1586            && src_tree.get(&path).is_none()
1587            && dst_tree.get(&path).is_none()
1588        {
1589            return Err(err(
1590                "FileDoesNotExistException",
1591                "The specified file does not exist.",
1592            ));
1593        }
1594        let (metadata, hunks) =
1595            conflict_metadata(repo, &source, &dest, base_tree, src_tree, dst_tree, &path);
1596        ok(json!({
1597            "conflictMetadata": metadata,
1598            "mergeHunks": hunks,
1599            "destinationCommitId": dest,
1600            "sourceCommitId": source,
1601            "baseCommitId": base,
1602        }))
1603    }
1604
1605    fn batch_describe_merge_conflicts(
1606        &self,
1607        req: &AwsRequest,
1608    ) -> Result<AwsResponse, AwsServiceError> {
1609        let b = body(req);
1610        let name = require_repository_name(&b)?;
1611        require(&b, "sourceCommitSpecifier", "CommitRequiredException")?;
1612        require(&b, "destinationCommitSpecifier", "CommitRequiredException")?;
1613        let merge_option = require(&b, "mergeOption", "MergeOptionRequiredException")?;
1614        if !validate::is_enum(validate::MERGE_OPTION, &merge_option) {
1615            return Err(err(
1616                "InvalidMergeOptionException",
1617                "The merge option is not valid.",
1618            ));
1619        }
1620        let account = self.account(req);
1621        let guard = self.state.read();
1622        let st = guard.get(&account);
1623        let repo = st
1624            .and_then(|s| s.repositories.get(&name))
1625            .ok_or_else(|| repo_not_found(&name))?;
1626        let source = resolve_commit(repo, &b, "sourceCommitSpecifier")?;
1627        let dest = resolve_commit(repo, &b, "destinationCommitSpecifier")?;
1628        let max_files = b
1629            .get("maxConflictFiles")
1630            .and_then(Value::as_i64)
1631            .filter(|n| *n >= 0)
1632            .map_or(usize::MAX, |n| n as usize);
1633        let max_hunks = b
1634            .get("maxMergeHunks")
1635            .and_then(Value::as_i64)
1636            .filter(|n| *n >= 0)
1637            .map_or(usize::MAX, |n| n as usize);
1638        let empty = std::collections::BTreeMap::new();
1639        let (base, base_tree, src_tree, dst_tree) = merge_inputs(repo, &source, &dest, &empty);
1640        let mut conflicts = Vec::new();
1641        for path in conflicting_paths_in(base_tree, src_tree, dst_tree) {
1642            if conflicts.len() >= max_files {
1643                break;
1644            }
1645            let (metadata, mut hunks) =
1646                conflict_metadata(repo, &source, &dest, base_tree, src_tree, dst_tree, &path);
1647            hunks.truncate(max_hunks);
1648            conflicts.push(json!({
1649                "conflictMetadata": metadata,
1650                "mergeHunks": hunks,
1651            }));
1652        }
1653        ok(json!({
1654            "conflicts": conflicts,
1655            "errors": [],
1656            "destinationCommitId": dest,
1657            "sourceCommitId": source,
1658            "baseCommitId": base,
1659        }))
1660    }
1661
1662    // ----- Pull requests -----
1663
1664    fn create_pull_request(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1665        let b = body(req);
1666        let title = match str_field(&b, "title") {
1667            Some(t) => t,
1668            None => {
1669                return Err(err(
1670                    "TitleRequiredException",
1671                    "A pull request title is required.",
1672                ))
1673            }
1674        };
1675        if title.chars().count() > 150 {
1676            return Err(err("InvalidTitleException", "The title is not valid."));
1677        }
1678        let targets = b
1679            .get("targets")
1680            .and_then(Value::as_array)
1681            .filter(|t| !t.is_empty())
1682            .ok_or_else(|| {
1683                err(
1684                    "TargetsRequiredException",
1685                    "Pull request targets are required.",
1686                )
1687            })?
1688            .clone();
1689        if let Some(d) = str_field(&b, "description") {
1690            if d.chars().count() > 10240 {
1691                return Err(err(
1692                    "InvalidDescriptionException",
1693                    "The description is not valid.",
1694                ));
1695            }
1696        }
1697        let account = self.account(req);
1698        let author = caller_arn(req);
1699        let mut guard = self.state.write();
1700        let st = guard.get_or_create(&account);
1701        let now = Utc::now();
1702        let mut pr_targets = Vec::new();
1703        let mut approval_rules = Vec::new();
1704        for t in &targets {
1705            let repo_name = t
1706                .get("repositoryName")
1707                .and_then(Value::as_str)
1708                .ok_or_else(|| {
1709                    err(
1710                        "RepositoryNameRequiredException",
1711                        "A repository name is required.",
1712                    )
1713                })?;
1714            let repo = st
1715                .repositories
1716                .get(repo_name)
1717                .ok_or_else(|| repo_not_found(repo_name))?;
1718            let source_ref = t
1719                .get("sourceReference")
1720                .and_then(Value::as_str)
1721                .ok_or_else(|| {
1722                    err(
1723                        "ReferenceNameRequiredException",
1724                        "A source reference is required.",
1725                    )
1726                })?;
1727            let dest_ref = t
1728                .get("destinationReference")
1729                .and_then(Value::as_str)
1730                .map(str::to_string)
1731                .or_else(|| {
1732                    repo.metadata
1733                        .get("defaultBranch")
1734                        .and_then(Value::as_str)
1735                        .map(str::to_string)
1736                })
1737                .unwrap_or_else(|| "main".to_string());
1738            if source_ref == dest_ref {
1739                return Err(err(
1740                    "SourceAndDestinationAreSameException",
1741                    "The source reference and destination reference are the same. You must specify different references for the source and destination.",
1742                ));
1743            }
1744            let source_commit = repo.branches.get(source_ref).cloned().ok_or_else(|| {
1745                err(
1746                    "ReferenceDoesNotExistException",
1747                    "The specified reference does not exist.",
1748                )
1749            })?;
1750            // The destination reference must also resolve to a real branch.
1751            let dest_commit = repo.branches.get(&dest_ref).cloned().ok_or_else(|| {
1752                err(
1753                    "ReferenceDoesNotExistException",
1754                    "The specified reference does not exist.",
1755                )
1756            })?;
1757            let base = merge_base(repo, &source_commit, &dest_commit);
1758            pr_targets.push(json!({
1759                "repositoryName": repo_name,
1760                "sourceReference": source_ref,
1761                "destinationReference": dest_ref,
1762                "sourceCommit": source_commit,
1763                "destinationCommit": dest_commit,
1764                "mergeBase": base,
1765                "mergeMetadata": { "isMerged": false },
1766            }));
1767            // Auto-populate approval rules from every approval-rule template
1768            // associated with the target repository (exactly as CodeCommit does).
1769            for tmpl_name in &repo.associated_templates {
1770                if approval_rules.iter().any(|r: &Value| {
1771                    r.get("approvalRuleName").and_then(Value::as_str) == Some(tmpl_name.as_str())
1772                }) {
1773                    continue;
1774                }
1775                if let Some(tmpl) = st.templates.get(tmpl_name) {
1776                    let content = tmpl
1777                        .get("approvalRuleTemplateContent")
1778                        .and_then(Value::as_str)
1779                        .unwrap_or_default()
1780                        .to_string();
1781                    approval_rules.push(json!({
1782                        "approvalRuleId": new_uuid(),
1783                        "approvalRuleName": tmpl_name,
1784                        "approvalRuleContent": content,
1785                        "ruleContentSha256": sha256_hex(content.as_bytes()),
1786                        "lastModifiedDate": ts(now),
1787                        "creationDate": ts(now),
1788                        "lastModifiedUser": author,
1789                        "originApprovalRuleTemplate": {
1790                            "approvalRuleTemplateId": tmpl
1791                                .get("approvalRuleTemplateId")
1792                                .cloned()
1793                                .unwrap_or(Value::Null),
1794                            "approvalRuleTemplateName": tmpl_name,
1795                        },
1796                    }));
1797                }
1798            }
1799        }
1800        st.pull_request_counter += 1;
1801        let pr_id = st.pull_request_counter.to_string();
1802        let revision_id = fresh_object_id();
1803        let pr = json!({
1804            "pullRequestId": pr_id,
1805            "title": title,
1806            "description": str_field(&b, "description").unwrap_or_default(),
1807            "lastActivityDate": ts(now),
1808            "creationDate": ts(now),
1809            "pullRequestStatus": "OPEN",
1810            "authorArn": author,
1811            "pullRequestTargets": pr_targets,
1812            "clientRequestToken": str_field(&b, "clientRequestToken").unwrap_or_default(),
1813            "revisionId": revision_id,
1814            "approvalRules": approval_rules,
1815        });
1816        st.pull_requests.insert(pr_id.clone(), pr.clone());
1817        st.pull_request_order.push(pr_id.clone());
1818        st.pull_request_events.insert(
1819            pr_id.clone(),
1820            vec![pr_event(
1821                &pr_id,
1822                "PULL_REQUEST_CREATED",
1823                &author,
1824                now,
1825                json!({}),
1826            )],
1827        );
1828        ok(json!({ "pullRequest": pr }))
1829    }
1830
1831    fn get_pull_request(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1832        let b = body(req);
1833        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
1834        let account = self.account(req);
1835        let guard = self.state.read();
1836        let st = guard.get(&account);
1837        // An open PR's target commits/mergeBase are recomputed from live branch
1838        // tips so a read always reflects what a merge would actually use.
1839        match st.and_then(|s| s.pull_requests.get(&id).map(|pr| refresh_pr_targets(s, pr))) {
1840            Some(pr) => ok(json!({ "pullRequest": pr })),
1841            None => Err(pr_not_found()),
1842        }
1843    }
1844
1845    fn list_pull_requests(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1846        let b = body(req);
1847        let name = require_repository_name(&b)?;
1848        check_enum(
1849            &b,
1850            "pullRequestStatus",
1851            validate::PULL_REQUEST_STATUS,
1852            "InvalidPullRequestStatusException",
1853        )?;
1854        let author_filter = str_field(&b, "authorArn");
1855        let status_filter = str_field(&b, "pullRequestStatus");
1856        let account = self.account(req);
1857        let guard = self.state.read();
1858        let st = guard.get(&account);
1859        let s = st.ok_or_else(|| repo_not_found(&name))?;
1860        if !s.repositories.contains_key(&name) {
1861            return Err(repo_not_found(&name));
1862        }
1863        let mut ids = Vec::new();
1864        for pid in &s.pull_request_order {
1865            let Some(pr) = s.pull_requests.get(pid) else {
1866                continue;
1867            };
1868            let in_repo = pr
1869                .get("pullRequestTargets")
1870                .and_then(Value::as_array)
1871                .map(|ts| {
1872                    ts.iter().any(|t| {
1873                        t.get("repositoryName").and_then(Value::as_str) == Some(name.as_str())
1874                    })
1875                })
1876                .unwrap_or(false);
1877            if !in_repo {
1878                continue;
1879            }
1880            if let Some(a) = &author_filter {
1881                if pr.get("authorArn").and_then(Value::as_str) != Some(a.as_str()) {
1882                    continue;
1883                }
1884            }
1885            if let Some(s2) = &status_filter {
1886                if pr.get("pullRequestStatus").and_then(Value::as_str) != Some(s2.as_str()) {
1887                    continue;
1888                }
1889            }
1890            ids.push(json!(pid));
1891        }
1892        ok(json!({ "pullRequestIds": ids }))
1893    }
1894
1895    fn update_pull_request_title(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1896        let b = body(req);
1897        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
1898        let title = match str_field(&b, "title") {
1899            Some(t) => t,
1900            None => {
1901                return Err(err(
1902                    "TitleRequiredException",
1903                    "A pull request title is required.",
1904                ))
1905            }
1906        };
1907        if title.chars().count() > 150 {
1908            return Err(err("InvalidTitleException", "The title is not valid."));
1909        }
1910        self.mutate_pr(req, &id, |pr| {
1911            pr["title"] = json!(title);
1912            pr["lastActivityDate"] = ts(Utc::now());
1913            Ok(())
1914        })
1915    }
1916
1917    fn update_pull_request_description(
1918        &self,
1919        req: &AwsRequest,
1920    ) -> Result<AwsResponse, AwsServiceError> {
1921        let b = body(req);
1922        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
1923        let desc = str_field(&b, "description").unwrap_or_default();
1924        if desc.chars().count() > 10240 {
1925            return Err(err(
1926                "InvalidDescriptionException",
1927                "The description is not valid.",
1928            ));
1929        }
1930        self.mutate_pr(req, &id, |pr| {
1931            pr["description"] = json!(desc);
1932            pr["lastActivityDate"] = ts(Utc::now());
1933            Ok(())
1934        })
1935    }
1936
1937    fn update_pull_request_status(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1938        let b = body(req);
1939        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
1940        let status = require(
1941            &b,
1942            "pullRequestStatus",
1943            "PullRequestStatusRequiredException",
1944        )?;
1945        if !validate::is_enum(validate::PULL_REQUEST_STATUS, &status) {
1946            return Err(err(
1947                "InvalidPullRequestStatusException",
1948                "The pull request status is not valid.",
1949            ));
1950        }
1951        self.mutate_pr(req, &id, |pr| {
1952            let current = pr.get("pullRequestStatus").and_then(Value::as_str).unwrap_or("OPEN");
1953            // The only valid transition is OPEN -> CLOSED. Reopening a closed PR
1954            // (CLOSED -> *), re-closing (CLOSED -> CLOSED), and a no-op
1955            // OPEN -> OPEN are all rejected, matching CodeCommit.
1956            if !(current == "OPEN" && status == "CLOSED") {
1957                return Err(err(
1958                    "InvalidPullRequestStatusUpdateException",
1959                    "The pull request status update is not valid. The only valid update is from OPEN to CLOSED.",
1960                ));
1961            }
1962            pr["pullRequestStatus"] = json!(status);
1963            pr["lastActivityDate"] = ts(Utc::now());
1964            Ok(())
1965        })
1966    }
1967
1968    fn describe_pull_request_events(
1969        &self,
1970        req: &AwsRequest,
1971    ) -> Result<AwsResponse, AwsServiceError> {
1972        let b = body(req);
1973        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
1974        check_enum(
1975            &b,
1976            "pullRequestEventType",
1977            validate::PULL_REQUEST_EVENT_TYPE,
1978            "InvalidPullRequestEventTypeException",
1979        )?;
1980        let account = self.account(req);
1981        let guard = self.state.read();
1982        let st = guard.get(&account);
1983        let s = st.ok_or_else(pr_not_found)?;
1984        if !s.pull_requests.contains_key(&id) {
1985            return Err(pr_not_found());
1986        }
1987        let events = s.pull_request_events.get(&id).cloned().unwrap_or_default();
1988        ok(json!({ "pullRequestEvents": events }))
1989    }
1990
1991    fn merge_pull_request_fast_forward(
1992        &self,
1993        req: &AwsRequest,
1994    ) -> Result<AwsResponse, AwsServiceError> {
1995        self.merge_pull_request(req, "FAST_FORWARD_MERGE")
1996    }
1997
1998    fn merge_pull_request_squash(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1999        self.merge_pull_request(req, "SQUASH_MERGE")
2000    }
2001
2002    fn merge_pull_request_three_way(
2003        &self,
2004        req: &AwsRequest,
2005    ) -> Result<AwsResponse, AwsServiceError> {
2006        self.merge_pull_request(req, "THREE_WAY_MERGE")
2007    }
2008
2009    fn merge_pull_request(
2010        &self,
2011        req: &AwsRequest,
2012        merge_option: &str,
2013    ) -> Result<AwsResponse, AwsServiceError> {
2014        let b = body(req);
2015        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2016        let name = require_repository_name(&b)?;
2017        validate_merge_conflict_enums(&b)?;
2018        let author = caller_arn(req);
2019        let account = self.account(req);
2020        let mut guard = self.state.write();
2021        let st = guard.get_or_create(&account);
2022        if !st.repositories.contains_key(&name) {
2023            return Err(repo_not_found(&name));
2024        }
2025        let now = Utc::now();
2026
2027        // Read the pull request's target and rules without holding a mutable
2028        // borrow across the object-store mutation below.
2029        let (source_ref, dest_ref, revision, rules) = {
2030            let pr = st.pull_requests.get(&id).ok_or_else(pr_not_found)?;
2031            if pr.get("pullRequestStatus").and_then(Value::as_str) == Some("CLOSED") {
2032                return Err(err(
2033                    "PullRequestAlreadyClosedException",
2034                    "The pull request status cannot be updated because it is already closed.",
2035                ));
2036            }
2037            let target = pr
2038                .get("pullRequestTargets")
2039                .and_then(Value::as_array)
2040                .and_then(|ts| {
2041                    ts.iter().find(|t| {
2042                        t.get("repositoryName").and_then(Value::as_str) == Some(name.as_str())
2043                    })
2044                })
2045                .ok_or_else(|| {
2046                    err(
2047                        "RepositoryNotAssociatedWithPullRequestException",
2048                        "The repository is not associated with the pull request.",
2049                    )
2050                })?;
2051            let source_ref = target
2052                .get("sourceReference")
2053                .and_then(Value::as_str)
2054                .unwrap_or_default()
2055                .to_string();
2056            let dest_ref = target
2057                .get("destinationReference")
2058                .and_then(Value::as_str)
2059                .unwrap_or_default()
2060                .to_string();
2061            let revision = pr
2062                .get("revisionId")
2063                .and_then(Value::as_str)
2064                .unwrap_or_default()
2065                .to_string();
2066            let rules = pr
2067                .get("approvalRules")
2068                .and_then(Value::as_array)
2069                .cloned()
2070                .unwrap_or_default();
2071            (source_ref, dest_ref, revision, rules)
2072        };
2073
2074        // Enforce approval rules (unless overridden), exactly as AWS does before
2075        // allowing a merge.
2076        let overridden = st
2077            .pull_request_overrides
2078            .get(&id)
2079            .and_then(|m| m.get(&revision))
2080            .is_some();
2081        if !overridden && !rules.is_empty() {
2082            let approvers: std::collections::BTreeSet<String> = st
2083                .pull_request_approvals
2084                .get(&id)
2085                .and_then(|m| m.get(&revision))
2086                .map(|m| {
2087                    m.iter()
2088                        .filter(|(_, v)| v.as_str() == "APPROVE")
2089                        .map(|(arn, _)| arn.clone())
2090                        .collect()
2091                })
2092                .unwrap_or_default();
2093            let all_satisfied = rules.iter().all(|r| {
2094                let content = r
2095                    .get("approvalRuleContent")
2096                    .and_then(Value::as_str)
2097                    .unwrap_or_default();
2098                rule_satisfied(content, &approvers)
2099            });
2100            if !all_satisfied {
2101                return Err(err(
2102                    "PullRequestApprovalRulesNotSatisfiedException",
2103                    "The pull request cannot be merged because one or more approval rules have not been satisfied.",
2104                ));
2105            }
2106        }
2107
2108        // Resolve the current tips and perform the merge against the object
2109        // store, advancing the destination branch to the resulting commit.
2110        let strategy = str_field(&b, "conflictResolutionStrategy").unwrap_or_default();
2111        let repo = st.repositories.get_mut(&name).expect("repo checked above");
2112        let source_tip = repo.branches.get(&source_ref).cloned().ok_or_else(|| {
2113            err(
2114                "ReferenceDoesNotExistException",
2115                "The specified reference does not exist.",
2116            )
2117        })?;
2118        let dest_tip = repo.branches.get(&dest_ref).cloned().ok_or_else(|| {
2119            err(
2120                "ReferenceDoesNotExistException",
2121                "The specified reference does not exist.",
2122            )
2123        })?;
2124        // Concurrency check: if the caller pins a source tip it must still match.
2125        if let Some(pinned) = str_field(&b, "sourceCommitId") {
2126            if pinned != source_tip {
2127                return Err(err(
2128                    "TipOfSourceReferenceIsDifferentException",
2129                    "The tip of the source reference is different from the specified commit ID.",
2130                ));
2131            }
2132        }
2133        let merge_commit_id = match merge_option {
2134            "FAST_FORWARD_MERGE" => {
2135                if !is_ancestor(repo, &dest_tip, &source_tip) {
2136                    return Err(err(
2137                        "ManualMergeRequiredException",
2138                        "The fast-forward merge cannot be performed because the destination is not an ancestor of the source.",
2139                    ));
2140                }
2141                repo.branches.insert(dest_ref.clone(), source_tip.clone());
2142                source_tip.clone()
2143            }
2144            _ => {
2145                let merged =
2146                    merge_trees(repo, &source_tip, &dest_tip, &strategy).ok_or_else(|| {
2147                        err(
2148                            "ManualMergeRequiredException",
2149                            "The merge cannot be completed because there are merge conflicts that must be resolved manually.",
2150                        )
2151                    })?;
2152                let tree_id = tree_id_for(&merged);
2153                let commit_id = fresh_object_id();
2154                let parents = merge_parents(merge_option, &dest_tip, &source_tip);
2155                let commit = build_commit(&commit_id, &tree_id, &parents, &b, req);
2156                repo.commits.insert(commit_id.clone(), commit);
2157                repo.trees.insert(commit_id.clone(), merged);
2158                repo.branches.insert(dest_ref.clone(), commit_id.clone());
2159                commit_id
2160            }
2161        };
2162
2163        // Advancing the destination branch invalidates approvals on any other
2164        // open PR sourced from it.
2165        refresh_source_revisions(st, &name, &dest_ref);
2166
2167        // Close the pull request and record the real merge metadata.
2168        let pr = st.pull_requests.get_mut(&id).expect("pr checked above");
2169        pr["pullRequestStatus"] = json!("CLOSED");
2170        pr["lastActivityDate"] = ts(now);
2171        if let Some(targets) = pr
2172            .get_mut("pullRequestTargets")
2173            .and_then(Value::as_array_mut)
2174        {
2175            for t in targets.iter_mut() {
2176                if t.get("repositoryName").and_then(Value::as_str) == Some(name.as_str()) {
2177                    t["mergeMetadata"] = json!({
2178                        "isMerged": true,
2179                        "mergedBy": author,
2180                        "mergeCommitId": merge_commit_id,
2181                        "mergeOption": merge_option,
2182                    });
2183                }
2184            }
2185        }
2186        let pr = pr.clone();
2187        st.pull_request_events
2188            .entry(id.clone())
2189            .or_default()
2190            .push(pr_event(
2191                &id,
2192                "PULL_REQUEST_MERGE_STATE_CHANGED",
2193                &author,
2194                now,
2195                json!({}),
2196            ));
2197        ok(json!({ "pullRequest": pr }))
2198    }
2199
2200    // ----- Pull-request approval rules / states -----
2201
2202    fn create_pull_request_approval_rule(
2203        &self,
2204        req: &AwsRequest,
2205    ) -> Result<AwsResponse, AwsServiceError> {
2206        let b = body(req);
2207        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2208        let rule_name = require(&b, "approvalRuleName", "ApprovalRuleNameRequiredException")?;
2209        let content = require(
2210            &b,
2211            "approvalRuleContent",
2212            "ApprovalRuleContentRequiredException",
2213        )?;
2214        if rule_name.chars().count() > 100 {
2215            return Err(err(
2216                "InvalidApprovalRuleNameException",
2217                "The approval rule name is not valid.",
2218            ));
2219        }
2220        if content.chars().count() > 3000 {
2221            return Err(err(
2222                "InvalidApprovalRuleContentException",
2223                "The approval rule content is not valid.",
2224            ));
2225        }
2226        let content = normalize_rule_content(&content);
2227        let now = Utc::now();
2228        let user = caller_arn(req);
2229        let account = self.account(req);
2230        let mut guard = self.state.write();
2231        let st = guard.get_or_create(&account);
2232        let pr = st.pull_requests.get_mut(&id).ok_or_else(pr_not_found)?;
2233        if pr.get("pullRequestStatus").and_then(Value::as_str) == Some("CLOSED") {
2234            return Err(err(
2235                "PullRequestAlreadyClosedException",
2236                "The pull request is already closed.",
2237            ));
2238        }
2239        let rules = pr
2240            .get_mut("approvalRules")
2241            .and_then(Value::as_array_mut)
2242            .ok_or_else(pr_not_found)?;
2243        if rules
2244            .iter()
2245            .any(|r| r.get("approvalRuleName").and_then(Value::as_str) == Some(rule_name.as_str()))
2246        {
2247            return Err(err(
2248                "ApprovalRuleNameAlreadyExistsException",
2249                "An approval rule with that name already exists.",
2250            ));
2251        }
2252        let rule = json!({
2253            "approvalRuleId": new_uuid(),
2254            "approvalRuleName": rule_name,
2255            "approvalRuleContent": content,
2256            "ruleContentSha256": sha256_hex(content.as_bytes()),
2257            "lastModifiedDate": ts(now),
2258            "creationDate": ts(now),
2259            "lastModifiedUser": user,
2260        });
2261        rules.push(rule.clone());
2262        ok(json!({ "approvalRule": rule }))
2263    }
2264
2265    fn delete_pull_request_approval_rule(
2266        &self,
2267        req: &AwsRequest,
2268    ) -> Result<AwsResponse, AwsServiceError> {
2269        let b = body(req);
2270        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2271        let rule_name = require(&b, "approvalRuleName", "ApprovalRuleNameRequiredException")?;
2272        let account = self.account(req);
2273        let mut guard = self.state.write();
2274        let st = guard.get_or_create(&account);
2275        let pr = st.pull_requests.get_mut(&id).ok_or_else(pr_not_found)?;
2276        let mut deleted_id = String::new();
2277        if let Some(rules) = pr.get_mut("approvalRules").and_then(Value::as_array_mut) {
2278            if let Some(pos) = rules.iter().position(|r| {
2279                r.get("approvalRuleName").and_then(Value::as_str) == Some(rule_name.as_str())
2280            }) {
2281                deleted_id = rules[pos]
2282                    .get("approvalRuleId")
2283                    .and_then(Value::as_str)
2284                    .unwrap_or_default()
2285                    .to_string();
2286                rules.remove(pos);
2287            }
2288        }
2289        ok(json!({ "approvalRuleId": deleted_id }))
2290    }
2291
2292    fn update_pull_request_approval_rule_content(
2293        &self,
2294        req: &AwsRequest,
2295    ) -> Result<AwsResponse, AwsServiceError> {
2296        let b = body(req);
2297        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2298        let rule_name = require(&b, "approvalRuleName", "ApprovalRuleNameRequiredException")?;
2299        let content = require(&b, "newRuleContent", "ApprovalRuleContentRequiredException")?;
2300        if content.chars().count() > 3000 {
2301            return Err(err(
2302                "InvalidApprovalRuleContentException",
2303                "The approval rule content is not valid.",
2304            ));
2305        }
2306        let content = normalize_rule_content(&content);
2307        let now = Utc::now();
2308        let user = caller_arn(req);
2309        let account = self.account(req);
2310        let mut guard = self.state.write();
2311        let st = guard.get_or_create(&account);
2312        let pr = st.pull_requests.get_mut(&id).ok_or_else(pr_not_found)?;
2313        let rules = pr
2314            .get_mut("approvalRules")
2315            .and_then(Value::as_array_mut)
2316            .ok_or_else(pr_not_found)?;
2317        let rule = rules
2318            .iter_mut()
2319            .find(|r| r.get("approvalRuleName").and_then(Value::as_str) == Some(rule_name.as_str()))
2320            .ok_or_else(|| {
2321                err(
2322                    "ApprovalRuleDoesNotExistException",
2323                    "The specified approval rule does not exist.",
2324                )
2325            })?;
2326        rule["approvalRuleContent"] = json!(content);
2327        rule["ruleContentSha256"] = json!(sha256_hex(content.as_bytes()));
2328        rule["lastModifiedDate"] = ts(now);
2329        rule["lastModifiedUser"] = json!(user);
2330        let rule = rule.clone();
2331        ok(json!({ "approvalRule": rule }))
2332    }
2333
2334    fn update_pull_request_approval_state(
2335        &self,
2336        req: &AwsRequest,
2337    ) -> Result<AwsResponse, AwsServiceError> {
2338        let b = body(req);
2339        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2340        let revision = require(&b, "revisionId", "RevisionIdRequiredException")?;
2341        let state = require(&b, "approvalState", "ApprovalStateRequiredException")?;
2342        if !validate::is_enum(validate::APPROVAL_STATE, &state) {
2343            return Err(err(
2344                "InvalidApprovalStateException",
2345                "The approval state is not valid.",
2346            ));
2347        }
2348        let user = caller_arn(req);
2349        let account = self.account(req);
2350        let mut guard = self.state.write();
2351        let st = guard.get_or_create(&account);
2352        let pr = st.pull_requests.get(&id).ok_or_else(pr_not_found)?;
2353        if pr.get("pullRequestStatus").and_then(Value::as_str) == Some("CLOSED") {
2354            return Err(err(
2355                "PullRequestAlreadyClosedException",
2356                "The pull request is already closed.",
2357            ));
2358        }
2359        let current_revision = pr
2360            .get("revisionId")
2361            .and_then(Value::as_str)
2362            .unwrap_or_default()
2363            .to_string();
2364        if revision != current_revision {
2365            return Err(err(
2366                "RevisionNotCurrentException",
2367                "The revision ID is not the current revision of the pull request.",
2368            ));
2369        }
2370        if pr.get("authorArn").and_then(Value::as_str) == Some(user.as_str()) {
2371            return Err(err(
2372                "PullRequestCannotBeApprovedByAuthorException",
2373                "The pull request cannot be approved by the author of the pull request.",
2374            ));
2375        }
2376        st.pull_request_approvals
2377            .entry(id)
2378            .or_default()
2379            .entry(revision)
2380            .or_default()
2381            .insert(user, state);
2382        ok(json!({}))
2383    }
2384
2385    fn get_pull_request_approval_states(
2386        &self,
2387        req: &AwsRequest,
2388    ) -> Result<AwsResponse, AwsServiceError> {
2389        let b = body(req);
2390        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2391        let revision = require(&b, "revisionId", "RevisionIdRequiredException")?;
2392        let account = self.account(req);
2393        let guard = self.state.read();
2394        let st = guard.get(&account);
2395        let s = st.ok_or_else(pr_not_found)?;
2396        if !s.pull_requests.contains_key(&id) {
2397            return Err(pr_not_found());
2398        }
2399        let approvals: Vec<Value> = s
2400            .pull_request_approvals
2401            .get(&id)
2402            .and_then(|m| m.get(&revision))
2403            .map(|m| {
2404                m.iter()
2405                    .filter(|(_, v)| v.as_str() == "APPROVE")
2406                    .map(|(arn, v)| json!({ "userArn": arn, "approvalState": v }))
2407                    .collect()
2408            })
2409            .unwrap_or_default();
2410        ok(json!({ "approvals": approvals }))
2411    }
2412
2413    fn evaluate_pull_request_approval_rules(
2414        &self,
2415        req: &AwsRequest,
2416    ) -> Result<AwsResponse, AwsServiceError> {
2417        let b = body(req);
2418        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2419        let revision = require(&b, "revisionId", "RevisionIdRequiredException")?;
2420        let account = self.account(req);
2421        let guard = self.state.read();
2422        let st = guard.get(&account);
2423        let s = st.ok_or_else(pr_not_found)?;
2424        let pr = s.pull_requests.get(&id).ok_or_else(pr_not_found)?;
2425        let current_revision = pr
2426            .get("revisionId")
2427            .and_then(Value::as_str)
2428            .unwrap_or_default();
2429        if revision != current_revision {
2430            return Err(err(
2431                "RevisionNotCurrentException",
2432                "The revision ID is not the current revision of the pull request.",
2433            ));
2434        }
2435        let overridden = s
2436            .pull_request_overrides
2437            .get(&id)
2438            .and_then(|m| m.get(&revision))
2439            .is_some();
2440        let rules = pr
2441            .get("approvalRules")
2442            .and_then(Value::as_array)
2443            .cloned()
2444            .unwrap_or_default();
2445        // Distinct reviewers who approved the current revision.
2446        let approvers: std::collections::BTreeSet<String> = s
2447            .pull_request_approvals
2448            .get(&id)
2449            .and_then(|m| m.get(&revision))
2450            .map(|m| {
2451                m.iter()
2452                    .filter(|(_, v)| v.as_str() == "APPROVE")
2453                    .map(|(arn, _)| arn.clone())
2454                    .collect()
2455            })
2456            .unwrap_or_default();
2457        let mut satisfied_names = Vec::new();
2458        let mut not_satisfied_names = Vec::new();
2459        for r in &rules {
2460            let Some(name) = r.get("approvalRuleName").cloned() else {
2461                continue;
2462            };
2463            let content = r
2464                .get("approvalRuleContent")
2465                .and_then(Value::as_str)
2466                .unwrap_or_default();
2467            // An override satisfies every rule; otherwise honor the rule's
2468            // NumberOfApprovalsNeeded and approver pool.
2469            if overridden || rule_satisfied(content, &approvers) {
2470                satisfied_names.push(name);
2471            } else {
2472                not_satisfied_names.push(name);
2473            }
2474        }
2475        let approved = not_satisfied_names.is_empty();
2476        ok(json!({
2477            "evaluation": {
2478                "approved": approved,
2479                "overridden": overridden,
2480                "approvalRulesSatisfied": satisfied_names,
2481                "approvalRulesNotSatisfied": not_satisfied_names,
2482            }
2483        }))
2484    }
2485
2486    fn override_pull_request_approval_rules(
2487        &self,
2488        req: &AwsRequest,
2489    ) -> Result<AwsResponse, AwsServiceError> {
2490        let b = body(req);
2491        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2492        let revision = require(&b, "revisionId", "RevisionIdRequiredException")?;
2493        let status = require(&b, "overrideStatus", "OverrideStatusRequiredException")?;
2494        if !validate::is_enum(validate::OVERRIDE_STATUS, &status) {
2495            return Err(err(
2496                "InvalidOverrideStatusException",
2497                "The override status is not valid.",
2498            ));
2499        }
2500        let user = caller_arn(req);
2501        let account = self.account(req);
2502        let mut guard = self.state.write();
2503        let st = guard.get_or_create(&account);
2504        {
2505            let pr = st.pull_requests.get(&id).ok_or_else(pr_not_found)?;
2506            let current_revision = pr
2507                .get("revisionId")
2508                .and_then(Value::as_str)
2509                .unwrap_or_default();
2510            if revision != current_revision {
2511                return Err(err(
2512                    "RevisionNotCurrentException",
2513                    "The revision ID is not the current revision of the pull request.",
2514                ));
2515            }
2516        }
2517        let overrides = st.pull_request_overrides.entry(id).or_default();
2518        if status == "OVERRIDE" {
2519            if overrides.contains_key(&revision) {
2520                return Err(err(
2521                    "OverrideAlreadySetException",
2522                    "The override status is already set.",
2523                ));
2524            }
2525            overrides.insert(revision, user);
2526        } else {
2527            overrides.remove(&revision);
2528        }
2529        ok(json!({}))
2530    }
2531
2532    fn get_pull_request_override_state(
2533        &self,
2534        req: &AwsRequest,
2535    ) -> Result<AwsResponse, AwsServiceError> {
2536        let b = body(req);
2537        let id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
2538        let revision = require(&b, "revisionId", "RevisionIdRequiredException")?;
2539        let account = self.account(req);
2540        let guard = self.state.read();
2541        let st = guard.get(&account);
2542        let s = st.ok_or_else(pr_not_found)?;
2543        if !s.pull_requests.contains_key(&id) {
2544            return Err(pr_not_found());
2545        }
2546        match s
2547            .pull_request_overrides
2548            .get(&id)
2549            .and_then(|m| m.get(&revision))
2550        {
2551            Some(arn) => ok(json!({ "overridden": true, "overrider": arn })),
2552            None => ok(json!({ "overridden": false })),
2553        }
2554    }
2555
2556    // ----- Approval-rule templates -----
2557
2558    fn create_approval_rule_template(
2559        &self,
2560        req: &AwsRequest,
2561    ) -> Result<AwsResponse, AwsServiceError> {
2562        let b = body(req);
2563        let name = require(
2564            &b,
2565            "approvalRuleTemplateName",
2566            "ApprovalRuleTemplateNameRequiredException",
2567        )?;
2568        let content = require(
2569            &b,
2570            "approvalRuleTemplateContent",
2571            "ApprovalRuleTemplateContentRequiredException",
2572        )?;
2573        if name.chars().count() > 100 {
2574            return Err(err(
2575                "InvalidApprovalRuleTemplateNameException",
2576                "The approval rule template name is not valid.",
2577            ));
2578        }
2579        if content.chars().count() > 3000 {
2580            return Err(err(
2581                "InvalidApprovalRuleTemplateContentException",
2582                "The approval rule template content is not valid.",
2583            ));
2584        }
2585        let content = normalize_rule_content(&content);
2586        if let Some(d) = str_field(&b, "approvalRuleTemplateDescription") {
2587            if d.chars().count() > 1000 {
2588                return Err(err(
2589                    "InvalidApprovalRuleTemplateDescriptionException",
2590                    "The approval rule template description is not valid.",
2591                ));
2592            }
2593        }
2594        let now = Utc::now();
2595        let user = caller_arn(req);
2596        let account = self.account(req);
2597        let mut guard = self.state.write();
2598        let st = guard.get_or_create(&account);
2599        if st.templates.contains_key(&name) {
2600            return Err(err(
2601                "ApprovalRuleTemplateNameAlreadyExistsException",
2602                format!("An approval rule template with the name {name} already exists."),
2603            ));
2604        }
2605        let mut tmpl = Map::new();
2606        tmpl.insert("approvalRuleTemplateId".into(), json!(new_uuid()));
2607        tmpl.insert("approvalRuleTemplateName".into(), json!(name));
2608        if let Some(d) = str_field(&b, "approvalRuleTemplateDescription") {
2609            tmpl.insert("approvalRuleTemplateDescription".into(), json!(d));
2610        }
2611        tmpl.insert("approvalRuleTemplateContent".into(), json!(content));
2612        tmpl.insert(
2613            "ruleContentSha256".into(),
2614            json!(sha256_hex(content.as_bytes())),
2615        );
2616        tmpl.insert("lastModifiedDate".into(), ts(now));
2617        tmpl.insert("creationDate".into(), ts(now));
2618        tmpl.insert("lastModifiedUser".into(), json!(user));
2619        let tmpl = Value::Object(tmpl);
2620        st.templates.insert(name.clone(), tmpl.clone());
2621        st.template_order.push(name);
2622        ok(json!({ "approvalRuleTemplate": tmpl }))
2623    }
2624
2625    fn get_approval_rule_template(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
2626        let b = body(req);
2627        let name = require(
2628            &b,
2629            "approvalRuleTemplateName",
2630            "ApprovalRuleTemplateNameRequiredException",
2631        )?;
2632        let account = self.account(req);
2633        let guard = self.state.read();
2634        let st = guard.get(&account);
2635        match st.and_then(|s| s.templates.get(&name)) {
2636            Some(t) => ok(json!({ "approvalRuleTemplate": t })),
2637            None => Err(template_not_found()),
2638        }
2639    }
2640
2641    fn delete_approval_rule_template(
2642        &self,
2643        req: &AwsRequest,
2644    ) -> Result<AwsResponse, AwsServiceError> {
2645        let b = body(req);
2646        let name = require(
2647            &b,
2648            "approvalRuleTemplateName",
2649            "ApprovalRuleTemplateNameRequiredException",
2650        )?;
2651        if name.chars().count() > 100 {
2652            return Err(err(
2653                "InvalidApprovalRuleTemplateNameException",
2654                "The approval rule template name is not valid.",
2655            ));
2656        }
2657        let account = self.account(req);
2658        let mut guard = self.state.write();
2659        let st = guard.get_or_create(&account);
2660        let id = st
2661            .templates
2662            .remove(&name)
2663            .and_then(|t| {
2664                t.get("approvalRuleTemplateId")
2665                    .and_then(Value::as_str)
2666                    .map(str::to_string)
2667            })
2668            .unwrap_or_default();
2669        st.template_order.retain(|n| n != &name);
2670        for repo in st.repositories.values_mut() {
2671            repo.associated_templates.retain(|t| t != &name);
2672        }
2673        ok(json!({ "approvalRuleTemplateId": id }))
2674    }
2675
2676    fn list_approval_rule_templates(
2677        &self,
2678        req: &AwsRequest,
2679    ) -> Result<AwsResponse, AwsServiceError> {
2680        let account = self.account(req);
2681        let guard = self.state.read();
2682        let st = guard.get(&account);
2683        let names: Vec<Value> = st
2684            .map(|s| s.template_order.iter().map(|n| json!(n)).collect())
2685            .unwrap_or_default();
2686        ok(json!({ "approvalRuleTemplateNames": names }))
2687    }
2688
2689    fn update_approval_rule_template_content(
2690        &self,
2691        req: &AwsRequest,
2692    ) -> Result<AwsResponse, AwsServiceError> {
2693        let b = body(req);
2694        let name = require(
2695            &b,
2696            "approvalRuleTemplateName",
2697            "ApprovalRuleTemplateNameRequiredException",
2698        )?;
2699        let content = require(
2700            &b,
2701            "newRuleContent",
2702            "ApprovalRuleTemplateContentRequiredException",
2703        )?;
2704        if content.chars().count() > 3000 {
2705            return Err(err(
2706                "InvalidApprovalRuleTemplateContentException",
2707                "The approval rule template content is not valid.",
2708            ));
2709        }
2710        let content = normalize_rule_content(&content);
2711        self.mutate_template(req, &name, |t| {
2712            t["approvalRuleTemplateContent"] = json!(content);
2713            t["ruleContentSha256"] = json!(sha256_hex(content.as_bytes()));
2714            t["lastModifiedDate"] = ts(Utc::now());
2715            Ok(())
2716        })
2717    }
2718
2719    fn update_approval_rule_template_description(
2720        &self,
2721        req: &AwsRequest,
2722    ) -> Result<AwsResponse, AwsServiceError> {
2723        let b = body(req);
2724        let name = require(
2725            &b,
2726            "approvalRuleTemplateName",
2727            "ApprovalRuleTemplateNameRequiredException",
2728        )?;
2729        let desc = require(
2730            &b,
2731            "approvalRuleTemplateDescription",
2732            "InvalidApprovalRuleTemplateDescriptionException",
2733        )?;
2734        if desc.chars().count() > 1000 {
2735            return Err(err(
2736                "InvalidApprovalRuleTemplateDescriptionException",
2737                "The approval rule template description is not valid.",
2738            ));
2739        }
2740        self.mutate_template(req, &name, |t| {
2741            t["approvalRuleTemplateDescription"] = json!(desc);
2742            t["lastModifiedDate"] = ts(Utc::now());
2743            Ok(())
2744        })
2745    }
2746
2747    fn update_approval_rule_template_name(
2748        &self,
2749        req: &AwsRequest,
2750    ) -> Result<AwsResponse, AwsServiceError> {
2751        let b = body(req);
2752        let old = require(
2753            &b,
2754            "oldApprovalRuleTemplateName",
2755            "ApprovalRuleTemplateNameRequiredException",
2756        )?;
2757        let new = require(
2758            &b,
2759            "newApprovalRuleTemplateName",
2760            "ApprovalRuleTemplateNameRequiredException",
2761        )?;
2762        if new.chars().count() > 100 {
2763            return Err(err(
2764                "InvalidApprovalRuleTemplateNameException",
2765                "The approval rule template name is not valid.",
2766            ));
2767        }
2768        let account = self.account(req);
2769        let mut guard = self.state.write();
2770        let st = guard.get_or_create(&account);
2771        if !st.templates.contains_key(&old) {
2772            return Err(template_not_found());
2773        }
2774        if old != new && st.templates.contains_key(&new) {
2775            return Err(err(
2776                "ApprovalRuleTemplateNameAlreadyExistsException",
2777                format!("An approval rule template with the name {new} already exists."),
2778            ));
2779        }
2780        let mut tmpl = st.templates.remove(&old).unwrap();
2781        tmpl["approvalRuleTemplateName"] = json!(new);
2782        tmpl["lastModifiedDate"] = ts(Utc::now());
2783        for n in &mut st.template_order {
2784            if n == &old {
2785                *n = new.clone();
2786            }
2787        }
2788        for repo in st.repositories.values_mut() {
2789            for t in &mut repo.associated_templates {
2790                if t == &old {
2791                    *t = new.clone();
2792                }
2793            }
2794        }
2795        st.templates.insert(new, tmpl.clone());
2796        ok(json!({ "approvalRuleTemplate": tmpl }))
2797    }
2798
2799    fn associate_template_with_repository(
2800        &self,
2801        req: &AwsRequest,
2802    ) -> Result<AwsResponse, AwsServiceError> {
2803        let b = body(req);
2804        let tmpl = require(
2805            &b,
2806            "approvalRuleTemplateName",
2807            "ApprovalRuleTemplateNameRequiredException",
2808        )?;
2809        let name = require_repository_name(&b)?;
2810        let account = self.account(req);
2811        let mut guard = self.state.write();
2812        let st = guard.get_or_create(&account);
2813        if !st.templates.contains_key(&tmpl) {
2814            return Err(template_not_found());
2815        }
2816        let repo = st
2817            .repositories
2818            .get_mut(&name)
2819            .ok_or_else(|| repo_not_found(&name))?;
2820        if !repo.associated_templates.contains(&tmpl) {
2821            repo.associated_templates.push(tmpl);
2822        }
2823        ok(json!({}))
2824    }
2825
2826    fn disassociate_template_from_repository(
2827        &self,
2828        req: &AwsRequest,
2829    ) -> Result<AwsResponse, AwsServiceError> {
2830        let b = body(req);
2831        let tmpl = require(
2832            &b,
2833            "approvalRuleTemplateName",
2834            "ApprovalRuleTemplateNameRequiredException",
2835        )?;
2836        let name = require_repository_name(&b)?;
2837        let account = self.account(req);
2838        let mut guard = self.state.write();
2839        let st = guard.get_or_create(&account);
2840        if !st.templates.contains_key(&tmpl) {
2841            return Err(template_not_found());
2842        }
2843        let repo = st
2844            .repositories
2845            .get_mut(&name)
2846            .ok_or_else(|| repo_not_found(&name))?;
2847        repo.associated_templates.retain(|t| t != &tmpl);
2848        ok(json!({}))
2849    }
2850
2851    fn batch_associate_template(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
2852        let b = body(req);
2853        let tmpl = require(
2854            &b,
2855            "approvalRuleTemplateName",
2856            "ApprovalRuleTemplateNameRequiredException",
2857        )?;
2858        let names = b
2859            .get("repositoryNames")
2860            .and_then(Value::as_array)
2861            .ok_or_else(|| {
2862                err(
2863                    "RepositoryNamesRequiredException",
2864                    "Repository names are required.",
2865                )
2866            })?
2867            .clone();
2868        let account = self.account(req);
2869        let mut guard = self.state.write();
2870        let st = guard.get_or_create(&account);
2871        if !st.templates.contains_key(&tmpl) {
2872            return Err(template_not_found());
2873        }
2874        let mut associated = Vec::new();
2875        let mut errors = Vec::new();
2876        for n in &names {
2877            let Some(rn) = n.as_str() else { continue };
2878            match st.repositories.get_mut(rn) {
2879                Some(repo) => {
2880                    if !repo.associated_templates.contains(&tmpl) {
2881                        repo.associated_templates.push(tmpl.clone());
2882                    }
2883                    associated.push(json!(rn));
2884                }
2885                None => errors.push(json!({
2886                    "repositoryName": rn,
2887                    "errorCode": "RepositoryDoesNotExist",
2888                    "errorMessage": format!("The repository {rn} does not exist."),
2889                })),
2890            }
2891        }
2892        ok(json!({ "associatedRepositoryNames": associated, "errors": errors }))
2893    }
2894
2895    fn batch_disassociate_template(
2896        &self,
2897        req: &AwsRequest,
2898    ) -> Result<AwsResponse, AwsServiceError> {
2899        let b = body(req);
2900        let tmpl = require(
2901            &b,
2902            "approvalRuleTemplateName",
2903            "ApprovalRuleTemplateNameRequiredException",
2904        )?;
2905        let names = b
2906            .get("repositoryNames")
2907            .and_then(Value::as_array)
2908            .ok_or_else(|| {
2909                err(
2910                    "RepositoryNamesRequiredException",
2911                    "Repository names are required.",
2912                )
2913            })?
2914            .clone();
2915        let account = self.account(req);
2916        let mut guard = self.state.write();
2917        let st = guard.get_or_create(&account);
2918        if !st.templates.contains_key(&tmpl) {
2919            return Err(template_not_found());
2920        }
2921        let mut disassociated = Vec::new();
2922        let mut errors = Vec::new();
2923        for n in &names {
2924            let Some(rn) = n.as_str() else { continue };
2925            match st.repositories.get_mut(rn) {
2926                Some(repo) => {
2927                    repo.associated_templates.retain(|t| t != &tmpl);
2928                    disassociated.push(json!(rn));
2929                }
2930                None => errors.push(json!({
2931                    "repositoryName": rn,
2932                    "errorCode": "RepositoryDoesNotExist",
2933                    "errorMessage": format!("The repository {rn} does not exist."),
2934                })),
2935            }
2936        }
2937        ok(json!({ "disassociatedRepositoryNames": disassociated, "errors": errors }))
2938    }
2939
2940    fn list_associated_templates_for_repository(
2941        &self,
2942        req: &AwsRequest,
2943    ) -> Result<AwsResponse, AwsServiceError> {
2944        let b = body(req);
2945        let name = require_repository_name(&b)?;
2946        let account = self.account(req);
2947        let guard = self.state.read();
2948        let st = guard.get(&account);
2949        let repo = st
2950            .and_then(|s| s.repositories.get(&name))
2951            .ok_or_else(|| repo_not_found(&name))?;
2952        let names: Vec<Value> = repo.associated_templates.iter().map(|t| json!(t)).collect();
2953        ok(json!({ "approvalRuleTemplateNames": names }))
2954    }
2955
2956    fn list_repositories_for_template(
2957        &self,
2958        req: &AwsRequest,
2959    ) -> Result<AwsResponse, AwsServiceError> {
2960        let b = body(req);
2961        let tmpl = require(
2962            &b,
2963            "approvalRuleTemplateName",
2964            "ApprovalRuleTemplateNameRequiredException",
2965        )?;
2966        let account = self.account(req);
2967        let guard = self.state.read();
2968        let st = guard.get(&account);
2969        let s = st.ok_or_else(template_not_found)?;
2970        if !s.templates.contains_key(&tmpl) {
2971            return Err(template_not_found());
2972        }
2973        let names: Vec<Value> = s
2974            .repository_order
2975            .iter()
2976            .filter(|n| {
2977                s.repositories
2978                    .get(*n)
2979                    .map(|r| r.associated_templates.contains(&tmpl))
2980                    .unwrap_or(false)
2981            })
2982            .map(|n| json!(n))
2983            .collect();
2984        ok(json!({ "repositoryNames": names }))
2985    }
2986
2987    // ----- Comments -----
2988
2989    fn post_comment_for_compared_commit(
2990        &self,
2991        req: &AwsRequest,
2992    ) -> Result<AwsResponse, AwsServiceError> {
2993        let b = body(req);
2994        let name = require_repository_name(&b)?;
2995        let after = require(&b, "afterCommitId", "CommitIdRequiredException")?;
2996        let content = require(&b, "content", "CommentContentRequiredException")?;
2997        if content.chars().count() > 10240 {
2998            return Err(err(
2999                "CommentContentSizeLimitExceededException",
3000                "The comment is too large. Comments are limited to 10,240 characters.",
3001            ));
3002        }
3003        let before = str_field(&b, "beforeCommitId");
3004        if before.as_deref() == Some(after.as_str()) {
3005            return Err(err(
3006                "BeforeCommitIdAndAfterCommitIdAreSameException",
3007                "The before commit ID and the after commit ID are the same.",
3008            ));
3009        }
3010        let account = self.account(req);
3011        let mut guard = self.state.write();
3012        let st = guard.get_or_create(&account);
3013        if !st.repositories.contains_key(&name) {
3014            return Err(repo_not_found(&name));
3015        }
3016        let now = Utc::now();
3017        let author = caller_arn(req);
3018        let comment_id = new_uuid();
3019        let location = b.get("location").cloned();
3020        let mut stored = comment_value(&comment_id, &content, &author, now, None);
3021        if let Some(obj) = stored.as_object_mut() {
3022            obj.insert("_ctxRepositoryName".into(), json!(name));
3023            obj.insert("_ctxAfterCommitId".into(), json!(after));
3024            if let Some(bc) = &before {
3025                obj.insert("_ctxBeforeCommitId".into(), json!(bc));
3026            }
3027            if let Some(loc) = &location {
3028                obj.insert("_ctxLocation".into(), loc.clone());
3029            }
3030        }
3031        st.comments.insert(comment_id.clone(), stored.clone());
3032        st.comment_order.push(comment_id);
3033        let public = public_comment(&stored);
3034        let mut out = Map::new();
3035        out.insert("repositoryName".into(), json!(name));
3036        if let Some(bc) = before {
3037            out.insert("beforeCommitId".into(), json!(bc));
3038        }
3039        out.insert("afterCommitId".into(), json!(after));
3040        if let Some(loc) = location {
3041            out.insert("location".into(), loc);
3042        }
3043        out.insert("comment".into(), public);
3044        ok(Value::Object(out))
3045    }
3046
3047    fn post_comment_for_pull_request(
3048        &self,
3049        req: &AwsRequest,
3050    ) -> Result<AwsResponse, AwsServiceError> {
3051        let b = body(req);
3052        let pr_id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
3053        let name = require_repository_name(&b)?;
3054        let before = require(&b, "beforeCommitId", "CommitIdRequiredException")?;
3055        let after = require(&b, "afterCommitId", "CommitIdRequiredException")?;
3056        let content = require(&b, "content", "CommentContentRequiredException")?;
3057        if content.chars().count() > 10240 {
3058            return Err(err(
3059                "CommentContentSizeLimitExceededException",
3060                "The comment is too large.",
3061            ));
3062        }
3063        let account = self.account(req);
3064        let mut guard = self.state.write();
3065        let st = guard.get_or_create(&account);
3066        if !st.repositories.contains_key(&name) {
3067            return Err(repo_not_found(&name));
3068        }
3069        if !st.pull_requests.contains_key(&pr_id) {
3070            return Err(pr_not_found());
3071        }
3072        let now = Utc::now();
3073        let author = caller_arn(req);
3074        let comment_id = new_uuid();
3075        let location = b.get("location").cloned();
3076        let mut stored = comment_value(&comment_id, &content, &author, now, None);
3077        if let Some(obj) = stored.as_object_mut() {
3078            obj.insert("_ctxRepositoryName".into(), json!(name));
3079            obj.insert("_ctxPullRequestId".into(), json!(pr_id));
3080            obj.insert("_ctxBeforeCommitId".into(), json!(before));
3081            obj.insert("_ctxAfterCommitId".into(), json!(after));
3082            if let Some(loc) = &location {
3083                obj.insert("_ctxLocation".into(), loc.clone());
3084            }
3085        }
3086        st.comments.insert(comment_id.clone(), stored.clone());
3087        st.comment_order.push(comment_id);
3088        let public = public_comment(&stored);
3089        let mut out = Map::new();
3090        out.insert("repositoryName".into(), json!(name));
3091        out.insert("pullRequestId".into(), json!(pr_id));
3092        out.insert("beforeCommitId".into(), json!(before));
3093        out.insert("afterCommitId".into(), json!(after));
3094        if let Some(loc) = location {
3095            out.insert("location".into(), loc);
3096        }
3097        out.insert("comment".into(), public);
3098        ok(Value::Object(out))
3099    }
3100
3101    fn post_comment_reply(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3102        let b = body(req);
3103        let in_reply_to = require(&b, "inReplyTo", "CommentIdRequiredException")?;
3104        let content = require(&b, "content", "CommentContentRequiredException")?;
3105        if content.chars().count() > 10240 {
3106            return Err(err(
3107                "CommentContentSizeLimitExceededException",
3108                "The comment is too large.",
3109            ));
3110        }
3111        let account = self.account(req);
3112        let mut guard = self.state.write();
3113        let st = guard.get_or_create(&account);
3114        let parent = st.comments.get(&in_reply_to).cloned().ok_or_else(|| {
3115            err(
3116                "CommentDoesNotExistException",
3117                "The specified comment does not exist.",
3118            )
3119        })?;
3120        let now = Utc::now();
3121        let author = caller_arn(req);
3122        let comment_id = new_uuid();
3123        let mut stored = comment_value(&comment_id, &content, &author, now, Some(&in_reply_to));
3124        // Inherit the parent's thread context.
3125        if let (Some(obj), Some(pobj)) = (stored.as_object_mut(), parent.as_object()) {
3126            for (k, v) in pobj {
3127                if k.starts_with("_ctx") {
3128                    obj.insert(k.clone(), v.clone());
3129                }
3130            }
3131        }
3132        st.comments.insert(comment_id.clone(), stored.clone());
3133        st.comment_order.push(comment_id);
3134        ok(json!({ "comment": public_comment(&stored) }))
3135    }
3136
3137    fn get_comment(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3138        let b = body(req);
3139        let id = require(&b, "commentId", "CommentIdRequiredException")?;
3140        let account = self.account(req);
3141        let guard = self.state.read();
3142        let st = guard.get(&account);
3143        let comment = st.and_then(|s| s.comments.get(&id)).ok_or_else(|| {
3144            err(
3145                "CommentDoesNotExistException",
3146                "The specified comment does not exist.",
3147            )
3148        })?;
3149        if comment.get("deleted").and_then(Value::as_bool) == Some(true) {
3150            return Err(err(
3151                "CommentDeletedException",
3152                "This comment has already been deleted.",
3153            ));
3154        }
3155        ok(json!({ "comment": public_comment(comment) }))
3156    }
3157
3158    fn get_comments_for_compared_commit(
3159        &self,
3160        req: &AwsRequest,
3161    ) -> Result<AwsResponse, AwsServiceError> {
3162        let b = body(req);
3163        let name = require_repository_name(&b)?;
3164        let after = require(&b, "afterCommitId", "CommitIdRequiredException")?;
3165        let before = str_field(&b, "beforeCommitId");
3166        let account = self.account(req);
3167        let guard = self.state.read();
3168        let st = guard.get(&account);
3169        let s = st.ok_or_else(|| repo_not_found(&name))?;
3170        if !s.repositories.contains_key(&name) {
3171            return Err(repo_not_found(&name));
3172        }
3173        let mut threads: Vec<Value> = Vec::new();
3174        for cid in &s.comment_order {
3175            let Some(c) = s.comments.get(cid) else {
3176                continue;
3177            };
3178            if c.get("_ctxPullRequestId").is_some() {
3179                continue;
3180            }
3181            if c.get("_ctxRepositoryName").and_then(Value::as_str) != Some(name.as_str()) {
3182                continue;
3183            }
3184            if c.get("_ctxAfterCommitId").and_then(Value::as_str) != Some(after.as_str()) {
3185                continue;
3186            }
3187            if let Some(bc) = &before {
3188                if c.get("_ctxBeforeCommitId").and_then(Value::as_str) != Some(bc.as_str()) {
3189                    continue;
3190                }
3191            }
3192            threads.push(json!({
3193                "repositoryName": name,
3194                "beforeCommitId": before,
3195                "afterCommitId": after,
3196                "location": c.get("_ctxLocation").cloned().unwrap_or(Value::Null),
3197                "comments": [public_comment(c)],
3198            }));
3199        }
3200        ok(json!({ "commentsForComparedCommitData": threads }))
3201    }
3202
3203    fn get_comments_for_pull_request(
3204        &self,
3205        req: &AwsRequest,
3206    ) -> Result<AwsResponse, AwsServiceError> {
3207        let b = body(req);
3208        let pr_id = require(&b, "pullRequestId", "PullRequestIdRequiredException")?;
3209        let account = self.account(req);
3210        let guard = self.state.read();
3211        let st = guard.get(&account);
3212        let s = st.ok_or_else(pr_not_found)?;
3213        if !s.pull_requests.contains_key(&pr_id) {
3214            return Err(pr_not_found());
3215        }
3216        let mut threads: Vec<Value> = Vec::new();
3217        for cid in &s.comment_order {
3218            let Some(c) = s.comments.get(cid) else {
3219                continue;
3220            };
3221            if c.get("_ctxPullRequestId").and_then(Value::as_str) != Some(pr_id.as_str()) {
3222                continue;
3223            }
3224            threads.push(json!({
3225                "pullRequestId": pr_id,
3226                "repositoryName": c.get("_ctxRepositoryName").cloned().unwrap_or(Value::Null),
3227                "beforeCommitId": c.get("_ctxBeforeCommitId").cloned().unwrap_or(Value::Null),
3228                "afterCommitId": c.get("_ctxAfterCommitId").cloned().unwrap_or(Value::Null),
3229                "location": c.get("_ctxLocation").cloned().unwrap_or(Value::Null),
3230                "comments": [public_comment(c)],
3231            }));
3232        }
3233        ok(json!({ "commentsForPullRequestData": threads }))
3234    }
3235
3236    fn update_comment(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3237        let b = body(req);
3238        let id = require(&b, "commentId", "CommentIdRequiredException")?;
3239        let content = require(&b, "content", "CommentContentRequiredException")?;
3240        if content.chars().count() > 10240 {
3241            return Err(err(
3242                "CommentContentSizeLimitExceededException",
3243                "The comment is too large.",
3244            ));
3245        }
3246        let caller = caller_arn(req);
3247        let account = self.account(req);
3248        let mut guard = self.state.write();
3249        let st = guard.get_or_create(&account);
3250        let comment = st.comments.get_mut(&id).ok_or_else(|| {
3251            err(
3252                "CommentDoesNotExistException",
3253                "The specified comment does not exist.",
3254            )
3255        })?;
3256        if comment.get("deleted").and_then(Value::as_bool) == Some(true) {
3257            return Err(err(
3258                "CommentDeletedException",
3259                "This comment has already been deleted.",
3260            ));
3261        }
3262        if comment.get("authorArn").and_then(Value::as_str) != Some(caller.as_str()) {
3263            return Err(err(
3264                "CommentNotCreatedByCallerException",
3265                "You cannot modify a comment that was not created by you.",
3266            ));
3267        }
3268        comment["content"] = json!(content);
3269        comment["lastModifiedDate"] = ts(Utc::now());
3270        ok(json!({ "comment": public_comment(comment) }))
3271    }
3272
3273    fn delete_comment_content(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3274        let b = body(req);
3275        let id = require(&b, "commentId", "CommentIdRequiredException")?;
3276        let account = self.account(req);
3277        let mut guard = self.state.write();
3278        let st = guard.get_or_create(&account);
3279        let comment = st.comments.get_mut(&id).ok_or_else(|| {
3280            err(
3281                "CommentDoesNotExistException",
3282                "The specified comment does not exist.",
3283            )
3284        })?;
3285        comment["deleted"] = json!(true);
3286        comment["content"] = json!("");
3287        comment["lastModifiedDate"] = ts(Utc::now());
3288        ok(json!({ "comment": public_comment(comment) }))
3289    }
3290
3291    fn put_comment_reaction(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3292        let b = body(req);
3293        let id = require(&b, "commentId", "CommentIdRequiredException")?;
3294        let reaction = require(&b, "reactionValue", "ReactionValueRequiredException")?;
3295        if !validate::REACTION_EMOJIS.contains(&reaction.as_str())
3296            && !reaction.starts_with(":")
3297            && !reaction.starts_with("\\u")
3298        {
3299            return Err(err(
3300                "InvalidReactionValueException",
3301                "The reaction value is not valid.",
3302            ));
3303        }
3304        let user = caller_arn(req);
3305        let account = self.account(req);
3306        let mut guard = self.state.write();
3307        let st = guard.get_or_create(&account);
3308        let comment = st.comments.get_mut(&id).ok_or_else(|| {
3309            err(
3310                "CommentDoesNotExistException",
3311                "The specified comment does not exist.",
3312            )
3313        })?;
3314        if comment.get("deleted").and_then(Value::as_bool) == Some(true) {
3315            return Err(err(
3316                "CommentDeletedException",
3317                "This comment has already been deleted.",
3318            ));
3319        }
3320        let reactions = comment
3321            .as_object_mut()
3322            .unwrap()
3323            .entry("_ctxReactions".to_string())
3324            .or_insert_with(|| json!({}));
3325        if let Some(obj) = reactions.as_object_mut() {
3326            let users = obj.entry(reaction).or_insert_with(|| json!([]));
3327            if let Some(arr) = users.as_array_mut() {
3328                if !arr.iter().any(|u| u.as_str() == Some(user.as_str())) {
3329                    arr.push(json!(user));
3330                }
3331            }
3332        }
3333        ok(json!({}))
3334    }
3335
3336    fn get_comment_reactions(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3337        let b = body(req);
3338        let id = require(&b, "commentId", "CommentIdRequiredException")?;
3339        let account = self.account(req);
3340        let guard = self.state.read();
3341        let st = guard.get(&account);
3342        let comment = st.and_then(|s| s.comments.get(&id)).ok_or_else(|| {
3343            err(
3344                "CommentDoesNotExistException",
3345                "The specified comment does not exist.",
3346            )
3347        })?;
3348        let mut reactions = Vec::new();
3349        if let Some(obj) = comment.get("_ctxReactions").and_then(Value::as_object) {
3350            for (emoji, users) in obj {
3351                let user_list: Vec<&str> = users
3352                    .as_array()
3353                    .map(|a| a.iter().filter_map(Value::as_str).collect())
3354                    .unwrap_or_default();
3355                reactions.push(json!({
3356                    "reaction": {
3357                        "emoji": emoji,
3358                        "shortCode": emoji,
3359                        "unicode": "",
3360                    },
3361                    "reactionUsers": user_list,
3362                    "reactionsFromDeletedUsersCount": 0,
3363                }));
3364            }
3365        }
3366        ok(json!({ "reactionsForComment": reactions }))
3367    }
3368
3369    // ----- Triggers -----
3370
3371    fn put_repository_triggers(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3372        let b = body(req);
3373        let name = require_repository_name(&b)?;
3374        let triggers = b
3375            .get("triggers")
3376            .and_then(Value::as_array)
3377            .ok_or_else(|| {
3378                err(
3379                    "RepositoryTriggersListRequiredException",
3380                    "The list of triggers for the repository is required.",
3381                )
3382            })?
3383            .clone();
3384        for t in &triggers {
3385            let tname = t.get("name").and_then(Value::as_str);
3386            if tname.map(str::is_empty).unwrap_or(true) {
3387                return Err(err(
3388                    "RepositoryTriggerNameRequiredException",
3389                    "A name for the trigger is required.",
3390                ));
3391            }
3392            let dest = t.get("destinationArn").and_then(Value::as_str);
3393            if dest.map(str::is_empty).unwrap_or(true) {
3394                return Err(err(
3395                    "RepositoryTriggerDestinationArnRequiredException",
3396                    "A destination ARN for the target service for the trigger is required.",
3397                ));
3398            }
3399            let events = t.get("events").and_then(Value::as_array);
3400            match events {
3401                None => {
3402                    return Err(err(
3403                        "RepositoryTriggerEventsListRequiredException",
3404                        "At least one event for the trigger is required.",
3405                    ))
3406                }
3407                Some(evs) => {
3408                    for e in evs {
3409                        if let Some(ev) = e.as_str() {
3410                            if !validate::is_enum(validate::REPOSITORY_TRIGGER_EVENT, ev) {
3411                                return Err(err(
3412                                    "InvalidRepositoryTriggerEventsException",
3413                                    "One or more of the trigger events is not valid.",
3414                                ));
3415                            }
3416                        }
3417                    }
3418                }
3419            }
3420        }
3421        let account = self.account(req);
3422        let mut guard = self.state.write();
3423        let st = guard.get_or_create(&account);
3424        let repo = st
3425            .repositories
3426            .get_mut(&name)
3427            .ok_or_else(|| repo_not_found(&name))?;
3428        let config_id = new_uuid();
3429        repo.triggers = triggers;
3430        repo.triggers_config_id = config_id.clone();
3431        ok(json!({ "configurationId": config_id }))
3432    }
3433
3434    fn get_repository_triggers(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3435        let b = body(req);
3436        let name = require_repository_name(&b)?;
3437        let account = self.account(req);
3438        let guard = self.state.read();
3439        let st = guard.get(&account);
3440        let repo = st
3441            .and_then(|s| s.repositories.get(&name))
3442            .ok_or_else(|| repo_not_found(&name))?;
3443        let config_id = if repo.triggers_config_id.is_empty() {
3444            new_uuid()
3445        } else {
3446            repo.triggers_config_id.clone()
3447        };
3448        ok(json!({
3449            "configurationId": config_id,
3450            "triggers": repo.triggers,
3451        }))
3452    }
3453
3454    fn test_repository_triggers(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3455        let b = body(req);
3456        let name = require_repository_name(&b)?;
3457        let triggers = b
3458            .get("triggers")
3459            .and_then(Value::as_array)
3460            .ok_or_else(|| {
3461                err(
3462                    "RepositoryTriggersListRequiredException",
3463                    "The list of triggers is required.",
3464                )
3465            })?
3466            .clone();
3467        let account = self.account(req);
3468        let guard = self.state.read();
3469        let st = guard.get(&account);
3470        let repo = st
3471            .and_then(|s| s.repositories.get(&name))
3472            .ok_or_else(|| repo_not_found(&name))?;
3473        // Enforce the same structural requirements as PutRepositoryTriggers.
3474        for t in &triggers {
3475            if t.get("name")
3476                .and_then(Value::as_str)
3477                .map(str::is_empty)
3478                .unwrap_or(true)
3479            {
3480                return Err(err(
3481                    "RepositoryTriggerNameRequiredException",
3482                    "A name for the trigger is required.",
3483                ));
3484            }
3485            if t.get("destinationArn")
3486                .and_then(Value::as_str)
3487                .map(str::is_empty)
3488                .unwrap_or(true)
3489            {
3490                return Err(err(
3491                    "RepositoryTriggerDestinationArnRequiredException",
3492                    "A destination ARN for the target service for the trigger is required.",
3493                ));
3494            }
3495            if t.get("events")
3496                .and_then(Value::as_array)
3497                .map(|e| e.is_empty())
3498                .unwrap_or(true)
3499            {
3500                return Err(err(
3501                    "RepositoryTriggerEventsListRequiredException",
3502                    "At least one event for the trigger is required.",
3503                ));
3504            }
3505        }
3506        // A test "executes" each trigger: its destination must resolve to a
3507        // supported delivery target (SNS topic / Lambda function in this account
3508        // and region), its events must be valid, and any branch it is scoped to
3509        // must exist. Triggers that cannot be delivered are reported as failures
3510        // with a real message rather than a blanket success.
3511        let mut successful = Vec::new();
3512        let mut failed = Vec::new();
3513        for t in &triggers {
3514            let tname = t.get("name").and_then(Value::as_str).unwrap_or_default();
3515            let dest = t
3516                .get("destinationArn")
3517                .and_then(Value::as_str)
3518                .unwrap_or_default();
3519            let mut failure: Option<String> = None;
3520            if !trigger_destination_ok(dest, &account, &req.region) {
3521                failure = Some(format!(
3522                    "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 ({}).",
3523                    req.region
3524                ));
3525            }
3526            if failure.is_none() {
3527                if let Some(evs) = t.get("events").and_then(Value::as_array) {
3528                    for e in evs {
3529                        if let Some(ev) = e.as_str() {
3530                            if !validate::is_enum(validate::REPOSITORY_TRIGGER_EVENT, ev) {
3531                                failure = Some(format!("The trigger event {ev} is not valid."));
3532                                break;
3533                            }
3534                        }
3535                    }
3536                }
3537            }
3538            if failure.is_none() {
3539                if let Some(branches) = t.get("branches").and_then(Value::as_array) {
3540                    for br in branches {
3541                        if let Some(bn) = br.as_str() {
3542                            if !repo.branches.contains_key(bn) {
3543                                failure = Some(format!(
3544                                    "The branch {bn} specified for the trigger does not exist in the repository."
3545                                ));
3546                                break;
3547                            }
3548                        }
3549                    }
3550                }
3551            }
3552            match failure {
3553                Some(msg) => {
3554                    failed.push(json!({ "trigger": tname, "failureMessage": msg }));
3555                }
3556                None => successful.push(json!(tname)),
3557            }
3558        }
3559        ok(json!({
3560            "successfulExecutions": successful,
3561            "failedExecutions": failed,
3562        }))
3563    }
3564
3565    // ----- Tagging -----
3566
3567    fn tag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3568        let b = body(req);
3569        let arn = require(&b, "resourceArn", "ResourceArnRequiredException")?;
3570        if !is_codecommit_arn(&arn) {
3571            return Err(err(
3572                "InvalidResourceArnException",
3573                "The resource ARN is not valid.",
3574            ));
3575        }
3576        let tags = b
3577            .get("tags")
3578            .and_then(Value::as_object)
3579            .ok_or_else(|| err("TagsMapRequiredException", "A map of tags is required."))?;
3580        let account = self.account(req);
3581        let mut guard = self.state.write();
3582        let st = guard.get_or_create(&account);
3583        if let Some(rn) = repo_name_from_arn(&arn) {
3584            if !st.repositories.contains_key(&rn) {
3585                return Err(repo_not_found(&rn));
3586            }
3587        }
3588        let entry = st.tags.entry(arn).or_default();
3589        for (k, v) in tags {
3590            if let Some(v) = v.as_str() {
3591                entry.insert(k.clone(), v.to_string());
3592            }
3593        }
3594        ok(json!({}))
3595    }
3596
3597    fn untag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3598        let b = body(req);
3599        let arn = require(&b, "resourceArn", "ResourceArnRequiredException")?;
3600        if !is_codecommit_arn(&arn) {
3601            return Err(err(
3602                "InvalidResourceArnException",
3603                "The resource ARN is not valid.",
3604            ));
3605        }
3606        let keys = b
3607            .get("tagKeys")
3608            .and_then(Value::as_array)
3609            .ok_or_else(|| {
3610                err(
3611                    "TagKeysListRequiredException",
3612                    "A list of tag keys is required.",
3613                )
3614            })?
3615            .clone();
3616        let account = self.account(req);
3617        let mut guard = self.state.write();
3618        let st = guard.get_or_create(&account);
3619        if let Some(rn) = repo_name_from_arn(&arn) {
3620            if !st.repositories.contains_key(&rn) {
3621                return Err(repo_not_found(&rn));
3622            }
3623        }
3624        if let Some(entry) = st.tags.get_mut(&arn) {
3625            for k in &keys {
3626                if let Some(k) = k.as_str() {
3627                    entry.remove(k);
3628                }
3629            }
3630        }
3631        ok(json!({}))
3632    }
3633
3634    fn list_tags_for_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3635        let b = body(req);
3636        let arn = require(&b, "resourceArn", "ResourceArnRequiredException")?;
3637        if !is_codecommit_arn(&arn) {
3638            return Err(err(
3639                "InvalidResourceArnException",
3640                "The resource ARN is not valid.",
3641            ));
3642        }
3643        let account = self.account(req);
3644        let guard = self.state.read();
3645        let st = guard.get(&account);
3646        if let Some(rn) = repo_name_from_arn(&arn) {
3647            if !st
3648                .map(|s| s.repositories.contains_key(&rn))
3649                .unwrap_or(false)
3650            {
3651                return Err(repo_not_found(&rn));
3652            }
3653        }
3654        let tags = st
3655            .and_then(|s| s.tags.get(&arn))
3656            .map(|m| {
3657                let mut obj = Map::new();
3658                for (k, v) in m {
3659                    obj.insert(k.clone(), json!(v));
3660                }
3661                Value::Object(obj)
3662            })
3663            .unwrap_or_else(|| json!({}));
3664        ok(json!({ "tags": tags }))
3665    }
3666
3667    // ----- Shared mutation helpers -----
3668
3669    fn mutate_pr(
3670        &self,
3671        req: &AwsRequest,
3672        id: &str,
3673        f: impl FnOnce(&mut Value) -> Result<(), AwsServiceError>,
3674    ) -> Result<AwsResponse, AwsServiceError> {
3675        let account = self.account(req);
3676        let mut guard = self.state.write();
3677        let st = guard.get_or_create(&account);
3678        let pr = st.pull_requests.get_mut(id).ok_or_else(pr_not_found)?;
3679        f(pr)?;
3680        let pr = pr.clone();
3681        ok(json!({ "pullRequest": pr }))
3682    }
3683
3684    fn mutate_template(
3685        &self,
3686        req: &AwsRequest,
3687        name: &str,
3688        f: impl FnOnce(&mut Value) -> Result<(), AwsServiceError>,
3689    ) -> Result<AwsResponse, AwsServiceError> {
3690        let account = self.account(req);
3691        let mut guard = self.state.write();
3692        let st = guard.get_or_create(&account);
3693        let t = st.templates.get_mut(name).ok_or_else(template_not_found)?;
3694        f(t)?;
3695        let t = t.clone();
3696        ok(json!({ "approvalRuleTemplate": t }))
3697    }
3698}
3699
3700// ---------------------------------------------------------------------------
3701// Error constructors for the service-wide "does not exist" responses.
3702// ---------------------------------------------------------------------------
3703
3704#[cfg(test)]
3705mod normalize_tests {
3706    use crate::codecommit_helpers::normalize_rule_content;
3707
3708    #[test]
3709    fn compacts_and_preserves_member_order() {
3710        let input = "  {\n\t  \"Version\": \"2018-11-08\",\n\t  \"DestinationReferences\": [\"refs/heads/master\"],\n\t  \"Statements\": []\n  }\n";
3711        let out = normalize_rule_content(input);
3712        assert_eq!(
3713            out,
3714            "{\"Version\":\"2018-11-08\",\"DestinationReferences\":[\"refs/heads/master\"],\"Statements\":[]}"
3715        );
3716    }
3717}
3718
3719#[cfg(test)]
3720mod engine_tests {
3721    use super::*;
3722
3723    fn entry(id: &str) -> FileEntry {
3724        FileEntry {
3725            blob_id: id.to_string(),
3726            mode: "100644".to_string(),
3727        }
3728    }
3729
3730    fn commit(parents: &[&str]) -> Value {
3731        json!({ "parents": parents, "treeId": "t" })
3732    }
3733
3734    // Defect 3: merge_base is the lowest common ancestor, not any ancestor.
3735    #[test]
3736    fn merge_base_is_lowest_common_ancestor() {
3737        // root <- X <- A ; and B is a merge commit whose parents are [X, root].
3738        // The common ancestors of A and B are {X, root}; the LCA is X. A
3739        // depth-first walk that returns the first common ancestor would wrongly
3740        // report root.
3741        let mut repo = Repo::default();
3742        repo.commits.insert("root".into(), commit(&[]));
3743        repo.commits.insert("X".into(), commit(&["root"]));
3744        repo.commits.insert("A".into(), commit(&["X"]));
3745        repo.commits.insert("B".into(), commit(&["X", "root"]));
3746        assert_eq!(merge_base(&repo, "A", "B"), Some("X".to_string()));
3747        assert_eq!(merge_base(&repo, "B", "A"), Some("X".to_string()));
3748    }
3749
3750    #[test]
3751    fn merge_base_of_a_diamond_is_the_fork_point() {
3752        // root <- L <- A ; root <- R <- B. LCA(A, B) == root.
3753        let mut repo = Repo::default();
3754        repo.commits.insert("root".into(), commit(&[]));
3755        repo.commits.insert("L".into(), commit(&["root"]));
3756        repo.commits.insert("R".into(), commit(&["root"]));
3757        repo.commits.insert("A".into(), commit(&["L"]));
3758        repo.commits.insert("B".into(), commit(&["R"]));
3759        assert_eq!(merge_base(&repo, "A", "B"), Some("root".to_string()));
3760    }
3761
3762    // Defect 2: a file deleted on the source branch is removed by the merge.
3763    #[test]
3764    fn three_way_merge_propagates_source_deletion() {
3765        let mut repo = Repo::default();
3766        repo.commits.insert("base".into(), commit(&[]));
3767        repo.commits.insert("src".into(), commit(&["base"]));
3768        repo.commits.insert("dst".into(), commit(&["base"]));
3769        // base has a.txt + b.txt.
3770        let mut base = Tree::new();
3771        base.insert("a.txt".into(), entry("a1"));
3772        base.insert("b.txt".into(), entry("b1"));
3773        repo.trees.insert("base".into(), base);
3774        // source deletes b.txt.
3775        let mut src = Tree::new();
3776        src.insert("a.txt".into(), entry("a1"));
3777        repo.trees.insert("src".into(), src);
3778        // destination is unchanged w.r.t. b.txt but adds c.txt.
3779        let mut dst = Tree::new();
3780        dst.insert("a.txt".into(), entry("a1"));
3781        dst.insert("b.txt".into(), entry("b1"));
3782        dst.insert("c.txt".into(), entry("c1"));
3783        repo.trees.insert("dst".into(), dst);
3784
3785        let merged = merge_trees(&repo, "src", "dst", "").expect("mergeable");
3786        assert!(!merged.contains_key("b.txt"), "source deletion must win");
3787        assert!(merged.contains_key("a.txt"));
3788        assert!(merged.contains_key("c.txt"));
3789    }
3790
3791    #[test]
3792    fn three_way_merge_flags_true_conflict() {
3793        let mut repo = Repo::default();
3794        repo.commits.insert("base".into(), commit(&[]));
3795        repo.commits.insert("src".into(), commit(&["base"]));
3796        repo.commits.insert("dst".into(), commit(&["base"]));
3797        let mut base = Tree::new();
3798        base.insert("a.txt".into(), entry("a1"));
3799        repo.trees.insert("base".into(), base);
3800        let mut src = Tree::new();
3801        src.insert("a.txt".into(), entry("a-src"));
3802        repo.trees.insert("src".into(), src);
3803        let mut dst = Tree::new();
3804        dst.insert("a.txt".into(), entry("a-dst"));
3805        repo.trees.insert("dst".into(), dst);
3806        // Both sides changed a.txt differently -> unresolved conflict.
3807        assert!(merge_trees(&repo, "src", "dst", "").is_none());
3808        let empty = Tree::new();
3809        let (_, bt, st, dt) = merge_inputs(&repo, "src", "dst", &empty);
3810        assert_eq!(conflicting_paths_in(bt, st, dt), vec!["a.txt"]);
3811        // A strategy resolves it deterministically.
3812        let merged = merge_trees(&repo, "src", "dst", "ACCEPT_SOURCE").expect("resolved");
3813        assert_eq!(merged.get("a.txt").unwrap().blob_id, "a-src");
3814    }
3815}
3816
3817#[cfg(test)]
3818mod handler_tests {
3819    use super::*;
3820    use bytes::Bytes;
3821    use fakecloud_core::auth::{Principal, PrincipalType};
3822    use fakecloud_core::multi_account::MultiAccountState;
3823    use http::{HeaderMap, Method};
3824    use parking_lot::RwLock;
3825    use std::collections::HashMap;
3826
3827    fn svc() -> CodeCommitService {
3828        CodeCommitService::new(Arc::new(RwLock::new(MultiAccountState::new(
3829            "000000000000",
3830            "us-east-1",
3831            "",
3832        ))))
3833    }
3834
3835    fn req_as(action: &str, body: Value, arn: Option<&str>) -> AwsRequest {
3836        let principal = arn.map(|a| Principal {
3837            arn: a.to_string(),
3838            user_id: "AIDATEST".to_string(),
3839            account_id: "000000000000".to_string(),
3840            principal_type: PrincipalType::User,
3841            source_identity: None,
3842            tags: None,
3843        });
3844        AwsRequest {
3845            service: "codecommit".into(),
3846            action: action.into(),
3847            region: "us-east-1".into(),
3848            account_id: "000000000000".into(),
3849            request_id: "req".into(),
3850            headers: HeaderMap::new(),
3851            query_params: HashMap::new(),
3852            body: Bytes::from(serde_json::to_vec(&body).unwrap()),
3853            body_stream: parking_lot::Mutex::new(None),
3854            path_segments: vec![],
3855            raw_path: String::new(),
3856            raw_query: String::new(),
3857            method: Method::POST,
3858            is_query_protocol: false,
3859            access_key_id: None,
3860            principal,
3861        }
3862    }
3863
3864    fn call(s: &CodeCommitService, action: &str, body: Value) -> Value {
3865        let resp = s
3866            .dispatch(action, &req_as(action, body, None))
3867            .expect("op ok");
3868        serde_json::from_slice(resp.body.expect_bytes()).unwrap()
3869    }
3870
3871    fn call_as(s: &CodeCommitService, action: &str, body: Value, arn: &str) -> Value {
3872        let resp = s
3873            .dispatch(action, &req_as(action, body, Some(arn)))
3874            .expect("op ok");
3875        serde_json::from_slice(resp.body.expect_bytes()).unwrap()
3876    }
3877
3878    fn call_err(s: &CodeCommitService, action: &str, body: Value) -> AwsServiceError {
3879        s.dispatch(action, &req_as(action, body, None))
3880            .err()
3881            .expect("op err")
3882    }
3883
3884    fn b64(s: &str) -> String {
3885        base64::engine::general_purpose::STANDARD.encode(s.as_bytes())
3886    }
3887
3888    /// Create a repository and return nothing; the caller uses "repo".
3889    fn make_repo(s: &CodeCommitService) {
3890        call(s, "CreateRepository", json!({ "repositoryName": "repo" }));
3891    }
3892
3893    /// Put a file on a branch, returning the new commit id.
3894    fn put(
3895        s: &CodeCommitService,
3896        branch: &str,
3897        path: &str,
3898        content: &str,
3899        parent: Option<&str>,
3900    ) -> String {
3901        let mut body = json!({
3902            "repositoryName": "repo",
3903            "branchName": branch,
3904            "filePath": path,
3905            "fileContent": b64(content),
3906        });
3907        if let Some(p) = parent {
3908            body["parentCommitId"] = json!(p);
3909        }
3910        call(s, "PutFile", body)["commitId"]
3911            .as_str()
3912            .unwrap()
3913            .to_string()
3914    }
3915
3916    /// A repo with `main` and `feature` diverging non-conflictingly from a
3917    /// shared base commit. Returns (source_tip, dest_tip).
3918    fn diverged_repo(s: &CodeCommitService) -> (String, String) {
3919        make_repo(s);
3920        let c1 = put(s, "main", "a.txt", "base", None);
3921        call(
3922            s,
3923            "CreateBranch",
3924            json!({ "repositoryName": "repo", "branchName": "feature", "commitId": c1 }),
3925        );
3926        let feature_tip = put(s, "feature", "b.txt", "from-feature", Some(&c1));
3927        let main_tip = put(s, "main", "c.txt", "from-main", Some(&c1));
3928        (feature_tip, main_tip)
3929    }
3930
3931    fn create_pr(s: &CodeCommitService, dest: &str) -> Value {
3932        call(
3933            s,
3934            "CreatePullRequest",
3935            json!({
3936                "title": "PR",
3937                "targets": [{
3938                    "repositoryName": "repo",
3939                    "sourceReference": "feature",
3940                    "destinationReference": dest,
3941                }],
3942            }),
3943        )["pullRequest"]
3944            .clone()
3945    }
3946
3947    // Defect 1: MergePullRequestByThreeWay advances the destination branch and
3948    // the returned mergeCommitId is a real, retrievable commit.
3949    #[test]
3950    fn merge_pr_three_way_advances_branch_and_writes_commit() {
3951        let s = svc();
3952        diverged_repo(&s);
3953        let pr = create_pr(&s, "main");
3954        let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
3955
3956        let out = call(
3957            &s,
3958            "MergePullRequestByThreeWay",
3959            json!({ "pullRequestId": pr_id, "repositoryName": "repo" }),
3960        );
3961        let target = &out["pullRequest"]["pullRequestTargets"][0];
3962        assert_eq!(target["mergeMetadata"]["isMerged"], json!(true));
3963        let merge_commit = target["mergeMetadata"]["mergeCommitId"]
3964            .as_str()
3965            .unwrap()
3966            .to_string();
3967        assert!(is_object_id(&merge_commit));
3968        assert_eq!(out["pullRequest"]["pullRequestStatus"], json!("CLOSED"));
3969
3970        // Destination branch now points at the merge commit.
3971        let branch = call(
3972            &s,
3973            "GetBranch",
3974            json!({ "repositoryName": "repo", "branchName": "main" }),
3975        );
3976        assert_eq!(branch["branch"]["commitId"], json!(merge_commit));
3977
3978        // GetCommit on the merge commit succeeds and it has two parents.
3979        let got = call(
3980            &s,
3981            "GetCommit",
3982            json!({ "repositoryName": "repo", "commitId": merge_commit }),
3983        );
3984        assert_eq!(got["commit"]["parents"].as_array().unwrap().len(), 2);
3985    }
3986
3987    // Defect 1/2: a squash merge that deletes a file on source removes it from
3988    // the merged destination tree, and the squash commit has a single parent.
3989    #[test]
3990    fn merge_pr_squash_deletes_file_and_has_one_parent() {
3991        let s = svc();
3992        make_repo(&s);
3993        let c1 = put(&s, "main", "a.txt", "a", None);
3994        let c2 = put(&s, "main", "b.txt", "b", Some(&c1));
3995        call(
3996            &s,
3997            "CreateBranch",
3998            json!({ "repositoryName": "repo", "branchName": "feature", "commitId": c2 }),
3999        );
4000        // feature deletes b.txt; main advances independently.
4001        call(
4002            &s,
4003            "DeleteFile",
4004            json!({ "repositoryName": "repo", "branchName": "feature", "filePath": "b.txt", "parentCommitId": c2 }),
4005        );
4006        put(&s, "main", "c.txt", "c", Some(&c2));
4007
4008        let pr = create_pr(&s, "main");
4009        let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4010        let out = call(
4011            &s,
4012            "MergePullRequestBySquash",
4013            json!({ "pullRequestId": pr_id, "repositoryName": "repo" }),
4014        );
4015        let merge_commit = out["pullRequest"]["pullRequestTargets"][0]["mergeMetadata"]
4016            ["mergeCommitId"]
4017            .as_str()
4018            .unwrap()
4019            .to_string();
4020        // Squash => single parent.
4021        let got = call(
4022            &s,
4023            "GetCommit",
4024            json!({ "repositoryName": "repo", "commitId": merge_commit }),
4025        );
4026        assert_eq!(got["commit"]["parents"].as_array().unwrap().len(), 1);
4027        // b.txt is gone on the destination branch after merge.
4028        let err = call_err(
4029            &s,
4030            "GetFile",
4031            json!({ "repositoryName": "repo", "commitSpecifier": "main", "filePath": "b.txt" }),
4032        );
4033        assert_eq!(err.code(), "FileDoesNotExistException");
4034    }
4035
4036    // Defect 4: MergeBranchesBySquash yields a single-parent commit.
4037    #[test]
4038    fn merge_branches_squash_single_parent() {
4039        let s = svc();
4040        let (feature_tip, main_tip) = diverged_repo(&s);
4041        let out = call(
4042            &s,
4043            "MergeBranchesBySquash",
4044            json!({
4045                "repositoryName": "repo",
4046                "sourceCommitSpecifier": feature_tip,
4047                "destinationCommitSpecifier": main_tip,
4048                "targetBranch": "main",
4049            }),
4050        );
4051        let cid = out["commitId"].as_str().unwrap().to_string();
4052        let got = call(
4053            &s,
4054            "GetCommit",
4055            json!({ "repositoryName": "repo", "commitId": cid }),
4056        );
4057        assert_eq!(got["commit"]["parents"].as_array().unwrap().len(), 1);
4058    }
4059
4060    // Defect 5 + 6: an associated 2-approval template is auto-applied on PR
4061    // create and is NOT satisfied by a single approval.
4062    #[test]
4063    fn template_applied_and_two_approvals_needed() {
4064        let s = svc();
4065        let content = "{\"Version\":\"2018-11-08\",\"Statements\":[{\"Type\":\"Approvers\",\"NumberOfApprovalsNeeded\":2,\"ApprovalPoolMembers\":[]}]}";
4066        call(
4067            &s,
4068            "CreateApprovalRuleTemplate",
4069            json!({ "approvalRuleTemplateName": "two", "approvalRuleTemplateContent": content }),
4070        );
4071        diverged_repo(&s);
4072        call(
4073            &s,
4074            "AssociateApprovalRuleTemplateWithRepository",
4075            json!({ "approvalRuleTemplateName": "two", "repositoryName": "repo" }),
4076        );
4077        let pr = create_pr(&s, "main");
4078        let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4079        let revision = pr["revisionId"].as_str().unwrap().to_string();
4080
4081        // The template's rule is materialized on the PR with template origin.
4082        let rules = pr["approvalRules"].as_array().unwrap();
4083        assert_eq!(rules.len(), 1);
4084        assert_eq!(rules[0]["approvalRuleName"], json!("two"));
4085        assert!(rules[0]["originApprovalRuleTemplate"].is_object());
4086
4087        // One approval is not enough for a rule needing two.
4088        call_as(
4089            &s,
4090            "UpdatePullRequestApprovalState",
4091            json!({ "pullRequestId": pr_id, "revisionId": revision, "approvalState": "APPROVE" }),
4092            "arn:aws:iam::000000000000:user/reviewer",
4093        );
4094        let eval = call(
4095            &s,
4096            "EvaluatePullRequestApprovalRules",
4097            json!({ "pullRequestId": pr_id, "revisionId": revision }),
4098        );
4099        assert_eq!(eval["evaluation"]["approved"], json!(false));
4100        assert_eq!(
4101            eval["evaluation"]["approvalRulesNotSatisfied"],
4102            json!(["two"])
4103        );
4104    }
4105
4106    // Defect 7: a closed pull request cannot be reopened.
4107    #[test]
4108    fn closed_pr_cannot_reopen() {
4109        let s = svc();
4110        diverged_repo(&s);
4111        let pr = create_pr(&s, "main");
4112        let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4113        call(
4114            &s,
4115            "UpdatePullRequestStatus",
4116            json!({ "pullRequestId": pr_id, "pullRequestStatus": "CLOSED" }),
4117        );
4118        let err = call_err(
4119            &s,
4120            "UpdatePullRequestStatus",
4121            json!({ "pullRequestId": pr_id, "pullRequestStatus": "OPEN" }),
4122        );
4123        assert_eq!(err.code(), "InvalidPullRequestStatusUpdateException");
4124    }
4125
4126    // Defect 8: a nonexistent destinationReference is rejected before create.
4127    #[test]
4128    fn create_pr_rejects_missing_destination() {
4129        let s = svc();
4130        diverged_repo(&s);
4131        let err = call_err(
4132            &s,
4133            "CreatePullRequest",
4134            json!({
4135                "title": "PR",
4136                "targets": [{
4137                    "repositoryName": "repo",
4138                    "sourceReference": "feature",
4139                    "destinationReference": "does-not-exist",
4140                }],
4141            }),
4142        );
4143        assert_eq!(err.code(), "ReferenceDoesNotExistException");
4144    }
4145
4146    // Defect 9 + 10: Describe / BatchDescribe report a real content conflict.
4147    #[test]
4148    fn describe_and_batch_report_conflict() {
4149        let s = svc();
4150        make_repo(&s);
4151        let c1 = put(&s, "main", "a.txt", "base-content", None);
4152        call(
4153            &s,
4154            "CreateBranch",
4155            json!({ "repositoryName": "repo", "branchName": "feature", "commitId": c1 }),
4156        );
4157        put(&s, "feature", "a.txt", "feature-side", Some(&c1));
4158        put(&s, "main", "a.txt", "main-side", Some(&c1));
4159
4160        let base_body = json!({
4161            "repositoryName": "repo",
4162            "sourceCommitSpecifier": "feature",
4163            "destinationCommitSpecifier": "main",
4164            "mergeOption": "THREE_WAY_MERGE",
4165        });
4166
4167        let mut d = base_body.clone();
4168        d["filePath"] = json!("a.txt");
4169        let desc = call(&s, "DescribeMergeConflicts", d);
4170        assert_eq!(desc["conflictMetadata"]["numberOfConflicts"], json!(1));
4171        assert_eq!(desc["conflictMetadata"]["contentConflict"], json!(true));
4172        assert_eq!(desc["mergeHunks"][0]["isConflict"], json!(true));
4173
4174        let batch = call(&s, "BatchDescribeMergeConflicts", base_body.clone());
4175        let conflicts = batch["conflicts"].as_array().unwrap();
4176        assert_eq!(conflicts.len(), 1);
4177        assert_eq!(conflicts[0]["conflictMetadata"]["filePath"], json!("a.txt"));
4178
4179        // GetMergeConflicts agrees: not mergeable, a.txt listed.
4180        let gmc = call(&s, "GetMergeConflicts", base_body);
4181        assert_eq!(gmc["mergeable"], json!(false));
4182        assert_eq!(gmc["conflictMetadataList"][0]["filePath"], json!("a.txt"));
4183    }
4184
4185    // Defect 1: a fast-forward PR merge that is not fast-forwardable is rejected
4186    // and leaves the PR open.
4187    #[test]
4188    fn merge_pr_fast_forward_non_ff_rejected() {
4189        let s = svc();
4190        diverged_repo(&s);
4191        let pr = create_pr(&s, "main");
4192        let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4193        let err = call_err(
4194            &s,
4195            "MergePullRequestByFastForward",
4196            json!({ "pullRequestId": pr_id, "repositoryName": "repo" }),
4197        );
4198        assert_eq!(err.code(), "ManualMergeRequiredException");
4199        // PR remains open.
4200        let got = call(&s, "GetPullRequest", json!({ "pullRequestId": pr_id }));
4201        assert_eq!(got["pullRequest"]["pullRequestStatus"], json!("OPEN"));
4202    }
4203
4204    fn branch_tip(s: &CodeCommitService, branch: &str) -> String {
4205        call(
4206            s,
4207            "GetBranch",
4208            json!({ "repositoryName": "repo", "branchName": branch }),
4209        )["branch"]["commitId"]
4210            .as_str()
4211            .unwrap()
4212            .to_string()
4213    }
4214
4215    fn one_approval_rule() -> &'static str {
4216        "{\"Version\":\"2018-11-08\",\"Statements\":[{\"Type\":\"Approvers\",\"NumberOfApprovalsNeeded\":1,\"ApprovalPoolMembers\":[]}]}"
4217    }
4218
4219    // Round-2 defect 1: a new commit on the PR's source branch mints a new
4220    // revisionId and invalidates approvals cast against the prior revision.
4221    #[test]
4222    fn new_source_commit_bumps_revision_and_invalidates_approvals() {
4223        let s = svc();
4224        let (feature_tip, _) = diverged_repo(&s);
4225        let pr = create_pr(&s, "main");
4226        let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4227        let rev1 = pr["revisionId"].as_str().unwrap().to_string();
4228        call(
4229            &s,
4230            "CreatePullRequestApprovalRule",
4231            json!({
4232                "pullRequestId": pr_id,
4233                "approvalRuleName": "r",
4234                "approvalRuleContent": one_approval_rule(),
4235            }),
4236        );
4237        // One approval at rev1 satisfies the 1-approval rule.
4238        call_as(
4239            &s,
4240            "UpdatePullRequestApprovalState",
4241            json!({ "pullRequestId": pr_id, "revisionId": rev1, "approvalState": "APPROVE" }),
4242            "arn:aws:iam::000000000000:user/reviewer",
4243        );
4244        let eval1 = call(
4245            &s,
4246            "EvaluatePullRequestApprovalRules",
4247            json!({ "pullRequestId": pr_id, "revisionId": rev1 }),
4248        );
4249        assert_eq!(eval1["evaluation"]["approved"], json!(true));
4250
4251        // A new commit on the source branch bumps the revisionId...
4252        put(&s, "feature", "d.txt", "more", Some(&feature_tip));
4253        let got = call(&s, "GetPullRequest", json!({ "pullRequestId": pr_id }));
4254        let rev2 = got["pullRequest"]["revisionId"]
4255            .as_str()
4256            .unwrap()
4257            .to_string();
4258        assert_ne!(rev1, rev2, "source advance must re-mint the revisionId");
4259
4260        // ...and the old approval no longer counts against the new revision.
4261        let eval2 = call(
4262            &s,
4263            "EvaluatePullRequestApprovalRules",
4264            json!({ "pullRequestId": pr_id, "revisionId": rev2 }),
4265        );
4266        assert_eq!(eval2["evaluation"]["approved"], json!(false));
4267    }
4268
4269    // Round-2 defect 4: GetPullRequest reflects the advanced source tip.
4270    #[test]
4271    fn get_pull_request_reflects_advanced_source_tip() {
4272        let s = svc();
4273        let (feature_tip, _) = diverged_repo(&s);
4274        let pr = create_pr(&s, "main");
4275        let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4276        assert_eq!(
4277            pr["pullRequestTargets"][0]["sourceCommit"],
4278            json!(feature_tip)
4279        );
4280        put(&s, "feature", "e.txt", "x", Some(&feature_tip));
4281        let new_tip = branch_tip(&s, "feature");
4282        let got = call(&s, "GetPullRequest", json!({ "pullRequestId": pr_id }));
4283        assert_eq!(
4284            got["pullRequest"]["pullRequestTargets"][0]["sourceCommit"],
4285            json!(new_tip)
4286        );
4287    }
4288
4289    // Round-2 defect 2: CreateUnreferencedMergeCommit SQUASH -> single parent.
4290    #[test]
4291    fn create_unreferenced_squash_single_parent() {
4292        let s = svc();
4293        diverged_repo(&s);
4294        let out = call(
4295            &s,
4296            "CreateUnreferencedMergeCommit",
4297            json!({
4298                "repositoryName": "repo",
4299                "sourceCommitSpecifier": "feature",
4300                "destinationCommitSpecifier": "main",
4301                "mergeOption": "SQUASH_MERGE",
4302            }),
4303        );
4304        let cid = out["commitId"].as_str().unwrap().to_string();
4305        let got = call(
4306            &s,
4307            "GetCommit",
4308            json!({ "repositoryName": "repo", "commitId": cid }),
4309        );
4310        assert_eq!(got["commit"]["parents"].as_array().unwrap().len(), 1);
4311    }
4312
4313    // Round-2 defect 3: an invalid conflictResolutionStrategy is rejected on both
4314    // MergePullRequestBy* and CreateUnreferencedMergeCommit.
4315    #[test]
4316    fn invalid_conflict_strategy_rejected() {
4317        let s = svc();
4318        diverged_repo(&s);
4319        let pr = create_pr(&s, "main");
4320        let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4321        let e1 = call_err(
4322            &s,
4323            "MergePullRequestByThreeWay",
4324            json!({
4325                "pullRequestId": pr_id,
4326                "repositoryName": "repo",
4327                "conflictResolutionStrategy": "BOGUS",
4328            }),
4329        );
4330        assert_eq!(e1.code(), "InvalidConflictResolutionStrategyException");
4331        let e2 = call_err(
4332            &s,
4333            "CreateUnreferencedMergeCommit",
4334            json!({
4335                "repositoryName": "repo",
4336                "sourceCommitSpecifier": "feature",
4337                "destinationCommitSpecifier": "main",
4338                "mergeOption": "THREE_WAY_MERGE",
4339                "conflictResolutionStrategy": "BOGUS",
4340            }),
4341        );
4342        assert_eq!(e2.code(), "InvalidConflictResolutionStrategyException");
4343    }
4344
4345    // Round-2 defect 5: OPEN -> OPEN status update is rejected.
4346    #[test]
4347    fn open_to_open_status_rejected() {
4348        let s = svc();
4349        diverged_repo(&s);
4350        let pr = create_pr(&s, "main");
4351        let pr_id = pr["pullRequestId"].as_str().unwrap().to_string();
4352        let err = call_err(
4353            &s,
4354            "UpdatePullRequestStatus",
4355            json!({ "pullRequestId": pr_id, "pullRequestStatus": "OPEN" }),
4356        );
4357        assert_eq!(err.code(), "InvalidPullRequestStatusUpdateException");
4358    }
4359
4360    // Round-2 defect 6: an unresolvable trigger destination is reported as a
4361    // failed execution, while a valid SNS destination succeeds.
4362    #[test]
4363    fn test_triggers_reports_unresolvable_destination() {
4364        let s = svc();
4365        make_repo(&s);
4366        let out = call(
4367            &s,
4368            "TestRepositoryTriggers",
4369            json!({
4370                "repositoryName": "repo",
4371                "triggers": [
4372                    {
4373                        "name": "ok",
4374                        "destinationArn": "arn:aws:sns:us-east-1:000000000000:topic",
4375                        "events": ["all"],
4376                    },
4377                    {
4378                        "name": "bad",
4379                        "destinationArn": "arn:aws:s3:::some-bucket",
4380                        "events": ["all"],
4381                    },
4382                ],
4383            }),
4384        );
4385        assert_eq!(out["successfulExecutions"], json!(["ok"]));
4386        let failed = out["failedExecutions"].as_array().unwrap();
4387        assert_eq!(failed.len(), 1);
4388        assert_eq!(failed[0]["trigger"], json!("bad"));
4389        assert!(failed[0]["failureMessage"].as_str().unwrap().contains("s3"));
4390    }
4391
4392    // Round-2 defect 7: deleting a repository drops its template associations.
4393    #[test]
4394    fn delete_repository_clears_template_association() {
4395        let s = svc();
4396        make_repo(&s);
4397        call(
4398            &s,
4399            "CreateApprovalRuleTemplate",
4400            json!({
4401                "approvalRuleTemplateName": "t",
4402                "approvalRuleTemplateContent": one_approval_rule(),
4403            }),
4404        );
4405        call(
4406            &s,
4407            "AssociateApprovalRuleTemplateWithRepository",
4408            json!({ "approvalRuleTemplateName": "t", "repositoryName": "repo" }),
4409        );
4410        let before = call(
4411            &s,
4412            "ListRepositoriesForApprovalRuleTemplate",
4413            json!({ "approvalRuleTemplateName": "t" }),
4414        );
4415        assert_eq!(before["repositoryNames"], json!(["repo"]));
4416        call(&s, "DeleteRepository", json!({ "repositoryName": "repo" }));
4417        let after = call(
4418            &s,
4419            "ListRepositoriesForApprovalRuleTemplate",
4420            json!({ "approvalRuleTemplateName": "t" }),
4421        );
4422        assert_eq!(after["repositoryNames"], json!([]));
4423    }
4424
4425    #[test]
4426    fn list_file_commit_history_filters_by_file_path() {
4427        // ListFileCommitHistory ignored filePath, returning every commit; it
4428        // must return only commits that touched the given path (bug-hunt).
4429        let s = svc();
4430        make_repo(&s);
4431        // c1 touches a.txt; c2 touches b.txt; c3 modifies a.txt again.
4432        let c1 = put(&s, "main", "a.txt", "a1", None);
4433        let c2 = put(&s, "main", "b.txt", "b1", Some(&c1));
4434        let c3 = put(&s, "main", "a.txt", "a2", Some(&c2));
4435
4436        // History for a.txt: only c3 (modify) and c1 (add).
4437        let hist = call(
4438            &s,
4439            "ListFileCommitHistory",
4440            json!({
4441                "repositoryName": "repo",
4442                "commitSpecifier": "main",
4443                "filePath": "a.txt"
4444            }),
4445        );
4446        let commits: Vec<&str> = hist["revisionDag"]
4447            .as_array()
4448            .unwrap()
4449            .iter()
4450            .map(|d| d["commit"].as_str().unwrap())
4451            .collect();
4452        assert_eq!(
4453            commits,
4454            vec![c3.as_str(), c1.as_str()],
4455            "only a.txt commits"
4456        );
4457
4458        // History for b.txt: only c2.
4459        let hist_b = call(
4460            &s,
4461            "ListFileCommitHistory",
4462            json!({
4463                "repositoryName": "repo",
4464                "commitSpecifier": "main",
4465                "filePath": "b.txt"
4466            }),
4467        );
4468        let commits_b: Vec<&str> = hist_b["revisionDag"]
4469            .as_array()
4470            .unwrap()
4471            .iter()
4472            .map(|d| d["commit"].as_str().unwrap())
4473            .collect();
4474        assert_eq!(commits_b, vec![c2.as_str()], "only b.txt commit");
4475    }
4476}