status.rs

  1use crate::{Oid, repository::RepoPath};
  2use anyhow::{Result, anyhow};
  3use collections::HashMap;
  4use gpui::SharedString;
  5use serde::{Deserialize, Serialize};
  6use std::{str::FromStr, sync::Arc};
  7use util::{ResultExt, rel_path::RelPath};
  8
  9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
 10pub enum FileStatus {
 11    Untracked,
 12    Ignored,
 13    Unmerged(UnmergedStatus),
 14    Tracked(TrackedStatus),
 15}
 16
 17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
 18pub struct UnmergedStatus {
 19    pub first_head: UnmergedStatusCode,
 20    pub second_head: UnmergedStatusCode,
 21}
 22
 23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
 24pub enum UnmergedStatusCode {
 25    Added,
 26    Deleted,
 27    Updated,
 28}
 29
 30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
 31pub struct TrackedStatus {
 32    pub index_status: StatusCode,
 33    pub worktree_status: StatusCode,
 34}
 35
 36#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
 37pub enum StatusCode {
 38    Modified,
 39    TypeChanged,
 40    Added,
 41    Deleted,
 42    Renamed,
 43    Copied,
 44    Unmodified,
 45}
 46
 47impl From<UnmergedStatus> for FileStatus {
 48    fn from(value: UnmergedStatus) -> Self {
 49        FileStatus::Unmerged(value)
 50    }
 51}
 52
 53impl From<TrackedStatus> for FileStatus {
 54    fn from(value: TrackedStatus) -> Self {
 55        FileStatus::Tracked(value)
 56    }
 57}
 58
 59#[derive(Debug, PartialEq, Eq, Clone, Copy)]
 60pub enum StageStatus {
 61    Staged,
 62    Unstaged,
 63    PartiallyStaged,
 64}
 65
 66impl StageStatus {
 67    pub const fn is_fully_staged(&self) -> bool {
 68        matches!(self, StageStatus::Staged)
 69    }
 70
 71    pub const fn is_fully_unstaged(&self) -> bool {
 72        matches!(self, StageStatus::Unstaged)
 73    }
 74
 75    pub const fn has_staged(&self) -> bool {
 76        matches!(self, StageStatus::Staged | StageStatus::PartiallyStaged)
 77    }
 78
 79    pub const fn has_unstaged(&self) -> bool {
 80        matches!(self, StageStatus::Unstaged | StageStatus::PartiallyStaged)
 81    }
 82
 83    pub const fn as_bool(self) -> Option<bool> {
 84        match self {
 85            StageStatus::Staged => Some(true),
 86            StageStatus::Unstaged => Some(false),
 87            StageStatus::PartiallyStaged => None,
 88        }
 89    }
 90}
 91
 92impl FileStatus {
 93    pub const fn worktree(worktree_status: StatusCode) -> Self {
 94        FileStatus::Tracked(TrackedStatus {
 95            index_status: StatusCode::Unmodified,
 96            worktree_status,
 97        })
 98    }
 99
100    pub const fn index(index_status: StatusCode) -> Self {
101        FileStatus::Tracked(TrackedStatus {
102            worktree_status: StatusCode::Unmodified,
103            index_status,
104        })
105    }
106
107    /// Generate a FileStatus Code from a byte pair, as described in
108    /// https://git-scm.com/docs/git-status#_output
109    ///
110    /// NOTE: That instead of '', we use ' ' to denote no change
111    fn from_bytes(bytes: [u8; 2]) -> anyhow::Result<Self> {
112        let status = match bytes {
113            [b'?', b'?'] => FileStatus::Untracked,
114            [b'!', b'!'] => FileStatus::Ignored,
115            [b'A', b'A'] => UnmergedStatus {
116                first_head: UnmergedStatusCode::Added,
117                second_head: UnmergedStatusCode::Added,
118            }
119            .into(),
120            [b'D', b'D'] => UnmergedStatus {
121                first_head: UnmergedStatusCode::Added,
122                second_head: UnmergedStatusCode::Added,
123            }
124            .into(),
125            [x, b'U'] => UnmergedStatus {
126                first_head: UnmergedStatusCode::from_byte(x)?,
127                second_head: UnmergedStatusCode::Updated,
128            }
129            .into(),
130            [b'U', y] => UnmergedStatus {
131                first_head: UnmergedStatusCode::Updated,
132                second_head: UnmergedStatusCode::from_byte(y)?,
133            }
134            .into(),
135            [x, y] => TrackedStatus {
136                index_status: StatusCode::from_byte(x)?,
137                worktree_status: StatusCode::from_byte(y)?,
138            }
139            .into(),
140        };
141        Ok(status)
142    }
143
144    pub fn staging(self) -> StageStatus {
145        match self {
146            FileStatus::Untracked | FileStatus::Ignored | FileStatus::Unmerged { .. } => {
147                StageStatus::Unstaged
148            }
149            FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) {
150                (StatusCode::Unmodified, _) => StageStatus::Unstaged,
151                (_, StatusCode::Unmodified) => StageStatus::Staged,
152                _ => StageStatus::PartiallyStaged,
153            },
154        }
155    }
156
157    pub fn is_conflicted(self) -> bool {
158        matches!(self, FileStatus::Unmerged { .. })
159    }
160
161    pub fn is_ignored(self) -> bool {
162        matches!(self, FileStatus::Ignored)
163    }
164
165    pub fn has_changes(&self) -> bool {
166        self.is_modified()
167            || self.is_created()
168            || self.is_deleted()
169            || self.is_untracked()
170            || self.is_conflicted()
171    }
172
173    pub fn is_modified(self) -> bool {
174        match self {
175            FileStatus::Tracked(tracked) => matches!(
176                (tracked.index_status, tracked.worktree_status),
177                (StatusCode::Modified, _) | (_, StatusCode::Modified)
178            ),
179            _ => false,
180        }
181    }
182
183    pub fn is_created(self) -> bool {
184        match self {
185            FileStatus::Tracked(tracked) => matches!(
186                (tracked.index_status, tracked.worktree_status),
187                (StatusCode::Added, _) | (_, StatusCode::Added)
188            ),
189            FileStatus::Untracked => true,
190            _ => false,
191        }
192    }
193
194    pub fn is_deleted(self) -> bool {
195        let FileStatus::Tracked(tracked) = self else {
196            return false;
197        };
198        tracked.index_status == StatusCode::Deleted && tracked.worktree_status != StatusCode::Added
199            || tracked.worktree_status == StatusCode::Deleted
200    }
201
202    pub fn is_untracked(self) -> bool {
203        matches!(self, FileStatus::Untracked)
204    }
205
206    pub fn summary(self) -> GitSummary {
207        match self {
208            FileStatus::Ignored => GitSummary::UNCHANGED,
209            FileStatus::Untracked => GitSummary::UNTRACKED,
210            FileStatus::Unmerged(_) => GitSummary::CONFLICT,
211            FileStatus::Tracked(TrackedStatus {
212                index_status,
213                worktree_status,
214            }) => GitSummary {
215                index: index_status.to_summary(),
216                worktree: worktree_status.to_summary(),
217                conflict: 0,
218                untracked: 0,
219                count: 1,
220            },
221        }
222    }
223}
224
225impl StatusCode {
226    fn from_byte(byte: u8) -> anyhow::Result<Self> {
227        match byte {
228            b'M' => Ok(StatusCode::Modified),
229            b'T' => Ok(StatusCode::TypeChanged),
230            b'A' => Ok(StatusCode::Added),
231            b'D' => Ok(StatusCode::Deleted),
232            b'R' => Ok(StatusCode::Renamed),
233            b'C' => Ok(StatusCode::Copied),
234            b' ' => Ok(StatusCode::Unmodified),
235            _ => anyhow::bail!("Invalid status code: {byte}"),
236        }
237    }
238
239    fn to_summary(self) -> TrackedSummary {
240        match self {
241            StatusCode::Modified | StatusCode::TypeChanged => TrackedSummary {
242                modified: 1,
243                ..TrackedSummary::UNCHANGED
244            },
245            StatusCode::Added => TrackedSummary {
246                added: 1,
247                ..TrackedSummary::UNCHANGED
248            },
249            StatusCode::Deleted => TrackedSummary {
250                deleted: 1,
251                ..TrackedSummary::UNCHANGED
252            },
253            StatusCode::Renamed | StatusCode::Copied | StatusCode::Unmodified => {
254                TrackedSummary::UNCHANGED
255            }
256        }
257    }
258
259    pub fn index(self) -> FileStatus {
260        FileStatus::Tracked(TrackedStatus {
261            index_status: self,
262            worktree_status: StatusCode::Unmodified,
263        })
264    }
265
266    pub fn worktree(self) -> FileStatus {
267        FileStatus::Tracked(TrackedStatus {
268            index_status: StatusCode::Unmodified,
269            worktree_status: self,
270        })
271    }
272}
273
274impl UnmergedStatusCode {
275    fn from_byte(byte: u8) -> anyhow::Result<Self> {
276        match byte {
277            b'A' => Ok(UnmergedStatusCode::Added),
278            b'D' => Ok(UnmergedStatusCode::Deleted),
279            b'U' => Ok(UnmergedStatusCode::Updated),
280            _ => anyhow::bail!("Invalid unmerged status code: {byte}"),
281        }
282    }
283}
284
285#[derive(Clone, Debug, Default, Copy, PartialEq, Eq)]
286pub struct TrackedSummary {
287    pub added: usize,
288    pub modified: usize,
289    pub deleted: usize,
290}
291
292impl TrackedSummary {
293    pub const UNCHANGED: Self = Self {
294        added: 0,
295        modified: 0,
296        deleted: 0,
297    };
298
299    pub const ADDED: Self = Self {
300        added: 1,
301        modified: 0,
302        deleted: 0,
303    };
304
305    pub const MODIFIED: Self = Self {
306        added: 0,
307        modified: 1,
308        deleted: 0,
309    };
310
311    pub const DELETED: Self = Self {
312        added: 0,
313        modified: 0,
314        deleted: 1,
315    };
316}
317
318impl std::ops::AddAssign for TrackedSummary {
319    fn add_assign(&mut self, rhs: Self) {
320        self.added += rhs.added;
321        self.modified += rhs.modified;
322        self.deleted += rhs.deleted;
323    }
324}
325
326impl std::ops::Add for TrackedSummary {
327    type Output = Self;
328
329    fn add(self, rhs: Self) -> Self::Output {
330        TrackedSummary {
331            added: self.added + rhs.added,
332            modified: self.modified + rhs.modified,
333            deleted: self.deleted + rhs.deleted,
334        }
335    }
336}
337
338impl std::ops::Sub for TrackedSummary {
339    type Output = Self;
340
341    fn sub(self, rhs: Self) -> Self::Output {
342        TrackedSummary {
343            added: self.added - rhs.added,
344            modified: self.modified - rhs.modified,
345            deleted: self.deleted - rhs.deleted,
346        }
347    }
348}
349
350#[derive(Clone, Debug, Default, Copy, PartialEq, Eq)]
351pub struct GitSummary {
352    pub index: TrackedSummary,
353    pub worktree: TrackedSummary,
354    pub conflict: usize,
355    pub untracked: usize,
356    pub count: usize,
357}
358
359impl GitSummary {
360    pub const CONFLICT: Self = Self {
361        conflict: 1,
362        count: 1,
363        ..Self::UNCHANGED
364    };
365
366    pub const UNTRACKED: Self = Self {
367        untracked: 1,
368        count: 1,
369        ..Self::UNCHANGED
370    };
371
372    pub const UNCHANGED: Self = Self {
373        index: TrackedSummary::UNCHANGED,
374        worktree: TrackedSummary::UNCHANGED,
375        conflict: 0,
376        untracked: 0,
377        count: 0,
378    };
379}
380
381impl From<FileStatus> for GitSummary {
382    fn from(status: FileStatus) -> Self {
383        status.summary()
384    }
385}
386
387impl sum_tree::ContextLessSummary for GitSummary {
388    fn zero() -> Self {
389        Default::default()
390    }
391
392    fn add_summary(&mut self, rhs: &Self) {
393        *self += *rhs;
394    }
395}
396
397impl std::ops::Add<Self> for GitSummary {
398    type Output = Self;
399
400    fn add(mut self, rhs: Self) -> Self {
401        self += rhs;
402        self
403    }
404}
405
406impl std::ops::AddAssign for GitSummary {
407    fn add_assign(&mut self, rhs: Self) {
408        self.index += rhs.index;
409        self.worktree += rhs.worktree;
410        self.conflict += rhs.conflict;
411        self.untracked += rhs.untracked;
412        self.count += rhs.count;
413    }
414}
415
416impl std::ops::Sub for GitSummary {
417    type Output = GitSummary;
418
419    fn sub(self, rhs: Self) -> Self::Output {
420        GitSummary {
421            index: self.index - rhs.index,
422            worktree: self.worktree - rhs.worktree,
423            conflict: self.conflict - rhs.conflict,
424            untracked: self.untracked - rhs.untracked,
425            count: self.count - rhs.count,
426        }
427    }
428}
429
430#[derive(Clone, Debug)]
431pub struct GitStatus {
432    pub entries: Arc<[(RepoPath, FileStatus)]>,
433}
434
435impl FromStr for GitStatus {
436    type Err = anyhow::Error;
437
438    fn from_str(s: &str) -> Result<Self> {
439        let mut entries = s
440            .split('\0')
441            .filter_map(|entry| {
442                let sep = entry.get(2..3)?;
443                if sep != " " {
444                    return None;
445                };
446                let path = &entry[3..];
447                // The git status output includes untracked directories as well as untracked files.
448                // We do our own processing to compute the "summary" status of each directory,
449                // so just skip any directories in the output, since they'll otherwise interfere
450                // with our handling of nested repositories.
451                if path.ends_with('/') {
452                    return None;
453                }
454                let status = entry.as_bytes()[0..2].try_into().unwrap();
455                let status = FileStatus::from_bytes(status).log_err()?;
456                // git-status outputs `/`-delimited repo paths, even on Windows.
457                let path = RepoPath::from_rel_path(RelPath::unix(path).log_err()?);
458                Some((path, status))
459            })
460            .collect::<Vec<_>>();
461        entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
462        // When a file exists in HEAD, is deleted in the index, and exists again in the working copy,
463        // git produces two lines for it, one reading `D ` (deleted in index, unmodified in working copy)
464        // and the other reading `??` (untracked). Merge these two into the equivalent of `DA`.
465        entries.dedup_by(|(a, a_status), (b, b_status)| {
466            const INDEX_DELETED: FileStatus = FileStatus::index(StatusCode::Deleted);
467            if a.ne(&b) {
468                return false;
469            }
470            match (*a_status, *b_status) {
471                (INDEX_DELETED, FileStatus::Untracked) | (FileStatus::Untracked, INDEX_DELETED) => {
472                    *b_status = TrackedStatus {
473                        index_status: StatusCode::Deleted,
474                        worktree_status: StatusCode::Added,
475                    }
476                    .into();
477                }
478                (x, y) if x == y => {}
479                _ => {
480                    log::warn!(
481                        "Unexpected duplicated status entries: {a_status:?} and {b_status:?}"
482                    );
483                }
484            }
485            true
486        });
487        Ok(Self {
488            entries: entries.into(),
489        })
490    }
491}
492
493impl Default for GitStatus {
494    fn default() -> Self {
495        Self {
496            entries: Arc::new([]),
497        }
498    }
499}
500
501pub enum DiffTreeType {
502    MergeBase {
503        base: SharedString,
504        head: SharedString,
505    },
506    Since {
507        base: SharedString,
508        head: SharedString,
509    },
510}
511
512impl DiffTreeType {
513    pub fn base(&self) -> &SharedString {
514        match self {
515            DiffTreeType::MergeBase { base, .. } => base,
516            DiffTreeType::Since { base, .. } => base,
517        }
518    }
519
520    pub fn head(&self) -> &SharedString {
521        match self {
522            DiffTreeType::MergeBase { head, .. } => head,
523            DiffTreeType::Since { head, .. } => head,
524        }
525    }
526}
527
528#[derive(Debug, PartialEq)]
529pub struct TreeDiff {
530    pub entries: HashMap<RepoPath, TreeDiffStatus>,
531}
532
533#[derive(Debug, Clone, PartialEq)]
534pub enum TreeDiffStatus {
535    Added,
536    Modified { old: Oid },
537    Deleted { old: Oid },
538}
539
540impl FromStr for TreeDiff {
541    type Err = anyhow::Error;
542
543    fn from_str(s: &str) -> Result<Self> {
544        let mut fields = s.split('\0');
545        let mut parsed = HashMap::default();
546        while let Some((status, path)) = fields.next().zip(fields.next()) {
547            let path = RepoPath::from_rel_path(RelPath::unix(path)?);
548
549            let mut fields = status.split(" ").skip(2);
550            let old_sha = fields
551                .next()
552                .ok_or_else(|| anyhow!("expected to find old_sha"))?
553                .to_owned()
554                .parse()?;
555            let _new_sha = fields
556                .next()
557                .ok_or_else(|| anyhow!("expected to find new_sha"))?;
558            let status = fields
559                .next()
560                .and_then(|s| {
561                    if s.len() == 1 {
562                        s.as_bytes().first()
563                    } else {
564                        None
565                    }
566                })
567                .ok_or_else(|| anyhow!("expected to find status"))?;
568
569            let result = match StatusCode::from_byte(*status)? {
570                StatusCode::Modified => TreeDiffStatus::Modified { old: old_sha },
571                StatusCode::Added => TreeDiffStatus::Added,
572                StatusCode::Deleted => TreeDiffStatus::Deleted { old: old_sha },
573                _status => continue,
574            };
575
576            parsed.insert(path, result);
577        }
578
579        Ok(Self { entries: parsed })
580    }
581}
582
583#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
584pub struct DiffStat {
585    pub added: u32,
586    pub deleted: u32,
587}
588
589#[derive(Clone, Debug)]
590pub struct GitDiffStat {
591    pub entries: Arc<[(RepoPath, DiffStat)]>,
592}
593
594/// Parses the output of `git diff --numstat` where output looks like:
595///
596/// ```text
597/// 24   12   dir/file.txt
598/// ```
599pub fn parse_numstat(output: &str) -> GitDiffStat {
600    let mut entries = Vec::new();
601    for line in output.lines() {
602        let line = line.trim();
603        if line.is_empty() {
604            continue;
605        }
606        let mut parts = line.splitn(3, '\t');
607        let (Some(added_str), Some(deleted_str), Some(path_str)) =
608            (parts.next(), parts.next(), parts.next())
609        else {
610            continue;
611        };
612        let Ok(added) = added_str.parse::<u32>() else {
613            continue;
614        };
615        let Ok(deleted) = deleted_str.parse::<u32>() else {
616            continue;
617        };
618        let Ok(path) = RepoPath::new(path_str) else {
619            continue;
620        };
621        entries.push((path, DiffStat { added, deleted }));
622    }
623    entries.sort_by(|(a, _), (b, _)| a.cmp(b));
624    entries.dedup_by(|(a, _), (b, _)| a == b);
625
626    GitDiffStat {
627        entries: entries.into(),
628    }
629}
630
631#[cfg(test)]
632mod tests {
633
634    use crate::{
635        repository::RepoPath,
636        status::{FileStatus, GitStatus, TreeDiff, TreeDiffStatus},
637    };
638
639    use super::{DiffStat, parse_numstat};
640
641    fn lookup<'a>(entries: &'a [(RepoPath, DiffStat)], path: &str) -> Option<&'a DiffStat> {
642        let path = RepoPath::new(path).unwrap();
643        entries.iter().find(|(p, _)| p == &path).map(|(_, s)| s)
644    }
645
646    #[test]
647    fn test_parse_numstat_normal() {
648        let input = "10\t5\tsrc/main.rs\n3\t1\tREADME.md\n";
649        let result = parse_numstat(input);
650        assert_eq!(result.entries.len(), 2);
651        assert_eq!(
652            lookup(&result.entries, "src/main.rs"),
653            Some(&DiffStat {
654                added: 10,
655                deleted: 5
656            })
657        );
658        assert_eq!(
659            lookup(&result.entries, "README.md"),
660            Some(&DiffStat {
661                added: 3,
662                deleted: 1
663            })
664        );
665    }
666
667    #[test]
668    fn test_parse_numstat_binary_files_skipped() {
669        // git diff --numstat outputs "-\t-\tpath" for binary files
670        let input = "-\t-\timage.png\n5\t2\tsrc/lib.rs\n";
671        let result = parse_numstat(input);
672        assert_eq!(result.entries.len(), 1);
673        assert!(lookup(&result.entries, "image.png").is_none());
674        assert_eq!(
675            lookup(&result.entries, "src/lib.rs"),
676            Some(&DiffStat {
677                added: 5,
678                deleted: 2
679            })
680        );
681    }
682
683    #[test]
684    fn test_parse_numstat_empty_input() {
685        assert!(parse_numstat("").entries.is_empty());
686        assert!(parse_numstat("\n\n").entries.is_empty());
687        assert!(parse_numstat("   \n  \n").entries.is_empty());
688    }
689
690    #[test]
691    fn test_parse_numstat_malformed_lines_skipped() {
692        let input = "not_a_number\t5\tfile.rs\n10\t5\tvalid.rs\n";
693        let result = parse_numstat(input);
694        assert_eq!(result.entries.len(), 1);
695        assert_eq!(
696            lookup(&result.entries, "valid.rs"),
697            Some(&DiffStat {
698                added: 10,
699                deleted: 5
700            })
701        );
702    }
703
704    #[test]
705    fn test_parse_numstat_incomplete_lines_skipped() {
706        // Lines with fewer than 3 tab-separated fields are skipped
707        let input = "10\t5\n7\t3\tok.rs\n";
708        let result = parse_numstat(input);
709        assert_eq!(result.entries.len(), 1);
710        assert_eq!(
711            lookup(&result.entries, "ok.rs"),
712            Some(&DiffStat {
713                added: 7,
714                deleted: 3
715            })
716        );
717    }
718
719    #[test]
720    fn test_parse_numstat_zero_stats() {
721        let input = "0\t0\tunchanged_but_present.rs\n";
722        let result = parse_numstat(input);
723        assert_eq!(
724            lookup(&result.entries, "unchanged_but_present.rs"),
725            Some(&DiffStat {
726                added: 0,
727                deleted: 0
728            })
729        );
730    }
731
732    #[test]
733    fn test_duplicate_untracked_entries() {
734        // Regression test for ZED-2XA: git can produce duplicate untracked entries
735        // for the same path. This should deduplicate them instead of panicking.
736        let input = "?? file.txt\0?? file.txt";
737        let status: GitStatus = input.parse().unwrap();
738        assert_eq!(status.entries.len(), 1);
739        assert_eq!(status.entries[0].1, FileStatus::Untracked);
740    }
741
742    #[test]
743    fn test_tree_diff_parsing() {
744        let input = ":000000 100644 0000000000000000000000000000000000000000 0062c311b8727c3a2e3cd7a41bc9904feacf8f98 A\x00.zed/settings.json\x00".to_owned() +
745            ":100644 000000 bb3e9ed2e97a8c02545bae243264d342c069afb3 0000000000000000000000000000000000000000 D\x00README.md\x00" +
746            ":100644 100644 42f097005a1f21eb2260fad02ec8c991282beee8 a437d85f63bb8c62bd78f83f40c506631fabf005 M\x00parallel.go\x00";
747
748        let output: TreeDiff = input.parse().unwrap();
749        assert_eq!(
750            output,
751            TreeDiff {
752                entries: [
753                    (
754                        RepoPath::new(".zed/settings.json").unwrap(),
755                        TreeDiffStatus::Added,
756                    ),
757                    (
758                        RepoPath::new("README.md").unwrap(),
759                        TreeDiffStatus::Deleted {
760                            old: "bb3e9ed2e97a8c02545bae243264d342c069afb3".parse().unwrap()
761                        }
762                    ),
763                    (
764                        RepoPath::new("parallel.go").unwrap(),
765                        TreeDiffStatus::Modified {
766                            old: "42f097005a1f21eb2260fad02ec8c991282beee8".parse().unwrap(),
767                        }
768                    ),
769                ]
770                .into_iter()
771                .collect()
772            }
773        )
774    }
775}