Getting started with Rust - part 1
I decided to look a bit into Rust. I’ve been hearing great things about it, the community seems to be (mostly) very inviting and open, and the language in itself seems to have a nice mix of modern features (lambdas, boxed types, extensions), while still allowing for low-level systems programming. The initial idea was to work through the The rust programming languagebook and write a bit about that. But that quickly became very boring for me, so I just decided to start writing some small tools to see what the language can do and how to use the various features of the language. So for a first program, I wanted to write something useful.
At my current project we work with lots of branches, each feature/bug/ticket has its own branch and with reviews, PRs different projects I sometimes lose track of what I exactly named my branch I was working on. I could of course just check GitHub, or run some magical git command, but this would be a nice small tool to write with Rust. This simple tool should do this:
- Look at the current list of
git
branches in a specific directory. - Determine the hash and date of the last commit of that specific branch.
- Print out the list of last commits per branch, ordered by date.
That way I can quickly see on which branch I was working, and which branches were recently updated, without having to switch to each branch, and do git log
and such.
Before we look at the code, a very big disclaimer. I haven’t looked at any optimizations yet, this was merely an exercise in Rust to get a working program. So any experienced Rust programmer will probably spot a whole lot of bad practices.
The final result
First, let’s see what the final program looks like:
$ ./rust-git ~/dev/git/some/project
Using /Users/jos/dev/git/some/project as git repository to analyse.
2020-02-10 15:18:22 UTC : d3161dedcf7a99d3991b0492fb08285b245c93eb : dev####
2020-02-10 11:42:47 UTC : 0b75ba1d2f745ea6c0950646b92ec8480684472d : set##########################
2020-02-10 07:09:51 UTC : cdf7bbc0aa090d87ca8bcdebbe0bb1df105ca1c2 : cha#######################
2020-02-07 13:32:56 UTC : d52cdd456b2a1c2fd406a46601bad2d785a32f1e : rem########################
2020-02-06 12:36:11 UTC : edc540c8c79d9fff3e713bba4f6514d018a69df7 : sen################################
2020-02-05 13:57:05 UTC : fbdf684693a1791fecccc37d0c894fab619831fa : fix######################
2020-01-31 10:32:18 UTC : 8d35760cf0cc588b25d6566d4ef9771d172a23f4 : set###################
2020-01-27 10:22:35 UTC : 91ea1fabf9f7e6058a3e9f3e6b69fb87930786da : not#############
...
It actually uses color, so it looks much better when you actually run it. I’ve hashed the biggest part of the branches for this example, since I’m testing it on a work project. But you probably get the idea. It shows the branches, when was the last commit, and what is the hash of that commmit.
Nothing too special. But fun to create. So without further ado, the code.
The code
We’ll start with the Toml. Using crates wasn’t so different as using any of the package managers like npm, Gradle, Maven, SBT and such. So for this sample I ended up with this:
[package]
name = "rust-git"
version = "0.1.0"
authors = ["Jos Dirksen <jos.dirksen@gmail.com>"]
edition = "2018"
[dependencies]
git2 = "0.10"
chrono = "0.4.10"
libc = "0.2.66"
colored = "1.9.2"
Basically I used the dependencies for this:
git2
: Provides access to git functionality by calling intolibgit2
.chrono
: For dealing with and converting timestamp.libc
: For an apparent issue with Rust, where you can’t pipe the output of a program to something likehead
orless
colored
: Which provides extension functions (which I didn’t knew Rust has), to colorize output.
This doesn’t look like much, but when doing typescript or react you often get a large number of transitive dependencies as well. So I ran into cargo-tree
, where you can create tree of these dependencies:
$ cargo-tree tree
rust-git v0.1.0 (/Users/jos/dev/git/smartjava/articles/rust-git)
├── chrono v0.4.10
│ ├── num-integer v0.1.42
│ │ └── num-traits v0.2.11
│ │ [build-dependencies]
│ │ └── autocfg v1.0.0
│ │ [build-dependencies]
│ │ └── autocfg v1.0.0 (*)
│ ├── num-traits v0.2.11 (*)
│ └── time v0.1.42
│ └── libc v0.2.66
├── colored v1.9.2
│ ├── atty v0.2.14
│ │ └── libc v0.2.66 (*)
│ └── lazy_static v1.4.0
├── git2 v0.10.2
│ ├── bitflags v1.2.1
│ ├── libc v0.2.66 (*)
│ ├── libgit2-sys v0.9.2
│ │ ├── libc v0.2.66 (*)
│ │ ├── libssh2-sys v0.2.14
│ │ │ ├── libc v0.2.66 (*)
│ │ │ ├── libz-sys v1.0.25
│ │ │ │ └── libc v0.2.66 (*)
│ │ │ │ [build-dependencies]
│ │ │ │ ├── cc v1.0.50
│ │ │ │ │ └── jobserver v0.1.21
│ │ │ │ │ └── libc v0.2.66 (*)
│ │ │ │ └── pkg-config v0.3.17
│ │ │ └── openssl-sys v0.9.54
│ │ │ └── libc v0.2.66 (*)
│ │ │ [build-dependencies]
│ │ │ ├── autocfg v1.0.0 (*)
│ │ │ ├── cc v1.0.50 (*)
│ │ │ └── pkg-config v0.3.17 (*)
│ │ │ [build-dependencies]
│ │ │ ├── cc v1.0.50 (*)
│ │ │ └── pkg-config v0.3.17 (*)
│ │ ├── libz-sys v1.0.25 (*)
│ │ └── openssl-sys v0.9.54 (*)
│ │ [build-dependencies]
│ │ ├── cc v1.0.50 (*)
│ │ └── pkg-config v0.3.17 (*)
│ ├── log v0.4.8
│ │ └── cfg-if v0.1.10
│ └── url v2.1.1
│ ├── idna v0.2.0
│ │ ├── matches v0.1.8
│ │ ├── unicode-bidi v0.3.4
│ │ │ └── matches v0.1.8 (*)
│ │ └── unicode-normalization v0.1.12
│ │ └── smallvec v1.2.0
│ ├── matches v0.1.8 (*)
│ └── percent-encoding v2.1.0
└── libc v0.2.66 (*)
which isn’t that shocking. Lot’s of build-dependencies
and a reasonable amount of other crates. Anyway, cargo
so far just worked, which was nice, and it feels really quick. But, of course, this is really just a very simple project, so that’s probably to be expected.
Now the main code… and like I already mentioned. This is my first step into Rust, so bare with me.
use git2::{Repository, BranchType, Branch};
use chrono::{Utc, TimeZone, DateTime};
use std::option::Option as Option;
use colored::*;
use std::env;
use std::process::exit;
struct BranchCommitTime {
branch_name: String,
last_commit: DateTime<Utc>,
hash: String
}
fn main() {
// to allow piping the result to other programs
//https://github.com/rust-lang/rust/issues/46016
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
let args: Vec<String> = env::args().collect();
match args.len() {
1 => {
println!("{}","Using current dir as git repository to analyse.".green());
show_branches_and_time(env::current_dir().unwrap().to_str().unwrap());
},
2 => {
println!("{}{}{}", "Using ".green() ,args[1].green(), " as git repository to analyse.".green());
show_branches_and_time(args[1].as_str());
},
_ => {
println!("{}", "Unexpected number of arguments".red());
exit(1);
}
};
}
fn show_branches_and_time(dir: &str) {
let repo = match Repository::open(dir) {
Ok(repo) => repo,
Err(e) => panic!("failed to init: {}", e),
};
// get all the local branches, and for each get the name and last commit time
// and return them as a new list of BranchCommitTime structs
let branches_list = get_all_local_branches(&repo);
let bct_list = branches_list.filter_map(|branch| {
// first get the name, or ignore the field when the name can't be determined
return match branch {
Ok((branch, _)) => {
let branch_name = get_branch_name(&branch);
let last_commit = get_branch_last_commit(&branch, &repo);
// flatmap these options to create an Option<BranchCommitTime>
let bct = branch_name
.and_then(|n| last_commit.map(|t|
BranchCommitTime { branch_name: n, last_commit: t.1, hash: t.0 }
));
bct
}
Err(_) => None
};
});
// collect the iterator in a vector so we can sort it. This has to
// be a mutable one, since we do sorting in place. Finally print out
// the sorted list to console.
let mut bct_v: Vec<_> = bct_list.collect();
bct_v.sort_by(|a, b| b.last_commit.cmp(&a.last_commit));
bct_v.iter().for_each(|bc| {
// for article purposes we'll hash the names
let hashed_name = hash_string(bc.branch_name.as_str());
println!("{} : {} : {}", bc.last_commit.to_string().yellow(), bc.hash, hashed_name.blue());
}
);
}
/// replace all the characters in a string, except the first 3
fn hash_string(name: &str) -> String {
let mut hashed_name = String::from("");
for (pos, e) in name.chars().enumerate() {
match pos {
0..=2 => hashed_name.push(e),
_ => hashed_name.push('#')
};
}
return hashed_name
}
/// We get the name, and convert it to a string, so we don't
/// run into ownership issues, or need to pass the lifetime around.
fn get_branch_name(branch: &Branch) -> Option<String> {
let name = match branch.name() {
Ok(r) => r.map(|n| String::from(n)),
Err(_) => None
};
return name;
}
/// get the last commit time from a branch, if it fails return none
fn get_branch_last_commit(branch: &Branch, repo: &Repository) -> Option<(String,DateTime<Utc>)> {
let p = branch.get().target().and_then(|oid| {
let commit = repo.find_commit(oid);
let t = commit.map(|c|
(c.id().to_string(), Utc.timestamp(c.time().seconds(), 0))
);
let res = t.ok();
res
});
return p;
}
/// Get all the local branches for the passed in repository
fn get_all_local_branches(repo: &Repository) -> git2::Branches {
let branches_list = match repo.branches(Option::Some(BranchType::Local)) {
Ok(br) => br,
Err(e) => panic!("failed to init: {}", e),
};
return branches_list;
}
Nothing to special, but it allowed me to learn a couple of interesting concepts from Rust. The basic flow is shown in the code. For the end of this article, I’d like to listen a couple of small conclusions I made so far regarding Rust.
The conclusions after one simple Rust project
Just a couple of points from my first project. In no particular order:
- Rustc is very nice with exceptions and help how to solve it: I really like how the
rustc
compiler communicates errors. It not only tells you what is wrong, but also explains why it is, and what you might need to do to solve it. I don’t understand all the proposals yet, but it really helps with learning how the language, and its specific feature work (or should work). - IntelliJ as editor has some rough edges: I’ve used the Intellij Rust plugin and that doesn’t feel really polished yet. I’ve been slowly moving away from IntelliJ for a couple of languages (Scala, Typescript and Javascript) to Visual Studio Code, which feels much leaner and quicker. I probably need to dive into the options available for Rust though.
- Option, Result feels natural: I really like that rust provides support for boxed types like Result and Option (and probably a lot more I haven’t looked at) out of the box. It really simplifies error handling, and feels much better than how it is done in Golang (at least for me).
- Missing
do
orfor-comprehension
: But even thoughResult
andOption
are great, I do miss thefor-comprehensions
from Scala or Haskellsdo
notation. After a quick search I already found a couple of libraries that add this syntactic sugar, so might look into that in the future as well. Kotlin, which I use daily at work, also doesn’t provide such a construct. But theArrow-kt
library provides a very nice extension for this to the core language. - Pattern matching: I’ve mostly used pattern matching in Scala, where I really like it. In Kotlin the support isn’t that great, but in Rust so far it feels quite nice. I’m still struggling a bit with the syntax at certain points, but so far I’m quite liking it.
- Borrow, ownership, lifetimes: Apparently returning a
&str
is something complex. I’ve avoided theCow
,Borrow
,Into
, lifetime annotation stuff so far. After reading a bit, I see the need for it, and it looks like Rust has chosen a great approach. So in a future project I’ll dive into it in more depth. - Odd lambda syntax: And finally, the lambda syntax takes some getting used to. It’s quite different from most languages I’ve used, but that’s probably just because all is new for me.
All in all, I’m really pleasantly suprised by this language. It’s been really fun creating this simple tool, and I never really felt too much constrained by the language. While I struggled with some parts, the documentation and compiler hints (and a good amount of trial-and-error) got me where I wanted. Now just to think of something to build for the next experiment….