pijul 0.12.2

A patch-based distributed version control system, easy to use and fast. Command-line interface.
use clap::{Arg, ArgMatches, SubCommand};
use commands::patch::print_patch;
use commands::{ask, default_explain, BasicOptions, StaticSubcommand};
use error::Error;
use libpijul::fs_representation::RepoPath;
use libpijul::patch::Patch;
use libpijul::{Branch, PatchId, Txn};
use regex::Regex;
use std::fs::File;
use std::io::{BufReader, Read};
use std::path::PathBuf;
use term;

pub fn invocation() -> StaticSubcommand {
    SubCommand::with_name("log")
        .about("List the patches applied to the given branch")
        .arg(
            Arg::with_name("repository")
                .long("repository")
                .help("Path to the repository to list.")
                .takes_value(true),
        )
        .arg(
            Arg::with_name("branch")
                .long("branch")
                .help("The branch to list.")
                .takes_value(true),
        )
        .arg(
            Arg::with_name("internal-id")
                .long("internal-id")
                .help("Display only patches with these internal identifiers.")
                .multiple(true)
                .takes_value(true),
        )
        .arg(
            Arg::with_name("hash-only")
                .long("hash-only")
                .help("Only display the hash of each path."),
        )
        .arg(
            Arg::with_name("repository-id")
                .long("repository-id")
                .help("display a header with the repository id")
        )
        .arg(
            Arg::with_name("path")
                .long("path")
                .multiple(true)
                .takes_value(true)
                .help("Only display patches that touch the given path."),
        )
        .arg(
            Arg::with_name("grep")
                .long("grep")
                .multiple(true)
                .takes_value(true)
                .help("Search patch name and description with a regular expression."),
        )
        .arg(
            Arg::with_name("last")
                .long("last")
                .takes_value(true)
                .help("Show only the last n patches. If `--first m` is also used, then (a) if the command normally outputs the last patches first, this means the last n patches of the first m ones. (b) Else, it means the first m patches of the last n ones."),
        )
        .arg(
            Arg::with_name("first")
                .long("first")
                .takes_value(true)
                .help("Show only the last n patches. If `--last m` is also used, then (a) if the command normally outputs the last patches first, this means the last m patches of the first n ones. (b) Else, it means the first n patches of the last m ones."),
        )
        .arg(
            Arg::with_name("patch")
                .long("patch")
                .short("p")
                .help("Show patches"),
        )
}

struct Settings<'a> {
    hash_only: bool,
    show_repoid: bool,
    show_patches: bool,
    regex: Vec<Regex>,
    opts: BasicOptions<'a>,
    path: Vec<RepoPath<PathBuf>>,
    first: Option<usize>,
    last: Option<usize>,
}

impl<'a> Settings<'a> {
    fn parse(args: &'a ArgMatches) -> Result<Self, Error> {
        let basic_opts = BasicOptions::from_args(args)?;
        let hash_only = args.is_present("hash-only");
        let first = args.value_of("first").and_then(|x| x.parse().ok());
        let last = args.value_of("last").and_then(|x| x.parse().ok());
        let show_patches = args.is_present("patch");
        let show_repoid = args.is_present("repository-id");
        let mut regex = Vec::new();
        if let Some(regex_args) = args.values_of("grep") {
            for r in regex_args {
                debug!("regex: {:?}", r);
                regex.push(Regex::new(r)?)
            }
        }
        let path = match args.values_of("path") {
            Some(arg_paths) => {
                let mut paths = Vec::new();
                for path in arg_paths {
                    let p = basic_opts.cwd.join(path);
                    let p = if let Ok(p) = std::fs::canonicalize(&p) {
                        p
                    } else {
                        p
                    };
                    paths.push(basic_opts.repo_root.relativize(&p)?.to_owned());
                }
                paths
            }
            None => Vec::new(),
        };
        Ok(Settings {
            hash_only,
            show_patches,
            show_repoid,
            regex,
            opts: basic_opts,
            path,
            first,
            last,
        })
    }
}

impl<'a> Settings<'a> {
    fn display_patch_(
        &self,
        txn: &Txn,
        branch: &Branch,
        nth: u64,
        patchid: PatchId,
    ) -> Result<(), Error> {
        let hash_ext = txn.get_external(patchid).unwrap();
        debug!("hash: {:?}", hash_ext.to_base58());

        let (matches_regex, o_patch) = if self.regex.is_empty() {
            (true, None)
        } else {
            let patch = self.opts.repo_root.read_patch_nochanges(hash_ext)?;
            let does_match = {
                let descr = match patch.description {
                    Some(ref d) => d,
                    None => "",
                };
                self.regex
                    .iter()
                    .any(|ref r| r.is_match(&patch.name) || r.is_match(descr))
            };
            (does_match, Some(patch))
        };
        if !matches_regex {
            return Ok(());
        };

        if self.hash_only {
            println!("{}:{}", hash_ext.to_base58(), nth);
        } else {
            let patch = match o_patch {
                None => self.opts.repo_root.read_patch_nochanges(hash_ext)?,
                Some(patch) => patch,
            };
            let mut term = if atty::is(atty::Stream::Stdout) {
                term::stdout()
            } else {
                None
            };
            ask::print_patch_descr(&mut term, &hash_ext.to_owned(), Some(patchid), &patch);
        }

        if self.show_patches {
            let mut patch_path = self.opts.repo_root.patches_dir().join(hash_ext.to_base58());
            patch_path.set_extension("gz");
            let f = File::open(&patch_path)?;

            let mut f = BufReader::new(f);
            let (hash, _, patch) = Patch::from_reader_compressed(&mut f)?;

            print_patch(&hash, &patch, txn, branch)?;
            println!();
        }

        Ok(())
    }

    fn display_patch(
        &self,
        txn: &Txn,
        branch: &Branch,
        n: u64,
        patchid: PatchId,
    ) -> Result<(), Error> {
        if self.path.is_empty() {
            self.display_patch_(txn, branch, n, patchid)?;
        } else {
            for path in self.path.iter() {
                let inode = txn.find_inode(&path)?;
                let key = if let Some(key) = txn.get_inodes(inode) {
                    key.key
                } else {
                    continue;
                };
                if txn.get_touched(key, patchid) {
                    self.display_patch_(txn, branch, n, patchid)?;
                    break;
                }
            }
        }
        Ok(())
    }

    fn is_touched(&self, txn: &Txn, patchid: PatchId) -> bool {
        self.path.is_empty()
            || self.path.iter().any(|path| {
                if let Ok(inode) = txn.find_inode(&path) {
                    if let Some(key) = txn.get_inodes(inode) {
                        return txn.get_touched(key.key, patchid);
                    }
                }
                false
            })
    }
}

pub fn run(args: &ArgMatches) -> Result<(), Error> {
    let settings = Settings::parse(args)?;
    let repo = settings.opts.open_repo()?;
    let txn = repo.txn_begin()?;
    let branch = match txn.get_branch(&settings.opts.branch()) {
        Some(b) => b,
        None => return Err(Error::NoSuchBranch),
    };

    if settings.show_repoid {
        let id_file = settings.opts.repo_root.id_file();
        let mut f = File::open(&id_file)?;
        let mut s = String::new();
        f.read_to_string(&mut s)?;
        if settings.hash_only {
            println!("{}", s.trim());
        } else {
            println!("Repository id: {}", s.trim());
            println!();
        }
    };
    if settings.hash_only {
        // If in binary form, show the patches in chronological order.
        let start = settings.last.and_then(|last| {
            txn.rev_iter_applied(&branch, None)
                .filter(|(_, patchid)| {
                    // Only select patches that touch the input path
                    // (if that path exists).
                    settings.is_touched(&txn, *patchid)
                })
                .take(last)
                .last()
                .map(|(n, _)| n)
        });
        debug!("start {:?}", start);
        for (n, (applied, patchid)) in txn.iter_applied(&branch, start).enumerate() {
            if let Some(first) = settings.first {
                if n >= first {
                    break;
                }
            }
            settings.display_patch(&txn, &branch, applied, patchid)?
        }
        return Ok(());
    }

    let txn = repo.txn_begin()?;
    if let Some(v) = args.values_of("internal-id") {
        for (n, patchid) in v.filter_map(|x| PatchId::from_base58(x)).enumerate() {
            settings.display_patch(&txn, &branch, n as u64, patchid)?;
        }
    } else {
        let first = if let Some(first) = settings.first {
            txn.iter_applied(&branch, None)
                .filter(|(_, patchid)| settings.is_touched(&txn, *patchid))
                .take(first)
                .last()
                .map(|(n, _)| n)
        } else {
            None
        };
        for (n, (applied, patchid)) in txn.rev_iter_applied(&branch, first).enumerate() {
            if let Some(last) = settings.last {
                if n >= last {
                    break;
                }
            }
            settings.display_patch(&txn, &branch, applied, patchid)?;
        }
    }
    Ok(())
}

pub fn explain(r: Result<(), Error>) {
    default_explain(r)
}