async-git 0.0.0-squat-name

Pure-rust async implementation of git built on tokio
Documentation
use std::env;
use std::path::{Path, PathBuf};
use std::str::FromStr;

use tokio::fs;

use crate::errors::{Error, Result};
use crate::plumbing::{Object, Oid, Ref};
use crate::util;

pub struct Repository {
    pub path: PathBuf,
}

impl Repository {
    /// Opens a git repository
    pub async fn open(path: impl AsRef<Path>) -> Result<Self> {
        let path = path.as_ref().to_path_buf();
        if !Repository::is_repo(&path).await? {
            return Err(Error::PathIsNotRepository(path));
        }
        Ok(Repository { path })
    }

    /// Get the head ref
    pub async fn head(&self) -> Result<Ref> {
        let head_path = self.path.join("HEAD");
        let head_ref = util::file_to_string(head_path).await?;
        Ok(Ref::parse(head_ref.trim())?)
    }

    /// Checks if the given path points to a git repository
    pub async fn is_repo(path: impl AsRef<Path>) -> Result<bool> {
        let path = path.as_ref();

        // if it's not a directory, obviously not
        if !path.is_dir() {
            return Ok(false);
        }

        // look for HEAD. there's 2 possibilities:
        // - HEAD is a symlink or a regular file and contains a "ref:" ref
        // - HEAD is a regular file and contains an OID (detached)
        let head_path = path.join("HEAD");
        if !head_path.exists() {
            return Ok(false);
        }
        let contents = util::file_to_string(&head_path).await?;
        if contents.starts_with("ref:") {
            // check the first case first
            let rest = contents.trim_start_matches("ref:").trim();
            if !rest.starts_with("refs/") {
                return Ok(false);
            }
        } else {
            // now try parsing as oid
            let contents = contents.trim();
            if !Oid::from_str(&contents).is_ok() {
                return Ok(false);
            }
            // at this point, we know it's an oid. reject if it's also a symlink
            if fs::read_link(&head_path).await.is_ok() {
                return Ok(false);
            }
        }

        // look for the objects directory
        let objects_path = match env::var("GIT_OBJECT_DIRECTORY") {
            Ok(path) => PathBuf::from(path),
            Err(_) => path.join("objects"),
        };
        if !objects_path.exists() {
            return Ok(false);
        }

        // look for the refs directory
        let refs_path = path.join("refs");
        if !refs_path.exists() {
            return Ok(false);
        }

        Ok(true)
    }

    /// Find the closest repository that the current repository belongs to.
    pub async fn find() -> Result<Option<PathBuf>> {
        let mut cwd = env::current_dir()?;

        // 3 options for each directory level:
        // - .git file (contains "gitdir: <path>")
        // - .git/ directory
        // - ./ (bare repo)
        loop {
            let dot_git = cwd.join(".git");
            if dot_git.exists() {
                if dot_git.is_file() {
                    let contents = util::file_to_string(&dot_git).await?;
                    if contents.starts_with("gitdir:") {
                        let path = PathBuf::from(contents.trim_start_matches("gitdir:").trim());
                        if Repository::is_repo(&path).await? {
                            return Ok(Some(path));
                        }
                    }
                } else if dot_git.is_dir() {
                    if Repository::is_repo(&dot_git).await? {
                        return Ok(Some(dot_git));
                    }
                }
            } else {
                if Repository::is_repo(&cwd).await? {
                    return Ok(Some(cwd));
                }
            }

            if Repository::is_repo(cwd.clone()).await? {
                return Ok(Some(cwd));
            }

            cwd = match cwd.parent() {
                Some(parent) => parent.to_path_buf(),
                None => return Ok(None),
            };
        }
    }

    pub fn get_object(&self, id: Oid) -> Object {
        Object {
            repo_path: self.path.clone(),
            id,
        }
    }
}