awpack 0.1.0

A package manager for the Awesome tiling window manager
use Error;

use std::path::{Path, PathBuf};
use std::io::prelude::*;
use std::{fs, io, fmt, str};

use toml;
use git2;

#[derive(Debug)]
pub struct Package
{
    pub name: String,
    pub path: PathBuf,
    pub remote_path: PackagePath,

    pub version: String,
    pub revision: String,
    // TODO: keep track of path.
}

#[derive(Debug)]
pub enum PackageError
{
    AlreadyExists,
    NotInstalled,
    NoMetadata,
}

impl Package
{
    pub fn open(remote_path: PackagePath, path: &Path) -> Result<Self, Error> {
        let metadata_file = path.join("package.toml");

        if !metadata_file.exists() {
            return Err(Error::Package(PackageError::NoMetadata));
        }

        let file = fs::File::open(metadata_file)?;
        let metadata_bytes: Result<Vec<u8>, _> = file.bytes().collect();
        let metadata_text = String::from_utf8(metadata_bytes?).unwrap();

        let metadata: toml::Value = metadata_text.parse().unwrap();

        let package_name = metadata.lookup("package.name").unwrap().as_str().unwrap();
        let package_version = metadata.lookup("package.version").unwrap().as_str().unwrap();

        let repository = git2::Repository::open(path).unwrap();
        let revision = format!("{}", repository.head().unwrap().target().unwrap());

        Ok(Package {
            name: package_name.to_owned(),
            path: path.to_owned(),
            remote_path: remote_path,

            version: package_version.to_owned(),
            revision: revision,
        })
    }

    pub fn fetch(&mut self) -> Result<bool, git2::Error> {
        let repository = self.open_repository()?;

        let previous_ref = repository.find_reference("refs/remotes/origin/master").unwrap();

        let remote_name = repository.remotes().unwrap().iter().next().unwrap().unwrap().to_owned();
        let mut remote = repository.find_remote(&remote_name)?;

        remote.connect(git2::Direction::Fetch)?;
        remote.download(&[], None)?;
        remote.disconnect();

        remote.update_tips(None, true,
                           git2::AutotagOption::Unspecified, None)?;

        let new_ref = repository.find_reference("refs/remotes/origin/master").unwrap();

        Ok(previous_ref != new_ref)
    }

    pub fn update(&mut self) -> Result<bool, git2::Error> {
        if self.fetch()? {
            let repository = self.open_repository()?;

            repository.set_head("refs/remotes/origin/master")?;
            repository.checkout_head(Some(git2::build::CheckoutBuilder::new().force()))?;

            self.revision = format!("{}", repository.head().unwrap().target().unwrap());

            Ok(true)
        } else {
            Ok(false)
        }
    }

    pub fn open_repository(&self) -> Result<git2::Repository, git2::Error> {
        git2::Repository::open(&self.path)
    }

    pub fn destroy(self) -> Result<(), io::Error> {
        fs::remove_dir_all(self.path)
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum PackagePath
{
    GitHub {
        organisation: String,
        repository: String,
    },
}

impl PackagePath
{
    pub fn url(&self) -> String {
        match *self {
            PackagePath::GitHub { ref organisation, ref repository } => {
                format!("https://github.com/{}/{}.git", organisation, repository)
            }
        }
    }

    pub fn directory(&self) -> String {
        match *self {
            PackagePath::GitHub { ref organisation, ref repository } => {
                format!("{}/{}", organisation, repository)
            },
        }
    }
}

impl fmt::Display for PackageError
{
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            PackageError::AlreadyExists => write!(fmt, "the package is already installed"),
            PackageError::NoMetadata => write!(fmt, "the package does not contain a package.toml file"),
            PackageError::NotInstalled => write!(fmt, "the package is not installed"),
        }
    }
}

impl fmt::Display for PackagePath
{
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        write!(fmt, "{}", self.directory())
    }
}

impl str::FromStr for PackagePath
{
    type Err = ();

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let parts: Vec<_> = s.split("/").collect();

        if parts.len() == 2 {
            Ok(PackagePath::GitHub {
                organisation: parts[0].to_owned(),
                repository: parts[1].to_owned(),
            })
        } else {
            Err(())
        }
    }
}