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
//! A command line tool to calculate the hits-of-code metric in a source code repository using Git.
//!
//! You can read more about hits-of-code metric in this blog post:
//! [Hits-of-Code Instead of SLoC][blog_post].
//!
//! Based on the [Ruby version by Yegor Bugayenko][ruby_version].
//!
//! [blog_post]: https://www.yegor256.com/2014/11/14/hits-of-code.html
//! [ruby_version]: https://github.com/yegor256/hoc
use std::{process, process::Command, str};

const COMMAND_ARGUMENTS: &'static [&'static str] = &[
    "log",
    "--pretty=tformat:",
    "--numstat",
    "--ignore-space-change",
    "--ignore-all-space",
    "--ignore-submodules",
    "--no-color",
    "--diff-filter=ACDM",
];

pub fn run(find_renames_and_copies: bool) {
    let mut extra_command_arguments: &'static [&'static str] = &[];
    // Enabling this option causes the git command to be significantly slower.
    if find_renames_and_copies {
        // From the git-log man page:
        //
        // -M[<n>], --find-renames[=<n>]
        //     If generating diffs, detect and report renames for each commit. For following files
        //     across renames while traversing history, see --follow. If n is specified, it is a
        //     threshold on the similarity index (i.e. amount of addition/deletions compared to the
        //     file's size). For example, -M90% means Git should consider a delete/add pair to be a
        //     rename if more than 90% of the file hasn't changed. Without a % sign, the number is
        //     to be read as a fraction, with a decimal point before it. I.e., -M5 becomes 0.5, and
        //     is thus the same as -M50%. Similarly, -M05 is the same as -M5%. To limit detection
        //     to exact renames, use -M100%. The default similarity index is 50%.
        //
        // -C[<n>], --find-copies[=<n>]
        //     Detect copies as well as renames. See also --find-copies-harder. If n is specified,
        //     it has the same meaning as for -M<n>.
        //
        // --find-copies-harder
        //     For performance reasons, by default, -C option finds copies only if the original
        //     file of the copy was modified in the same changeset. This flag makes the command
        //     inspect unmodified files as candidates for the source of copy. This is a very
        //     expensive operation for large projects, so use it with caution. Giving more than one
        //     -C option has the same effect.
        extra_command_arguments = &["--find-renames", "--find-copies-harder"];
    }

    let output = Command::new("git")
        .args([COMMAND_ARGUMENTS, extra_command_arguments].concat())
        .output()
        .expect("git command failed");

    // Output format (whitespace formatting added for readability):
    //
    // 21   \t  0   \t  .gitignore  \n
    // 7    \t  11  \t  Cargo.toml  \n
    // 3    \t  0   \t  src/main.rs \n
    if output.status.success() {
        let mut total: usize = 0;
        let mut num: Vec<u8> = vec![];
        let mut in_word = false;

        for c in output.stdout.iter() {
            match c {
                b'\t' => {
                    if !num.is_empty() {
                        total += integer_from_slice(num.as_slice()) as usize;
                        num.clear();
                    }
                }
                b'\n' => in_word = false,
                b'0'...b'9' => {
                    if !in_word {
                        num.push(*c);
                    }
                }
                _ => in_word = true,
            }
        }
        println!("{}", total);
    } else {
        if let Some(code) = output.status.code() {
            eprintln!("git command exited with status {}", code)
        } else {
            eprintln!("git command killed with a signal")
        }
        process::exit(1);
    }
}

fn integer_from_slice(slice: &[u8]) -> usize {
    let n: usize = str::from_utf8(slice)
        .expect("not a string")
        .parse()
        .expect("not a number");
    n
}