Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
626ceb3
Fix import
mre Feb 18, 2021
975b7fb
Add subcommand for merge
mre Feb 18, 2021
214a2f9
impl Default for Year (generating empty timelines)
mre Feb 18, 2021
5ad51fc
Split up parser into ParseState and core Parser
mre Feb 18, 2021
1fa6d45
Work on merge setup
mre Feb 18, 2021
c48f9bb
Execute `run` by default if there are no additional arguments
mre Feb 18, 2021
38a27da
Update docs
mre Feb 18, 2021
1695eee
Work on merger
mre Feb 18, 2021
598df77
Remove unused TryFrom
mre Feb 18, 2021
0777aa3
Show full year
mre Feb 19, 2021
1b3610f
handle start and end date in output
mre Mar 3, 2021
6149ac9
Fix test
mre Mar 8, 2021
24d6f57
work on merge
mre Mar 8, 2021
6cf9f12
WIP: merge multiple timeslines
woolfg Mar 8, 2021
babfe92
fix merge 2 test
mre Mar 10, 2021
2085384
Fix clippy warnings
mre Mar 10, 2021
1443239
Add hash to timeline
mre Mar 10, 2021
d01be81
fix lints
mre Mar 10, 2021
a48ee27
Less clones
mre Mar 10, 2021
ee203c1
Remove unused import
mre Mar 10, 2021
fa390bc
fix merge call
mre Mar 10, 2021
2a553a2
Fix import
mre Feb 18, 2021
250b815
Add subcommand for merge
mre Feb 18, 2021
27adefe
impl Default for Year (generating empty timelines)
mre Feb 18, 2021
42ca666
Split up parser into ParseState and core Parser
mre Feb 18, 2021
19840f2
Work on merge setup
mre Feb 18, 2021
fac6c44
Execute `run` by default if there are no additional arguments
mre Feb 18, 2021
a239c07
Update docs
mre Feb 18, 2021
a7a68b9
Work on merger
mre Feb 18, 2021
b013988
Remove unused TryFrom
mre Feb 18, 2021
ea60627
Show full year
mre Feb 19, 2021
a5fb023
handle start and end date in output
mre Mar 3, 2021
622c4c6
Fix test
mre Mar 8, 2021
615c0bf
work on merge
mre Mar 8, 2021
37de103
WIP: merge multiple timeslines
woolfg Mar 8, 2021
cfc6076
fix merge 2 test
mre Mar 10, 2021
7cb3063
Fix clippy warnings
mre Mar 10, 2021
fc822a1
Add hash to timeline
mre Mar 10, 2021
452a2e3
fix lints
mre Mar 10, 2021
05af9d2
Less clones
mre Mar 10, 2021
5cc6726
Remove unused import
mre Mar 10, 2021
74fe53a
fix merge call
mre Mar 10, 2021
ca4cac5
txt for help option
woolfg Mar 11, 2021
86202e4
test for intesity computation #5 -> failing
woolfg Mar 11, 2021
5b15954
Merge branch 'merge' of github.com:codeprintsdev/analyzer into merge
mre Mar 15, 2021
6653dca
Add timestamp
mre Mar 15, 2021
231ef0e
formatting
mre Mar 15, 2021
07054b3
changed output of merge to merged_codeprints_*.json
mre Mar 15, 2021
e80175c
formatting
mre Mar 15, 2021
6f4f175
corrected fixture after fixing the api
woolfg Mar 16, 2021
6c51be2
fix typo in intensity string
mre Mar 18, 2021
cdd23c8
Merge remote-tracking branch 'origin/master' into merge
mre Apr 7, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ serde_json = "1.0.62"
chrono = "0.4.19"
quantiles = "0.7.1"
structopt = "0.3.21"
glob = "0.3.0"
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ RUN apt-get update \
COPY --from=builder /analyzer/target/release/codeprints-analyzer /usr/local/bin/codeprints-analyzer
WORKDIR /repo
ENTRYPOINT [ "codeprints-analyzer" ]
# The standard command parses the commits of a repository
CMD ["run"]
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ codeprints-analyzer
You can set the start- and end-date of the output.

```
docker run -v `pwd`:/repo codeprints/analyzer --after "2020-12-24" --before "2021-02-10"
docker run -v `pwd`:/repo codeprints/analyzer run --after "2020-12-24" --before "2021-02-10"
```

The syntax is exactly the same that `git` also uses.
Expand All @@ -53,14 +53,27 @@ If you work in a bigger team, you might want to filter the contributions by
author. Here is how:

```
docker run -v `pwd`:/repo codeprints/analyzer --author "Matthias" --author "Octocat"
docker run -v `pwd`:/repo codeprints/analyzer run --author "Matthias" --author "Octocat"
```

To get a list of all author names, run `git shortlog --summary --numbered --email`.

(You can also filter by committers. The difference is subtle, but in contrast to authors, these are the
contributors who pushed/committed a patch to the repository.)

## Merging multiple codeprints output files

Do you have multiple repositories that you want to analyze?
No problem! Just run the tool in every repository folder.
After that, copy the files into a single folder and run the following command:

```
docker run -v `pwd`:/repo codeprints/analyzer merge
```

This will merge all codeprints\_\*.json files in the `repo` directory into one file.
(It will accumulate all contribution counts for each day.)

## More options

To get an exhaustive list of options, run
Expand Down
66 changes: 53 additions & 13 deletions src/bin/analyzer/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,67 @@
)]

use anyhow::{Context, Result};
use codeprints_analyzer::count_commits;
use codeprints_analyzer::Parser;
use codeprints_analyzer::Timeline;
use codeprints_analyzer::{count_commits, Merger};
use glob::glob;
use std::fs;
use structopt::StructOpt;

mod options;
use options::Opt;
use options::Command;

const OUTPUT_FILE: &'static str = "codeprints.json";
fn write(timeline: &Timeline, output_file: &str) -> Result<()> {
let output = serde_json::to_string_pretty(&timeline)?;
fs::write(output_file, output)?;
println!("done!");
println!("Output file: {}", output_file);
Ok(())
}

fn main() -> Result<()> {
let opt = Opt::from_args();
let opt = Command::from_args();

print!("Analyzing commits in current repository...");
let input = count_commits(opt.before, opt.after, opt.author, opt.committer)
.context("Cannot read project history. Make sure there is no typo in the command")?;
let mut parser = Parser::new(input);
let timeline = parser.parse()?;
let output = serde_json::to_string_pretty(&timeline)?;
fs::write(OUTPUT_FILE, output)?;
println!("done!");
println!("Output file: {}", OUTPUT_FILE);
match opt {
Command::Run {
before,
after,
author,
committer,
} => {
print!("Analyzing commits in current repository...");
let input = count_commits(&before, &after, author, committer).context(
"Cannot read project history. Make sure there is no typo in the command",
)?;
let mut parser = Parser::new(input);
if let Some(before) = before {
parser.set_before(before)?;
}
if let Some(after) = after {
parser.set_after(after)?;
}
let timeline = parser.parse()?;

write(&timeline, "codeprints.json")?;
}
Command::Merge {} => {
// Find all `codeprints*.json` files in the current directory
// using glob.
let mut merger = Merger::new();
for entry in glob("codeprints*.json")? {
match entry {
Ok(path) => {
println!("Merging {}", path.display());
let input = fs::read_to_string(path)?;
let mut parser = Parser::new(input);
let timeline = parser.parse()?;
merger.merge_timeline(&timeline)?;
}
Err(e) => println!("{:?}", e),
}
}
write(&merger.timeline()?, "codeprints_merged.json")?;
}
};
Ok(())
}
37 changes: 20 additions & 17 deletions src/bin/analyzer/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,27 @@ use structopt::StructOpt;

#[derive(StructOpt, Debug)]
#[structopt(name = "codeprints-analyzer")]
pub struct Opt {
/// Limit the commits output to ones with author header lines
/// that match the specified pattern.
/// This is passed verbatim to git. See `git log --help` for more info.
#[structopt(short, long)]
pub author: Vec<String>,
pub enum Command {
Run {
/// Limit the commits output to ones with author header lines
/// that match the specified pattern.
/// This is passed verbatim to git. See `git log --help` for more info.
#[structopt(short, long)]
author: Vec<String>,

/// Limit the commits output to ones with committer header lines
/// that match the specified pattern.
/// This is passed verbatim to git. See `git log --help` for more info.
#[structopt(short, long)]
pub committer: Vec<String>,
/// Limit the commits output to ones with committer header lines
/// that match the specified pattern.
/// This is passed verbatim to git. See `git log --help` for more info.
#[structopt(short, long)]
committer: Vec<String>,

// Show commits older than a specific date.
#[structopt(alias = "until", long)]
pub before: Option<String>,
// Show commits older than a specific date.
#[structopt(alias = "until", long)]
before: Option<String>,

// Show commits more recent than a specific date.
#[structopt(alias = "since", long)]
pub after: Option<String>,
// Show commits more recent than a specific date.
#[structopt(alias = "since", long)]
after: Option<String>,
},
Merge {},
}
17 changes: 14 additions & 3 deletions src/git.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use anyhow::Result;
use anyhow::{Context, Result};
use chrono::NaiveDate;
use duct::cmd;

/// Get the count of commits for each day from the git logs
pub fn count_commits(
before: Option<String>,
after: Option<String>,
before: &Option<String>,
after: &Option<String>,
authors: Vec<String>,
committers: Vec<String>,
) -> Result<String> {
Expand All @@ -28,3 +29,13 @@ pub fn count_commits(
let commits = cmd("git", &args).read()?;
Ok(commits)
}

// Parse a date from the git log
pub fn parse_date(line: &str) -> Result<Option<NaiveDate>> {
if line.trim().is_empty() {
// Empty lines are allowed, but skipped
return Ok(None);
}
let date: NaiveDate = line.parse().context(format!("Invalid date {}", line))?;
Ok(Some(date))
}
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@
)]

mod git;
mod merge;
mod parser;
mod quartiles;
mod types;

pub use crate::git::count_commits;
pub use crate::merge::Merger;
pub use crate::parser::Parser;
pub use crate::types::Timeline as Timeline;
151 changes: 151 additions & 0 deletions src/merge.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
use std::convert::TryFrom;

use crate::{git, parser::ParseState, types::Timeline};
use anyhow::Result;

/// Merger merges multiple timelines into one
#[derive(Debug)]
pub struct Merger {
state: ParseState,
}

impl Merger {
/// Create a new merger with a clean state (no timeline merged)
pub fn new() -> Self {
let state = ParseState::default();
Merger { state }
}

/// Merge a single timeline
pub fn merge_timeline(&mut self, timeline: &Timeline) -> Result<()> {
///TODO: make sure that all days are filled with 0 between start and end
for contribution in &timeline.contributions {
let date = contribution.date.clone();
let date = git::parse_date(&date)?;
let count = contribution.count;

if let Some(date) = date {
self.state.update_years(date, count);
self.state.update_days(date, count);
}
}
Ok(())
}

/// Merge multiple timelines together into one
/// This is helpful when analyzing multiple repositories and trying to combine
/// the individual results.
pub fn merge(&mut self, timelines: &[Timeline]) -> Result<Timeline> {
for timeline in timelines {
self.merge_timeline(timeline)?
}
Ok(Timeline::try_from(&self.state)?)
}

/// Return the merged timeline of all inputs
pub fn timeline(&self) -> Result<Timeline> {
Ok(Timeline::try_from(&self.state)?)
}
}


#[cfg(test)]
mod test {
use super::*;
use crate::types::{Contribution, Range, Year};

#[test]
fn test_merge_none() {
let mut merger = Merger::new();
assert_eq!(merger.merge(&[]).unwrap(), Timeline::default());
}

#[test]
fn test_merge_one() {
let mut timeline = Timeline::default();

let year = "2020".into();
let total = 1234;
let range = Range {
start: "2020-01-01".into(),
end: "2020-01-02".into(),
};

let year1 = Year { year, total, range };
let years = vec![year1];
timeline.years = years;

let contributions = vec![
Contribution {
date: "2020-01-01".into(),
count: 1000,
color: "".into(),
intensity: 4,
},
Contribution {
date: "2020-01-02".into(),
count: 234,
color: "".into(),
intensity: 4,
},
];

timeline.contributions = contributions;

let mut merger = Merger::new();
let merged = merger.merge(&[timeline.clone()]).unwrap();
assert_eq!(merged.years.len(), 1);
let year = &merged.years[0];
assert_eq!(year.year, "2020");
assert_eq!(year.total, 1234);
assert_eq!(year.range.start, "2020-01-01");
assert_eq!(year.range.end, "2020-01-02");
}

#[test]
fn test_merge_multiple() {
let mut timeline1 = Timeline::default();
let mut timeline2 = Timeline::default();

let range1 = Range {
start: "2020-01-01".into(),
end: "2020-01-02".into(),
};
let range2 = Range {
start: "2020-01-01".into(),
end: "2020-01-03".into(),
};

let year2 = Year { year: "2020".into(), total: 0, range: range2 };
let year1 = Year { year: "2020".into(), total: 123, range: range1 };
timeline1.years = vec![year1];
timeline2.years = vec![year2];

let contributions = vec![
Contribution {
date: "2020-01-01".into(),
count: 1000,
color: "".into(),
intensity: 4,
},
Contribution {
date: "2020-01-02".into(),
count: 234,
color: "".into(),
intensity: 4,
},
];

timeline1.contributions = contributions.clone();
timeline2.contributions = contributions.clone();

let mut merger = Merger::new();
let merged = merger.merge(&[timeline1.clone(),timeline2.clone()]).unwrap();
assert_eq!(merged.years.len(), 1);
let year = &merged.years[0];
assert_eq!(year.year, "2020");
assert_eq!(year.total, 2468);
assert_eq!(year.range.start, "2020-01-01");
assert_eq!(year.range.end, "2020-01-03");
}
}
Loading