1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// Copyright 2017-2018 by Aldrin J D'Souza.
// Licensed under the MIT License <https://opensource.org/licenses/MIT>

// All git interactions
use super::Result;
use std::iter::FromIterator;
use std::process::{Command, Output};

/// Check if we're in an git repository?
pub fn in_git_repository() -> Result<bool> {
    git(&["rev-parse", "--is-inside-work-tree"]).map(|o| o.status.success())
}

/// Get the last tag
pub fn last_tag() -> Result<Option<String>> {
    last_tags(1).map(|mut v| v.pop())
}

/// Get the SHAs for all commits returned by `git log <args>`
pub fn commits_in_log(args: &[String]) -> Result<Vec<String>> {
    let mut log_args = vec!["log", "--format=format:%H"];
    log_args.extend(args.iter().map(String::as_str));
    git(&log_args).map(|o| read_lines(&o))
}

/// Get the commit message for the given sha
pub fn get_commit_message(sha: &str) -> Result<Vec<String>> {
    git(&[
        "log",
        "--format=format:%H%n%an%n%aD%n%s%n%b",
        "--max-count=1",
        sha,
    ]).map(|o| read_lines(&o))
}

/// Get the fetch url for the given origin
pub fn get_remote_url(name: &str) -> Result<Option<String>> {
    git(&["remote", "get-url", name])
        .map(|o| read_lines(&o))
        .map(|mut v: Vec<String>| v.pop().and_then(usable_url))
}

/// Check if the remote URL is usable for links
fn usable_url(raw: String) -> Option<String> {
    if raw.to_lowercase().starts_with("http") {
        if let Some(index) = raw.rfind(".git") {
            return Some(raw[0..index].to_string());
        } else {
            return Some(raw);
        }
    }
    None
}

/// Get the last n tags
fn last_tags(n: i32) -> Result<Vec<String>> {
    git(&[
        "for-each-ref",
        &format!("--count={}", n),
        "--sort=-taggerdate",
        "--format=%(refname:short)",
        "refs/tags/*",
    ]).map(|o| read_lines(&o))
}

/// Invoke a git command with the given arguments.
fn git(args: &[&str]) -> Result<Output> {
    trace!("git {}", args.join(" "));
    let output = Command::new("git").args(args).output()?;
    if output.status.success() {
        Ok(output)
    } else {
        Err(format_err!("{}", String::from_utf8_lossy(&output.stderr)))
    }
}

/// Read the lines from the output and gather them into a String collection.
fn read_lines<T: FromIterator<String>>(o: &Output) -> T {
    String::from_utf8_lossy(&o.stdout)
        .lines()
        .map(String::from)
        .collect::<T>()
}

#[cfg(test)]
mod tests {
    #[test]
    fn in_git_repository() {
        assert!(super::in_git_repository().unwrap());
    }

    #[test]
    fn last_tag() {
        assert!(super::last_tag().is_ok());
    }

    #[test]
    fn commits_in_log() {
        use super::commits_in_log;
        let mut range = vec![String::from("v0.1.1..v0.2.0")];
        let commits = commits_in_log(&range);
        assert!(commits.is_ok(), "{:?}", commits);
        let forward = commits.unwrap();
        assert_eq!(forward.len(), 2);

        // Add a `git log` option
        range.push(String::from("--reverse"));
        let commits = commits_in_log(&range);
        assert!(commits.is_ok(), "{:?}", commits);
        let mut backward = commits.unwrap();
        backward.reverse();
        assert_eq!(backward, forward);
    }

    #[test]
    fn get_commit_message() {
        use super::get_commit_message;
        assert!(get_commit_message("v0.1.1").is_ok());
        assert!(get_commit_message("bad").is_err());
    }

    #[test]
    fn get_usable_url() {
        use super::usable_url;
        let ssh = String::from("git@github.com:aldrin/git-changelog.git");
        let raw = String::from("https://github.com/aldrin/git-changelog.git");
        let usable = "https://github.com/aldrin/git-changelog";
        assert_eq!(usable_url(usable.to_string()), Some(usable.to_string()));
        assert_eq!(usable_url(raw), Some(usable.to_string()));
        assert_eq!(usable_url(ssh), None);
    }

    #[test]
    fn get_remote_url() {
        use super::get_remote_url;
        let expected = Some(String::from("https://github.com/aldrin/git-changelog"));
        let found = get_remote_url("origin").unwrap();
        assert!(get_remote_url("bad").is_err());
        assert_eq!(found, expected);
    }
}