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