Skip to main content

repo/
repository_context.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Context annotation helpers for attaching metadata to file and state targets.
3
4use std::{
5    collections::BTreeMap,
6    path::{Path, PathBuf},
7};
8
9use objects::object::{
10    Annotation, AnnotationScope, Blob, ContentHash, ContextBlob, ContextTarget, EntryType, State,
11    Tree, TreeEntry,
12};
13
14use super::{HeddleError, Repository, Result};
15
16#[derive(Clone, Debug, PartialEq, Eq)]
17pub struct ContextEntry {
18    pub target: ContextTarget,
19    pub blob: ContextBlob,
20}
21
22impl Repository {
23    /// Get the context blob for a target from the given state's context tree.
24    pub fn get_context_blob(
25        &self,
26        context_root: &ContentHash,
27        target: &ContextTarget,
28    ) -> Result<Option<ContextBlob>> {
29        let Some(blob_hash) = self.lookup_context_leaf_for_target(context_root, target)? else {
30            return Ok(None);
31        };
32        let Some(blob) = self.store.get_blob(&blob_hash)? else {
33            return Ok(None);
34        };
35        ContextBlob::decode(blob.content())
36            .map(Some)
37            .map_err(|e| HeddleError::InvalidObject(format!("invalid context blob: {e}")))
38    }
39
40    /// Store a context blob at a target, returning the new context tree root hash.
41    ///
42    /// If `context_root` is None, creates a new context tree from scratch.
43    pub fn set_context_blob(
44        &self,
45        context_root: Option<&ContentHash>,
46        target: &ContextTarget,
47        blob: &ContextBlob,
48    ) -> Result<ContentHash> {
49        let bytes = blob
50            .encode()
51            .map_err(|e| HeddleError::InvalidObject(format!("encode context: {e}")))?;
52        let blob_hash = self.store.put_blob(&Blob::new(bytes))?;
53
54        let current_tree = match context_root {
55            Some(root) => self.store.get_tree(root)?.unwrap_or_default(),
56            None => Tree::new(),
57        };
58
59        let mut root_hash =
60            self.insert_leaf_at_path(&current_tree, &target.storage_path(), blob_hash)?;
61
62        if let (Some(existing_root), Some(legacy_path)) =
63            (context_root, target.legacy_storage_path())
64            && legacy_path != target.storage_path()
65            && self
66                .lookup_context_leaf(existing_root, &legacy_path)?
67                .is_some()
68        {
69            root_hash = self
70                .remove_leaf_at_path(&root_hash, &legacy_path)?
71                .unwrap_or(root_hash);
72        }
73
74        Ok(root_hash)
75    }
76
77    /// Remove context at a target (optionally filtered by scope).
78    ///
79    /// Returns the new context tree root, or None if the tree is now empty.
80    pub fn remove_context_at_target(
81        &self,
82        context_root: &ContentHash,
83        target: &ContextTarget,
84        scope: Option<&AnnotationScope>,
85    ) -> Result<Option<ContentHash>> {
86        if let Some(scope) = scope {
87            if let Some(mut blob) = self.get_context_blob(context_root, target)? {
88                blob.annotations.retain(|a| !a.scope.matches(scope));
89                if blob.annotations.is_empty() {
90                    return self.remove_context_target(context_root, target);
91                }
92                let new_root = self.set_context_blob(Some(context_root), target, &blob)?;
93                return Ok(Some(new_root));
94            }
95            return Ok(Some(*context_root));
96        }
97
98        self.remove_context_target(context_root, target)
99    }
100
101    pub fn remove_context_target(
102        &self,
103        context_root: &ContentHash,
104        target: &ContextTarget,
105    ) -> Result<Option<ContentHash>> {
106        let mut current = self.remove_leaf_at_path(context_root, &target.storage_path())?;
107        if current.is_none()
108            && let Some(legacy_path) = target.legacy_storage_path()
109        {
110            current = self.remove_leaf_at_path(context_root, &legacy_path)?;
111        }
112        Ok(current)
113    }
114
115    /// List all context entries in the tree, optionally filtered by file prefix.
116    pub fn list_context_entries(
117        &self,
118        context_root: &ContentHash,
119        prefix: Option<&Path>,
120    ) -> Result<Vec<ContextEntry>> {
121        let tree = match self.store.get_tree(context_root)? {
122            Some(t) => t,
123            None => return Ok(Vec::new()),
124        };
125        let mut results = BTreeMap::new();
126        self.walk_context_tree(&tree, &PathBuf::new(), prefix, &mut results)?;
127        Ok(results
128            .into_iter()
129            .map(|(_, (target, blob))| ContextEntry { target, blob })
130            .collect())
131    }
132
133    pub fn find_annotation(
134        &self,
135        context_root: &ContentHash,
136        annotation_id: &str,
137    ) -> Result<Option<(ContextTarget, ContextBlob, usize)>> {
138        for entry in self.list_context_entries(context_root, None)? {
139            if let Some(index) = entry
140                .blob
141                .annotations
142                .iter()
143                .position(|annotation| annotation.annotation_id == annotation_id)
144            {
145                return Ok(Some((entry.target, entry.blob, index)));
146            }
147        }
148        Ok(None)
149    }
150
151    // --- private helpers ---
152
153    fn lookup_context_leaf_for_target(
154        &self,
155        root: &ContentHash,
156        target: &ContextTarget,
157    ) -> Result<Option<ContentHash>> {
158        let storage_path = target.storage_path();
159        if let Some(hash) = self.lookup_context_leaf(root, &storage_path)? {
160            return Ok(Some(hash));
161        }
162        if let Some(legacy_path) = target.legacy_storage_path() {
163            return self.lookup_context_leaf(root, &legacy_path);
164        }
165        Ok(None)
166    }
167
168    fn lookup_context_leaf(&self, root: &ContentHash, path: &Path) -> Result<Option<ContentHash>> {
169        let Some((name, rest)) = split_path(path) else {
170            return Ok(None);
171        };
172        let Some(tree) = self.store.get_tree(root)? else {
173            return Ok(None);
174        };
175        let Some(entry) = tree.get(name) else {
176            return Ok(None);
177        };
178        if rest.as_os_str().is_empty() {
179            return Ok(entry.is_blob().then_some(entry.hash));
180        }
181        if !entry.is_tree() {
182            return Ok(None);
183        }
184        self.lookup_context_leaf(&entry.hash, rest)
185    }
186
187    fn insert_leaf_at_path(
188        &self,
189        tree: &Tree,
190        path: &Path,
191        blob_hash: ContentHash,
192    ) -> Result<ContentHash> {
193        let Some((name, rest)) = split_path(path) else {
194            return Err(HeddleError::InvalidObject("empty path".to_string()));
195        };
196
197        let mut new_tree = tree.clone();
198
199        if rest.as_os_str().is_empty() {
200            new_tree.insert(TreeEntry::file(name, blob_hash, false)?);
201        } else {
202            let subtree = tree
203                .get(name)
204                .filter(|e| e.is_tree())
205                .and_then(|e| self.store.get_tree(&e.hash).ok().flatten())
206                .unwrap_or_default();
207
208            let sub_hash = self.insert_leaf_at_path(&subtree, rest, blob_hash)?;
209            new_tree.insert(TreeEntry::directory(name, sub_hash)?);
210        }
211
212        self.store.put_tree(&new_tree)
213    }
214
215    fn remove_leaf_at_path(&self, root: &ContentHash, path: &Path) -> Result<Option<ContentHash>> {
216        let Some(tree) = self.store.get_tree(root)? else {
217            return Ok(None);
218        };
219        let Some((name, rest)) = split_path(path) else {
220            return Ok(None);
221        };
222
223        let mut new_tree = tree.clone();
224
225        if rest.as_os_str().is_empty() {
226            new_tree.remove(name);
227        } else {
228            let Some(entry) = tree.get(name) else {
229                return Ok(Some(*root));
230            };
231            if !entry.is_tree() {
232                return Ok(Some(*root));
233            }
234            match self.remove_leaf_at_path(&entry.hash, rest)? {
235                Some(sub_hash) => {
236                    new_tree.insert(TreeEntry::directory(name, sub_hash)?);
237                }
238                None => {
239                    new_tree.remove(name);
240                }
241            }
242        }
243
244        if new_tree.is_empty() {
245            Ok(None)
246        } else {
247            Ok(Some(self.store.put_tree(&new_tree)?))
248        }
249    }
250
251    fn walk_context_tree(
252        &self,
253        tree: &Tree,
254        current_path: &Path,
255        prefix: Option<&Path>,
256        results: &mut BTreeMap<String, (ContextTarget, ContextBlob)>,
257    ) -> Result<()> {
258        for entry in tree.entries() {
259            let entry_path = current_path.join(&entry.name);
260            match entry.entry_type {
261                EntryType::Tree => {
262                    if let Some(prefix) = prefix
263                        && !prefix.starts_with(&entry_path)
264                        && !entry_path.starts_with(prefix)
265                        && !entry_path.starts_with("__files")
266                        && !entry_path.starts_with("__states")
267                    {
268                        continue;
269                    }
270                    if let Some(subtree) = self.store.get_tree(&entry.hash)? {
271                        self.walk_context_tree(&subtree, &entry_path, prefix, results)?;
272                    }
273                }
274                EntryType::Blob => {
275                    let Some(target) = ContextTarget::from_storage_path(&entry_path) else {
276                        continue;
277                    };
278                    if let Some(prefix) = prefix
279                        && let Some(path) = target.path()
280                        && !Path::new(path).starts_with(prefix)
281                    {
282                        continue;
283                    }
284                    if let Some(blob) = self.store.get_blob(&entry.hash)?
285                        && let Ok(context) = ContextBlob::decode(blob.content())
286                    {
287                        results.insert(context_entry_key(&target), (target, context));
288                    }
289                }
290                EntryType::Symlink => {}
291            }
292        }
293        Ok(())
294    }
295
296    /// Carry a single parent state's context tree forward onto a new
297    /// snapshot. Because context trees are content-addressed, this is a
298    /// pointer copy: the new state's `context` field gets the same
299    /// `ContentHash` as the parent. Annotations attached upstream remain
300    /// active at the new state, and the existing on-demand staleness check
301    /// (which compares the stored `source_hash` against the current bytes
302    /// at the anchor) naturally reports drift caused by the new tree.
303    ///
304    /// Returns `None` when the parent has no context tree.
305    pub fn inherit_parent_context(parent: &State) -> Option<ContentHash> {
306        parent.context
307    }
308
309    /// Build a unioned context tree across multiple parent states for a
310    /// merge snapshot. Annotations from every parent appear in the result;
311    /// when the same `annotation_id` is present on more than one parent the
312    /// revision with the latest `created_at` wins (with a stable tiebreak
313    /// on revision_id so the merge stays deterministic).
314    ///
315    /// Targets that only exist on one side propagate unchanged (single-blob
316    /// pointer copy via the existing tree). Targets present on both sides
317    /// are merged blob-by-blob: annotations are deduped by id, the per-id
318    /// revisions are picked by latest-`created_at`, and the resulting blob
319    /// is rewritten via `set_context_blob`.
320    ///
321    /// Returns `None` when none of the parents has any context.
322    pub fn union_parent_contexts(&self, parents: &[&State]) -> Result<Option<ContentHash>> {
323        // Fast paths: nothing or single-parent.
324        let mut roots: Vec<ContentHash> = parents.iter().filter_map(|p| p.context).collect();
325        if roots.is_empty() {
326            return Ok(None);
327        }
328        if roots.len() == 1 {
329            return Ok(Some(roots.pop().expect("len == 1")));
330        }
331        if roots.iter().all(|r| *r == roots[0]) {
332            // All parents pointed at the same context tree; pointer copy.
333            return Ok(Some(roots[0]));
334        }
335
336        // Walk every parent and merge by `context_entry_key`. Each entry's
337        // blob gets unioned into a running map; ties are broken by the
338        // revision-comparator below.
339        let mut merged: BTreeMap<String, (ContextTarget, ContextBlob)> = BTreeMap::new();
340        for parent_root in &roots {
341            for entry in self.list_context_entries(parent_root, None)? {
342                let key = context_entry_key(&entry.target);
343                match merged.remove(&key) {
344                    None => {
345                        merged.insert(key, (entry.target, entry.blob));
346                    }
347                    Some((target, existing)) => {
348                        let merged_blob = merge_context_blobs(existing, entry.blob);
349                        merged.insert(key, (target, merged_blob));
350                    }
351                }
352            }
353        }
354
355        if merged.is_empty() {
356            return Ok(None);
357        }
358
359        // Rebuild the tree from scratch by writing each blob.
360        let mut root: Option<ContentHash> = None;
361        for (_, (target, blob)) in merged {
362            if blob.annotations.is_empty() {
363                continue;
364            }
365            let new_root = self.set_context_blob(root.as_ref(), &target, &blob)?;
366            root = Some(new_root);
367        }
368
369        Ok(root)
370    }
371}
372
373/// Merge two `ContextBlob`s by unioning their annotations on
374/// `annotation_id`. When an id appears in both, the annotation with the
375/// later current-revision `created_at` wins; ties are broken by
376/// `revision_id` to keep the result deterministic. The resulting blob
377/// preserves `format_version` from `left`.
378fn merge_context_blobs(left: ContextBlob, right: ContextBlob) -> ContextBlob {
379    let format_version = left.format_version;
380    let mut by_id: BTreeMap<String, Annotation> = BTreeMap::new();
381    for annotation in left.annotations.into_iter().chain(right.annotations) {
382        match by_id.remove(&annotation.annotation_id) {
383            None => {
384                by_id.insert(annotation.annotation_id.clone(), annotation);
385            }
386            Some(existing) => {
387                let winner = pick_newer_annotation(existing, annotation);
388                by_id.insert(winner.annotation_id.clone(), winner);
389            }
390        }
391    }
392    ContextBlob {
393        format_version,
394        annotations: by_id.into_values().collect(),
395    }
396}
397
398fn pick_newer_annotation(a: Annotation, b: Annotation) -> Annotation {
399    let ts_a = a
400        .current_revision()
401        .map(|r| r.created_at)
402        .unwrap_or(i64::MIN);
403    let ts_b = b
404        .current_revision()
405        .map(|r| r.created_at)
406        .unwrap_or(i64::MIN);
407    if ts_a > ts_b {
408        a
409    } else if ts_b > ts_a {
410        b
411    } else {
412        // Deterministic tiebreak on the revision_id of each side's current
413        // revision (lexicographic — revision_ids are stable strings).
414        let rev_a = a
415            .current_revision()
416            .map(|r| r.revision_id.as_str())
417            .unwrap_or("");
418        let rev_b = b
419            .current_revision()
420            .map(|r| r.revision_id.as_str())
421            .unwrap_or("");
422        if rev_a >= rev_b { a } else { b }
423    }
424}
425
426fn context_entry_key(target: &ContextTarget) -> String {
427    match target {
428        ContextTarget::File { path } => format!("file:{path}"),
429        ContextTarget::State { change_id } => format!("state:{}", change_id.to_string_full()),
430    }
431}
432
433fn split_path(path: &Path) -> Option<(&str, &Path)> {
434    let mut components = path.components();
435    let first = components.next()?;
436    let std::path::Component::Normal(name) = first else {
437        return None;
438    };
439    Some((name.to_str()?, components.as_path()))
440}
441
442#[cfg(test)]
443mod tests {
444    use objects::object::{Annotation, AnnotationKind, ChangeId};
445    use tempfile::TempDir;
446
447    use super::{Repository, *};
448
449    fn setup() -> (TempDir, Repository) {
450        let dir = TempDir::new().unwrap();
451        let repo = Repository::init_default(dir.path()).unwrap();
452        (dir, repo)
453    }
454
455    fn make_annotation(scope: AnnotationScope, content: &str) -> Annotation {
456        Annotation::new(
457            scope,
458            AnnotationKind::Rationale,
459            content.to_string(),
460            vec![],
461            "test@example.com".to_string(),
462            1700000000,
463            None,
464            None,
465        )
466    }
467
468    #[test]
469    fn get_and_set_context_blob_for_file_target() {
470        let (_dir, repo) = setup();
471        let target = ContextTarget::file("src/main.rs").unwrap();
472        let blob = ContextBlob::new(vec![make_annotation(AnnotationScope::File, "Entry point")]);
473
474        let root = repo.set_context_blob(None, &target, &blob).unwrap();
475        let retrieved = repo.get_context_blob(&root, &target).unwrap().unwrap();
476
477        assert_eq!(retrieved, blob);
478    }
479
480    #[test]
481    fn supports_state_targets() {
482        let (_dir, repo) = setup();
483        let target = ContextTarget::state(ChangeId::generate());
484        let blob = ContextBlob::new(vec![make_annotation(AnnotationScope::File, "review note")]);
485
486        let root = repo.set_context_blob(None, &target, &blob).unwrap();
487        let retrieved = repo.get_context_blob(&root, &target).unwrap().unwrap();
488        assert_eq!(retrieved, blob);
489    }
490
491    #[test]
492    fn remove_context_blob_by_scope() {
493        let (_dir, repo) = setup();
494        let target = ContextTarget::file("src/lib.rs").unwrap();
495        let blob = ContextBlob::new(vec![
496            make_annotation(AnnotationScope::File, "file-level"),
497            make_annotation(AnnotationScope::Lines(1, 10), "range-level"),
498        ]);
499
500        let root = repo.set_context_blob(None, &target, &blob).unwrap();
501        let new_root = repo
502            .remove_context_at_target(&root, &target, Some(&AnnotationScope::Lines(1, 10)))
503            .unwrap()
504            .unwrap();
505        let remaining = repo.get_context_blob(&new_root, &target).unwrap().unwrap();
506
507        assert_eq!(remaining.annotations.len(), 1);
508        assert_eq!(
509            remaining
510                .annotations
511                .first()
512                .unwrap()
513                .current_revision()
514                .unwrap()
515                .content,
516            "file-level"
517        );
518    }
519
520    #[test]
521    fn list_context_entries_filters_by_prefix() {
522        let (_dir, repo) = setup();
523        let target1 = ContextTarget::file("src/main.rs").unwrap();
524        let target2 = ContextTarget::file("src/lib.rs").unwrap();
525        let target3 = ContextTarget::file("tests/test.rs").unwrap();
526        let blob1 = ContextBlob::new(vec![make_annotation(AnnotationScope::File, "first")]);
527        let blob2 = ContextBlob::new(vec![make_annotation(AnnotationScope::File, "second")]);
528        let blob3 = ContextBlob::new(vec![make_annotation(AnnotationScope::File, "third")]);
529
530        let root1 = repo.set_context_blob(None, &target1, &blob1).unwrap();
531        let root2 = repo
532            .set_context_blob(Some(&root1), &target2, &blob2)
533            .unwrap();
534        let root3 = repo
535            .set_context_blob(Some(&root2), &target3, &blob3)
536            .unwrap();
537
538        let all = repo.list_context_entries(&root3, None).unwrap();
539        assert_eq!(all.len(), 3);
540
541        let src_only = repo
542            .list_context_entries(&root3, Some(Path::new("src")))
543            .unwrap();
544        assert_eq!(src_only.len(), 2);
545
546        let exact_root_file = repo
547            .list_context_entries(&root3, Some(Path::new("tests/test.rs")))
548            .unwrap();
549        assert_eq!(exact_root_file.len(), 1);
550    }
551
552    #[test]
553    fn find_annotation_returns_target_and_index() {
554        let (_dir, repo) = setup();
555        let target = ContextTarget::file("src/main.rs").unwrap();
556        let blob = ContextBlob::new(vec![make_annotation(AnnotationScope::File, "first")]);
557        let annotation_id = blob.annotations[0].annotation_id.clone();
558        let root = repo.set_context_blob(None, &target, &blob).unwrap();
559
560        let found = repo
561            .find_annotation(&root, &annotation_id)
562            .unwrap()
563            .unwrap();
564        assert_eq!(found.0, target);
565        assert_eq!(found.2, 0);
566    }
567
568    /// Build a synthetic State whose `context` field points at a freshly
569    /// rooted context tree containing the given annotations on a single
570    /// file target. The state's `tree` is left at its default — the helpers
571    /// under test never inspect it.
572    fn state_with_context(repo: &Repository, path: &str, anns: Vec<Annotation>) -> State {
573        let target = ContextTarget::file(path).unwrap();
574        let blob = ContextBlob::new(anns);
575        let root = repo.set_context_blob(None, &target, &blob).unwrap();
576        let mut state = State::new_snapshot(
577            ContentHash::compute(b""),
578            vec![],
579            objects::object::Attribution::human(objects::object::Principal::new(
580                "test",
581                "test@example.com",
582            )),
583        );
584        state = state.with_context(root);
585        state
586    }
587
588    fn ann_with_id(id: &str, content: &str, created_at: i64) -> Annotation {
589        let mut a = Annotation::new(
590            AnnotationScope::File,
591            AnnotationKind::Rationale,
592            content.to_string(),
593            vec![],
594            "test@example.com".to_string(),
595            created_at,
596            None,
597            None,
598        );
599        a.annotation_id = id.to_string();
600        a
601    }
602
603    #[test]
604    fn inherit_parent_context_passes_through_pointer() {
605        let (_dir, repo) = setup();
606        let parent = state_with_context(
607            &repo,
608            "src/lib.rs",
609            vec![make_annotation(AnnotationScope::File, "first")],
610        );
611        let inherited = Repository::inherit_parent_context(&parent);
612        assert_eq!(inherited, parent.context);
613    }
614
615    #[test]
616    fn inherit_parent_context_yields_none_when_parent_has_none() {
617        let parent = State::new_snapshot(
618            ContentHash::compute(b""),
619            vec![],
620            objects::object::Attribution::human(objects::object::Principal::new(
621                "test",
622                "test@example.com",
623            )),
624        );
625        assert_eq!(Repository::inherit_parent_context(&parent), None);
626    }
627
628    #[test]
629    fn union_parent_contexts_returns_none_for_empty_parents() {
630        let (_dir, repo) = setup();
631        let p = State::new_snapshot(
632            ContentHash::compute(b""),
633            vec![],
634            objects::object::Attribution::human(objects::object::Principal::new(
635                "test",
636                "test@example.com",
637            )),
638        );
639        let merged = repo.union_parent_contexts(&[&p, &p]).unwrap();
640        assert_eq!(merged, None);
641    }
642
643    #[test]
644    fn union_parent_contexts_pointer_copies_when_one_side_has_context() {
645        let (_dir, repo) = setup();
646        let parent_with = state_with_context(
647            &repo,
648            "src/lib.rs",
649            vec![make_annotation(AnnotationScope::File, "first")],
650        );
651        let parent_without = State::new_snapshot(
652            ContentHash::compute(b""),
653            vec![],
654            objects::object::Attribution::human(objects::object::Principal::new(
655                "test",
656                "test@example.com",
657            )),
658        );
659        let merged = repo
660            .union_parent_contexts(&[&parent_with, &parent_without])
661            .unwrap();
662        assert_eq!(merged, parent_with.context);
663    }
664
665    #[test]
666    fn union_parent_contexts_carries_disjoint_annotations() {
667        let (_dir, repo) = setup();
668        let left = state_with_context(
669            &repo,
670            "src/lib.rs",
671            vec![ann_with_id("ann-a", "left side", 1)],
672        );
673        let right = state_with_context(
674            &repo,
675            "src/main.rs",
676            vec![ann_with_id("ann-b", "right side", 1)],
677        );
678        let merged = repo
679            .union_parent_contexts(&[&left, &right])
680            .unwrap()
681            .expect("merged context root");
682        let entries = repo.list_context_entries(&merged, None).unwrap();
683        assert_eq!(entries.len(), 2);
684        let mut ids: Vec<String> = entries
685            .iter()
686            .flat_map(|e| e.blob.annotations.iter().map(|a| a.annotation_id.clone()))
687            .collect();
688        ids.sort();
689        assert_eq!(ids, vec!["ann-a".to_string(), "ann-b".to_string()]);
690    }
691
692    #[test]
693    fn union_parent_contexts_dedupes_same_id_with_newest_revision_wins() {
694        let (_dir, repo) = setup();
695        let older = ann_with_id("ann-shared", "older content", 1);
696        let newer = ann_with_id("ann-shared", "newer content", 9);
697        let left = state_with_context(&repo, "src/lib.rs", vec![older]);
698        let right = state_with_context(&repo, "src/lib.rs", vec![newer]);
699        let merged = repo
700            .union_parent_contexts(&[&left, &right])
701            .unwrap()
702            .expect("merged context root");
703        let entries = repo.list_context_entries(&merged, None).unwrap();
704        assert_eq!(entries.len(), 1);
705        let blob = &entries[0].blob;
706        assert_eq!(blob.annotations.len(), 1);
707        let revision = blob.annotations[0]
708            .current_revision()
709            .expect("annotation has a revision");
710        assert_eq!(revision.content, "newer content");
711    }
712}