buntrail-services 0.2.0

Filesystem, Git, terminal, search and connection services.
Documentation
use super::{LocalGitService, process, repository::oid};
use crate::files::io_error;
use buntrail_core::contracts::*;
use std::{ffi::OsString, os::unix::ffi::OsStrExt, sync::atomic::AtomicBool};

impl LocalGitService {
    pub fn commit_sync(
        &self,
        query: CommitQuery,
        cancel: &AtomicBool,
    ) -> Result<CommitPage, CoreError> {
        let id = oid(query.oid.as_bytes())?;
        let root = self.validate(&query.worktree, cancel)?;
        let mut args = process::args(&[
            "show",
            "--no-show-signature",
            "-s",
            "--format=%H%x00%P%x00%an%x00%ae%x00%aI%x00%B%x00",
        ]);
        args.push(id.clone().into());
        args.push("--".into());
        let raw = self.command(&root, &args, cancel)?;
        let fields = raw.split(|b| *b == 0).collect::<Vec<_>>();
        if fields.len() != 7 {
            return Err(io_error("Invalid commit metadata"));
        }
        let parents = fields[1]
            .split(|b| *b == b' ')
            .filter(|v| !v.is_empty())
            .map(oid)
            .collect::<Result<Vec<_>, _>>()?;
        let mut args = process::args(&[
            "diff-tree",
            "--root",
            "--no-commit-id",
            "--name-status",
            "-r",
            "-z",
            "-M",
        ]);
        if let Some(parent) = parents.first() {
            args.push(parent.into());
        }
        args.push(id.clone().into());
        args.push("--".into());
        let raw = self.command(&root, &args, cancel)?;
        if !raw.is_empty() && !raw.ends_with(&[0]) {
            return Err(io_error("Incomplete commit files"));
        }
        let mut records = raw.split(|b| *b == 0);
        let mut files = vec![];
        let path = |bytes: &[u8]| -> Result<TargetPath, CoreError> {
            if bytes.is_empty()
                || bytes.starts_with(b"/")
                || bytes.split(|b| *b == b'/').any(|p| p == b".." || p == b".")
            {
                return Err(io_error("Invalid commit path"));
            }
            TargetPath::posix([root.as_os_str().as_bytes(), b"/", bytes].concat())
        };
        while let Some(status) = records.next() {
            if status.is_empty() {
                break;
            }
            let first = records
                .next()
                .ok_or_else(|| io_error("Incomplete commit path"))?;
            let (change, previous_path, p) = match status[0] {
                b'R' | b'C' => (
                    DiffChange::Renamed,
                    Some(path(first)?),
                    records
                        .next()
                        .ok_or_else(|| io_error("Missing rename target"))?,
                ),
                b'A' => (DiffChange::Added, None, first),
                b'D' => (DiffChange::Deleted, None, first),
                b'M' | b'T' => (DiffChange::Modified, None, first),
                _ => return Err(io_error("Invalid commit change")),
            };
            files.push(GitCommitFile {
                path: path(p)?,
                previous_path,
                change,
            });
        }
        let offset = usize::try_from(query.cursor.unwrap_or(0)).map_err(io_error)?;
        if offset > files.len() {
            return Err(io_error("Invalid commit file cursor"));
        }
        let end = (offset + 200).min(files.len());
        Ok(CommitPage {
            oid: oid(fields[0])?,
            parents,
            author: String::from_utf8_lossy(fields[2]).into_owned(),
            email: String::from_utf8_lossy(fields[3]).into_owned(),
            authored_at: String::from_utf8_lossy(fields[4]).into_owned(),
            message: String::from_utf8_lossy(fields[5]).into_owned(),
            next_cursor: if end < files.len() {
                Some(end as u64)
            } else {
                None
            },
            files: files[offset..end].to_vec(),
        })
    }
    pub fn commit_diff_sync(
        &self,
        request: DiffRequest,
        cancel: &AtomicBool,
    ) -> Result<DiffSnapshot, CoreError> {
        let DiffSource::Commit(id) = &request.source else {
            return Err(io_error("Expected commit source"));
        };
        let root = self.validate(&request.worktree, cancel)?;
        let mut query = CommitQuery {
            worktree: request.worktree.clone(),
            oid: id.clone(),
            cursor: None,
        };
        let (metadata, file) = loop {
            let page = self.commit_sync(query.clone(), cancel)?;
            if let Some(file) = page.files.iter().find(|f| f.path == request.path).cloned() {
                break (page, file);
            }
            match page.next_cursor {
                Some(cursor) => query.cursor = Some(cursor),
                None => return Err(io_error("File is not changed in this commit")),
            }
        };
        let mut args = process::args(if metadata.parents.is_empty() {
            &["diff-tree", "--root", "--no-commit-id", "-p"][..]
        } else {
            &["diff"][..]
        });
        args.extend(process::args(&[
            "--no-ext-diff",
            "--no-textconv",
            "--no-color",
            "--binary",
            "-M",
        ]));
        if let Some(parent) = metadata.parents.first() {
            args.push(parent.into());
        }
        args.push(metadata.oid.into());
        args.push("--".into());
        for path in std::iter::once(&file.path).chain(file.previous_path.iter()) {
            let raw = crate::files::path(path)?;
            let relative = raw.strip_prefix(&root).map_err(io_error)?;
            args.push(OsString::from(relative));
        }
        let output = process::run(&root, &args, cancel)?;
        let content = if output.interrupted {
            DiffContent::Unavailable(
                "Diff exceeded its output/deadline bound or was cancelled".into(),
            )
        } else if !output.success {
            DiffContent::Unavailable(output.diagnostic())
        } else if output
            .stdout
            .split(|b| *b == b'\n')
            .any(|v| v == b"GIT binary patch")
        {
            DiffContent::Binary
        } else if output
            .stdout
            .split(|b| *b == b'\n')
            .any(|v| v.starts_with(b"index ") && v.ends_with(b" 160000"))
        {
            DiffContent::Unavailable("Submodule commit changed".into())
        } else {
            DiffContent::Text
        };
        let bytes = if content == DiffContent::Text {
            output.stdout
        } else {
            vec![]
        };
        Ok(DiffSnapshot {
            source: request.source,
            content,
            change: file.change,
            previous_path: file.previous_path,
            expected: self.state(&root, cancel)?,
            bytes,
        })
    }
}