-
Notifications
You must be signed in to change notification settings - Fork 55
/
git.rs
133 lines (121 loc) · 4.7 KB
/
git.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// Copyright 2018 The Rust Project Developers
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.
//! Get git commits with help of the libgit2 library
const RUST_SRC_URL: &str = "https://github.com/rust-lang/rust";
const RUST_SRC_REPO: Option<&str> = option_env!("RUST_SRC_REPO");
use std::path::Path;
use chrono::{TimeZone, Utc};
use failure::{bail, Error};
use git2::build::RepoBuilder;
use git2::{Commit as Git2Commit, Repository};
use log::debug;
use crate::Commit;
impl Commit {
// Takes &mut because libgit2 internally caches summaries
fn from_git2_commit(commit: &mut Git2Commit<'_>) -> Self {
Commit {
sha: commit.id().to_string(),
date: Utc.timestamp(commit.time().seconds(), 0),
summary: String::from_utf8_lossy(commit.summary_bytes().unwrap()).to_string(),
}
}
}
fn lookup_rev<'rev>(repo: &'rev Repository, rev: &str) -> Result<Git2Commit<'rev>, Error> {
if let Ok(c) = repo.revparse_single(rev)?.into_commit() {
return Ok(c);
}
bail!("Could not find a commit for revision specifier '{}'", rev)
}
fn get_repo() -> Result<Repository, Error> {
let loc = Path::new("rust.git");
match (RUST_SRC_REPO, loc.exists()) {
(Some(_), _) | (_, true) => {
let path = RUST_SRC_REPO.map(Path::new).unwrap_or(loc);
eprintln!("opening existing repository at {:?}", path);
let repo = Repository::open(path)?;
{
eprintln!("refreshing repository");
let mut remote = repo
.find_remote("origin")
.or_else(|_| repo.remote_anonymous("origin"))?;
remote.fetch(&["master"], None, None)?;
}
Ok(repo)
}
(None, false) => {
eprintln!("cloning rust repository");
Ok(RepoBuilder::new()
.bare(true)
.clone(RUST_SRC_URL, Path::new("rust.git"))?)
}
}
}
pub(crate) fn get_commit(sha: &str) -> Result<Commit, Error> {
let repo = get_repo()?;
let mut rev = lookup_rev(&repo, sha)?;
Ok(Commit::from_git2_commit(&mut rev))
}
/// Returns the bors merge commits between the two specified boundaries
/// (boundaries inclusive).
pub fn get_commits_between(first_commit: &str, last_commit: &str) -> Result<Vec<Commit>, Error> {
let repo = get_repo()?;
eprintln!("looking up first commit");
let mut first = lookup_rev(&repo, first_commit)?;
eprintln!("looking up second commit");
let last = lookup_rev(&repo, last_commit)?;
// Sanity check -- our algorithm below only works reliably if the
// two commits are merge commits made by bors
let assert_by_bors = |c: &Git2Commit<'_>| -> Result<(), Error> {
match c.author().name() {
Some("bors") => Ok(()),
Some(author) => bail!("Expected author {} to be bors for {}", author, c.id()),
None => bail!("No author for {}", c.id()),
}
};
eprintln!("checking that commits are by bors and thus have ci artifacts...");
assert_by_bors(&first)?;
assert_by_bors(&last)?;
// Now find the commits
// We search from the last and always take the first of its parents,
// to only get merge commits.
// This uses the fact that all bors merge commits have the earlier
// merge commit as their first parent.
eprintln!("finding bors merge commits");
let mut res = Vec::new();
let mut current = last;
loop {
assert_by_bors(¤t)?;
res.push(Commit::from_git2_commit(&mut current));
match current.parents().next() {
Some(c) => {
if c.author().name() != Some("bors") {
debug!(
"{:?} has non-bors author: {:?}, skipping",
c.id(),
c.author().name()
);
current = c.parents().next().unwrap();
continue;
}
current = c;
if current.id() == first.id() {
// Reached the first commit, our end of the search.
break;
}
}
None => bail!("reached end of repo without encountering the first commit"),
}
}
res.push(Commit::from_git2_commit(&mut first));
// Reverse in order to obtain chronological order
res.reverse();
eprintln!(
"found {} bors merge commits in the specified range",
res.len()
);
Ok(res)
}