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}