Skip to content

Commit

Permalink
Added a mode which lists all tagged versions (#7)
Browse files Browse the repository at this point in the history
* Added a mode which lists all tagged versions

This mode is triggered by running the program in this mode:

```
cargo run ~/git/chaos # Only one parameter: the repo name
```

This will read the list of tags from git, and use them to produce a reasonably-nice looking changelog based on the list of git commits.

* Bug fix: Remove initial v prefix in tags

This is overly simplistic and agressive, and probably won't cover all cases properly. But _for me_, it works rather well and makes the program more useful, so let's go with this for now.

* Use a better way to get the git tags

The problem with the previous appraoch was that it included a bit of "junk" in terms of origin/master etc., which meant that the most recent tag was often excluded => no changelog entry would be produced for it. This has now all been fixed, and the program works like a charm.

* Peer review: Be more consistent with where we print the newline.
  • Loading branch information
perlun authored Feb 10, 2017
1 parent 288354f commit a982352
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 84 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ version = "0.1.0"
authors = ["Per Lundberg <perlun@gmail.com>"]

[dependencies]
semver = "0.5.0"
84 changes: 84 additions & 0 deletions src/changelog_generator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use std::process::Command;

pub struct ChangelogGenerator {
pub repository_path: String,
pub from_revision: String,
pub to_revision: String
}

impl ChangelogGenerator {
pub fn generate_changelog(self) {
let output = self.get_log_output();
let lines = ChangelogGenerator::get_lines_from(&output);
let mut lines_iterator = lines.iter();

println!("## {}", self.to_revision);
print!("[Full Changelog](https://github.com/{}/compare/{}...{})\n\n",
self.get_repo_slug(),
self.from_revision,
self.to_revision);

loop {
match lines_iterator.next() {
Some(line) => {
if line.is_empty() {
break;
}
println!("* {}", line)
}
None => break,
}
}

print!("\n");
}

fn get_log_output(&self) -> String {
let output = Command::new("git")
.arg("log")
.arg("--oneline")
.arg(self.range())
.current_dir(&self.repository_path)
.output()
.unwrap_or_else(|e| panic!("Failed to run 'git log' with error: {}", e));
String::from_utf8_lossy(&output.stdout).into_owned()
}

fn range(&self) -> String {
format!("{}..{}", self.from_revision, self.to_revision)
}

fn get_lines_from(output: &str) -> Vec<&str> {
output.split('\n')
.collect()
}

fn get_repo_slug(&self) -> String {
let output = Command::new("git")
.arg("remote")
.arg("get-url")
.arg("origin")
.current_dir(&self.repository_path)
.output()
.unwrap_or_else(|e| panic!("Failed to run 'git log' with error: {}", e));

let url = String::from_utf8_lossy(&output.stdout).into_owned();

// The command output contains a trailing newline that we want to get rid of.
let trimmed_url = url.trim();

self.get_repo_slug_from(trimmed_url)
}

fn get_repo_slug_from(&self, url: &str) -> String {
// This very simplistic and stupid algorithm works for repos of these forms:
// https://github.com/dekellum/fishwife.git
// git@github.com:chaos4ever/chaos.git
let mangled_url = url.replace(":", "/").replace(".git", "");
let mut url_parts: Vec<_> = mangled_url.split('/').collect();
let repo_name = url_parts.pop().unwrap();
let org_name = url_parts.pop().unwrap();

format!("{}/{}", org_name, repo_name)
}
}
75 changes: 75 additions & 0 deletions src/git_tag_parser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use semver::Version;
use std::process::Command;

pub struct GitTagParser {
pub repository_path: String
}

impl GitTagParser {
// Returns a vector of "from", "to" tuples for each tag found in the repository. The "from" revision is the previous semver
// tag, the "to" revision is the current semver tag.
pub fn get_version_tag_pairs(&self) -> Vec<(String, String)> {
let mut from_version = self.get_root_ancestor();
let mut tag_pairs: Vec<(String, String)> = self.semver_tags().into_iter().rev().map(|tag| {
let old_from_version = from_version.clone();
let to_version = tag;
from_version = to_version.clone();

(old_from_version, to_version)
}).collect();

// TODO: Add the pair from "last tag to HEAD" if they do not point to the same rev.
tag_pairs.reverse();

tag_pairs
}

fn semver_tags(&self) -> Vec<String> {
let tags = self.get_tags();
tags.into_iter().filter(|e| match Version::parse(e.replace("v", "").as_str()) {
Ok(_) => true,
Err(_) => false
}).collect()
}

// A lot of parameters to this one. 'git tag -l' is much simpler, but the problem is that it produces a list of
// tags that is sorted in the wrong order. We want them in the order that they exist in the repo.
fn get_tags_args() -> Vec<String> {
[
"for-each-ref",
"--format=%(refname)",
"refs/tags/*"
].iter().map(|e| e.to_string()).collect()
}

fn get_tags(&self) -> Vec<String> {
let output = Command::new("git")
.args(&GitTagParser::get_tags_args())
.current_dir(&self.repository_path)
.output()
.unwrap_or_else(|e| panic!("Failed to run 'git tag' with error: {}", e));
let output_stdout = String::from_utf8_lossy(&output.stdout);
let output_lines = output_stdout.split('\n');

output_lines.map(|e|
e.to_string()
.replace("refs/tags/", "")
.trim()
.to_string()
).rev().collect()
}

fn get_root_ancestor(&self) -> String {
let output = Command::new("git")
.args(&[
"rev-list",
"--max-parents=0",
"HEAD"
])
.current_dir(&self.repository_path)
.output()
.unwrap_or_else(|e| panic!("Failed to run 'git tag' with error: {}", e));
let output_stdout = String::from_utf8_lossy(&output.stdout);
output_stdout.trim().to_string()
}
}
115 changes: 31 additions & 84 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,101 +1,48 @@
#![deny(unused_imports)]
#![deny(unused_variables)]

extern crate semver;

mod changelog_generator;
mod git_tag_parser;

use changelog_generator::ChangelogGenerator;
use git_tag_parser::GitTagParser;
use std::env;
use std::process::Command;
use std::process::exit;

struct Changelog {
repository_path: String,
from_revision: String,
to_revision: String
}
fn main() {
let args: Vec<_> = env::args().collect();

impl Changelog {
pub fn generate_changelog(self) {
let output = self.get_log_output();
let lines = Changelog::get_lines_from(&output);
let mut lines_iterator = lines.iter();
if args.len() == 2 {
let ref repository_path = args[1];
let git_tag_parser = GitTagParser {
repository_path: repository_path.clone()
};

println!("## {}", self.to_revision);
print!("[Full Changelog](https://github.com/{}/compare/{}...{})\n\n",
self.get_repo_slug(), self.from_revision, self.to_revision);
let version_tag_pairs = git_tag_parser.get_version_tag_pairs();

loop {
match lines_iterator.next() {
Some(line) => {
if line.is_empty() { return; }
println!("* {}", line)
},
None => break
}
for (from_tag, to_tag) in version_tag_pairs.into_iter() {
let generator = ChangelogGenerator {
repository_path: repository_path.clone(),
from_revision: from_tag,
to_revision: to_tag
};
generator.generate_changelog();
}
}

fn get_log_output(&self) -> String {
let output = Command::new("git")
.arg("log")
.arg("--oneline")
.arg(self.range())
.current_dir(&self.repository_path)
.output()
.unwrap_or_else(|e| panic!("Failed to run 'git log' with error: {}", e));
String::from_utf8_lossy(&output.stdout).into_owned()
}

fn range(&self) -> String {
format!("{}..{}", self.from_revision, self.to_revision)
}

fn get_lines_from(output: &str) -> Vec<&str> {
output
.split('\n')
.collect()
}

fn get_repo_slug(&self) -> String {
let output = Command::new("git")
.arg("remote")
.arg("get-url")
.arg("origin")
.current_dir(&self.repository_path)
.output()
.unwrap_or_else(|e| panic!("Failed to run 'git log' with error: {}", e));

let url = String::from_utf8_lossy(&output.stdout).into_owned();

// The command output contains a trailing newline that we want to get rid of.
let trimmed_url = url.trim();

self.get_repo_slug_from(trimmed_url)
}

fn get_repo_slug_from(&self, url: &str) -> String {
// This very simplistic and stupid algorithm works for repos of these forms:
// https://github.com/dekellum/fishwife.git
// git@github.com:chaos4ever/chaos.git
let mangled_url = url.replace(":", "/").replace(".git", "");
let mut url_parts: Vec<_> = mangled_url.split('/').collect();
let repo_name = url_parts.pop().unwrap();
let org_name = url_parts.pop().unwrap();

format!("{}/{}", org_name, repo_name)
}
}

fn main() {
let args: Vec<_> = env::args().collect();

if args.len() == 4 {
let changelog = Changelog {
} else if args.len() == 4 {
let generator = ChangelogGenerator {
repository_path: args[1].clone(),
from_revision: args[2].clone(),
to_revision: args[3].clone()
};
changelog.generate_changelog();
}
else {
println!("Usage: {} <path> <from_revision> <to_revision>\n", args[0]);
generator.generate_changelog();
} else {
println!("Usage: {} <path> [<from_revision> <to_revision>]\n",
args[0]);
println!("The path must be a clone of valid git repository.");
println!("If the 'from_revision' and 'to_revision' are not provided, a full log \
including all SemVer-tagged versions will be produced.");
exit(1);
}
}

0 comments on commit a982352

Please sign in to comment.