hoc/
lib.rs

1//! A command line tool to calculate the hits-of-code metric in a source code repository using Git.
2//!
3//! You can read more about hits-of-code metric in this blog post:
4//! [Hits-of-Code Instead of SLoC][blog_post].
5//!
6//! Based on the [Ruby version by Yegor Bugayenko][ruby_version].
7//!
8//! [blog_post]: https://www.yegor256.com/2014/11/14/hits-of-code.html
9//! [ruby_version]: https://github.com/yegor256/hoc
10use std::{process, process::Command, str};
11
12const COMMAND_ARGUMENTS: &'static [&'static str] = &[
13    "log",
14    "--pretty=tformat:",
15    "--numstat",
16    "--ignore-space-change",
17    "--ignore-all-space",
18    "--ignore-submodules",
19    "--no-color",
20    "--diff-filter=ACDM",
21];
22
23pub fn run(find_renames_and_copies: bool) {
24    let mut extra_command_arguments: &'static [&'static str] = &[];
25    // Enabling this option causes the git command to be significantly slower.
26    if find_renames_and_copies {
27        // From the git-log man page:
28        //
29        // -M[<n>], --find-renames[=<n>]
30        //     If generating diffs, detect and report renames for each commit. For following files
31        //     across renames while traversing history, see --follow. If n is specified, it is a
32        //     threshold on the similarity index (i.e. amount of addition/deletions compared to the
33        //     file's size). For example, -M90% means Git should consider a delete/add pair to be a
34        //     rename if more than 90% of the file hasn't changed. Without a % sign, the number is
35        //     to be read as a fraction, with a decimal point before it. I.e., -M5 becomes 0.5, and
36        //     is thus the same as -M50%. Similarly, -M05 is the same as -M5%. To limit detection
37        //     to exact renames, use -M100%. The default similarity index is 50%.
38        //
39        // -C[<n>], --find-copies[=<n>]
40        //     Detect copies as well as renames. See also --find-copies-harder. If n is specified,
41        //     it has the same meaning as for -M<n>.
42        //
43        // --find-copies-harder
44        //     For performance reasons, by default, -C option finds copies only if the original
45        //     file of the copy was modified in the same changeset. This flag makes the command
46        //     inspect unmodified files as candidates for the source of copy. This is a very
47        //     expensive operation for large projects, so use it with caution. Giving more than one
48        //     -C option has the same effect.
49        extra_command_arguments = &["--find-renames", "--find-copies-harder"];
50    }
51
52    let output = Command::new("git")
53        .args([COMMAND_ARGUMENTS, extra_command_arguments].concat())
54        .output()
55        .expect("git command failed");
56
57    // Output format (whitespace formatting added for readability):
58    //
59    // 21   \t  0   \t  .gitignore  \n
60    // 7    \t  11  \t  Cargo.toml  \n
61    // 3    \t  0   \t  src/main.rs \n
62    if output.status.success() {
63        let mut total: usize = 0;
64        let mut num: Vec<u8> = vec![];
65        let mut in_word = false;
66
67        for c in output.stdout.iter() {
68            match c {
69                b'\t' => {
70                    if !num.is_empty() {
71                        total += integer_from_slice(num.as_slice()) as usize;
72                        num.clear();
73                    }
74                }
75                b'\n' => in_word = false,
76                b'0'...b'9' => {
77                    if !in_word {
78                        num.push(*c);
79                    }
80                }
81                _ => in_word = true,
82            }
83        }
84        println!("{}", total);
85    } else {
86        if let Some(code) = output.status.code() {
87            eprintln!("git command exited with status {}", code)
88        } else {
89            eprintln!("git command killed with a signal")
90        }
91        process::exit(1);
92    }
93}
94
95fn integer_from_slice(slice: &[u8]) -> usize {
96    let n: usize = str::from_utf8(slice)
97        .expect("not a string")
98        .parse()
99        .expect("not a number");
100    n
101}