-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added a mode which lists all tagged versions (#7)
* 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
Showing
4 changed files
with
191 additions
and
84 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,3 +4,4 @@ version = "0.1.0" | |
authors = ["Per Lundberg <perlun@gmail.com>"] | ||
|
||
[dependencies] | ||
semver = "0.5.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |