Skip to content

Commit

Permalink
Add subcommand to review arbitrary remote
Browse files Browse the repository at this point in the history
  • Loading branch information
SuperSandro2000 committed Aug 13, 2024
1 parent f71f48a commit 75bc6e6
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 83 deletions.
2 changes: 1 addition & 1 deletion src/changes.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct RepoChange {
pub name: String,
pub remote: String,
Expand Down
230 changes: 148 additions & 82 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,63 +3,142 @@
mod changes;
mod images;

use anyhow::anyhow;
use anyhow::Context;
use anyhow::{anyhow, Context};
use changes::{Change, ChangeCommit, RepoChange};
use clap::builder::styling::Style;
use clap::Parser;
use clap::{builder::styling::Style, Parser, Subcommand};
use git2::Repository;
use images::ContainerImages;
use octocrab::commits::PullRequestTarget;
use octocrab::models::pulls;
use octocrab::models::{pulls, pulls::ReviewState};
use octocrab::Octocrab;
use std::{env, str, sync::Arc};

const BOLD_UNDERLINE: Style = Style::new().bold().underline();

/// Program to simplify PCI double approval process across repositories
#[derive(Parser, Debug)]
#[derive(Parser)]
#[command(version, about, long_about = None, after_help = format!("{BOLD_UNDERLINE}Environment variables:{BOLD_UNDERLINE:#}
GITHUB_TOKEN GitHub token to use for API requests
"))]
// see https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/variables for environment variables
struct Args {
/// Git repository where to discover images.yaml files
#[command(propagate_version = true)]
// see https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/variables for environment variablesuse
struct Cli {
/// The git base ref to compare against
#[arg(
short,
long,
env = "GITHUB_WORKSPACE",
env = "GITHUB_BASE_REF",
hide_env_values = true,
required = true
required = false,
global = true
)]
workspace: String,
base: String,

/// The git base ref to compare against
/// The git head ref or source branch of the PR to compare against
#[arg(
long,
default_value = "HEAD",
env = "GITHUB_BASE_REF",
hide_env_values = true
env = "GITHUB_HEAD_REF",
hide_env_values = true,
required = false,
global = true
)]
base: String,

/// The git head ref or source branch of the PR to compare against
#[arg(long, env = "GITHUB_HEAD_REF", hide_env_values = true, required = true)]
head: String,

#[command(subcommand)]
command: Commands,
}

#[derive(Subcommand)]
#[command(propagate_version = true)]
enum Commands {
/// Analyzes commits in a repo and finds relevant reviews
Repo {
/// GitHub git remote to use
remote: String,
},

/// Analyzes a helm-charts repo, finds sources from values.yaml files and runs repo subcommand on them
HelmChart {
/// Git repository where to discover images.yaml files
#[arg(
short,
long,
env = "GITHUB_WORKSPACE",
hide_env_values = true,
required = false,
global = true
)]
workspace: String,
},
}

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
let args = Args::parse();
let cli = Cli::parse();

let repo = Repository::open(args.workspace).context("failed to open repository")?;
octocrab::initialise(
Octocrab::builder()
.personal_token(env::var("GITHUB_TOKEN").context("missing GITHUB_TOKEN env")?)
.build()
.context("failed to build octocrab client")?,
);
let octocrab = octocrab::instance();

match &cli.command {
Commands::Repo { remote } => {
let repo = &mut RepoChange {
name: parse_remote(remote).context("while parsing remote")?.1,
remote: remote.clone(),
base_commit: cli.base,
head_commit: cli.head,
changes: Vec::new(),
};
find_reviews(&octocrab, repo)
.await
.context("while finding reviews")?;

print_changes(&[repo.clone()]);
}
Commands::HelmChart { workspace } => {
find_values_yaml(&octocrab, workspace.clone(), cli.base, cli.head)
.await
.context("while finding values.yaml files")?;
}
}

Ok(())
}

fn parse_remote(remote: &String) -> Result<(String, String), anyhow::Error> {
let repo_parts: Vec<&str> = remote
.strip_prefix("https://github.com/")
.ok_or(anyhow!("can't strip https://github.com/ prefix"))?
.split('/')
.collect();
let repo_owner = repo_parts[0].to_string();
let repo_name = repo_parts[1]
.strip_suffix(".git")
.ok_or(anyhow!("can't strip .git suffix"))?
.to_string();

return Ok((repo_owner, repo_name));
}

async fn find_values_yaml(
octocrab: &Arc<Octocrab>,
workspace: String,
base: String,
head: String,
) -> Result<(), anyhow::Error> {
let repo = Repository::open(workspace).context("failed to open repository")?;

let base_ref = repo
.revparse_single(&args.base)
.revparse_single(&base)
.context("can't parse base_ref")?
.id();

let head_ref = repo
.revparse_single(&args.head)
.revparse_single(&head)
.context("can't parse head_ref")?
.id();

Expand Down Expand Up @@ -119,75 +198,27 @@ async fn main() -> Result<(), anyhow::Error> {
}
}

octocrab::initialise(
octocrab::Octocrab::builder()
.personal_token(env::var("GITHUB_TOKEN").context("missing GITHUB_TOKEN env")?)
.build()
.context("failed to build octocrab client")?,
);
let octocrab = octocrab::instance();

for repo in &mut changes {
collect_repo_changes(&octocrab, repo)
find_reviews(octocrab, repo)
.await
.context("failed collecting repo changes")?;
.context("while collecting repo changes")?;
}

for change in &changes {
println!(
"Name {} from {} moved from {} to {}",
change.name, change.remote, change.base_commit, change.head_commit
);
println!("| Commit link | Pull Request link | Approvals | Reviewer's verdict |");
println!("|-------------|-------------------|-----------|--------------------|");
for commit_change in &change.changes {
let pr_link = commit_change.pr_link.clone();
println!(
"| {} | {} | {} | <enter your decision> |",
commit_change
.commits
.iter()
.map(|x| format!(
"[{}]({})",
match x.headline.char_indices().nth(45) {
None => x.headline.clone(),
Some((idx, _)) => x.headline[..idx].to_string() + "…",
},
x.link
))
.collect::<Vec<String>>()
.join(" ,<br>"),
match pr_link {
Some(pr_link) => short_md_link(pr_link),
None => String::new(),
},
commit_change.approvals.join("None"),
);
}
}
print_changes(&changes);

Ok(())
}

async fn collect_repo_changes(
octocrab: &Arc<octocrab::Octocrab>,
async fn find_reviews(
octocrab: &Arc<Octocrab>,
repo: &mut RepoChange,
) -> Result<(), anyhow::Error> {
let repo_parts: Vec<&str> = repo
.remote
.strip_prefix("https://github.com/")
.ok_or(anyhow!("can't strip https://github.com/ prefix"))?
.split('/')
.collect();
let repo_owner = repo_parts[0];
let repo_name = repo_parts[1]
.strip_suffix(".git")
.ok_or(anyhow!("can't strip .git suffix"))?;
let (repo_owner, repo_name) = parse_remote(&repo.remote).context("while parsing remote")?;

let link_prefix = format!("https://github.com/{repo_owner}/{repo_name}");

let compare = octocrab
.commits(repo_owner, repo_name)
.commits(repo_owner.clone(), repo_name.clone())
.compare(&repo.base_commit, &repo.head_commit)
.send()
.await
Expand All @@ -198,7 +229,7 @@ async fn collect_repo_changes(

for commit in &compare.commits {
let mut associated_prs_page = octocrab
.commits(repo_owner, repo_name)
.commits(repo_owner.clone(), repo_name.clone())
.associated_pull_requests(PullRequestTarget::Sha(commit.sha.clone()))
.send()
.await
Expand Down Expand Up @@ -227,7 +258,7 @@ async fn collect_repo_changes(
println!("pr number: {:}", associated_pr.number);

let mut pr_reviews_page = octocrab
.pulls(repo_owner, repo_name)
.pulls(repo_owner.clone(), repo_name.clone())
.list_reviews(associated_pr.number)
.send()
.await
Expand Down Expand Up @@ -277,13 +308,48 @@ async fn collect_repo_changes(
Ok(())
}

fn print_changes(changes: &[RepoChange]) {
for change in changes {
println!(
"Name {} from {} moved from {} to {}",
change.name, change.remote, change.base_commit, change.head_commit
);
println!("| Commit link | Pull Request link | Approvals | Reviewer's verdict |");
println!("|-------------|-------------------|-----------|--------------------|");
for commit_change in &change.changes {
let pr_link = commit_change.pr_link.clone();
println!(
"| {} | {} | {} | <enter your decision> |",
commit_change
.commits
.iter()
.map(|x| format!(
"[{}]({})",
match x.headline.char_indices().nth(45) {
None => x.headline.clone(),
Some((idx, _)) => x.headline[..idx].to_string() + "…",
},
x.link
))
.collect::<Vec<String>>()
.join(" ,<br>"),
match pr_link {
Some(pr_link) => short_md_link(pr_link),
None => String::new(),
},
commit_change.approvals.join("None"),
);
}
}
}

fn collect_approved_reviews(
pr_reviews: &[pulls::Review],
review: &mut Change,
) -> Result<(), anyhow::Error> {
for pr_review in pr_reviews {
// TODO: do we need to check if this is the last review of the user?
if pr_review.state.ok_or(anyhow!("review has no state"))? == pulls::ReviewState::Approved {
if pr_review.state.ok_or(anyhow!("review has no state"))? == ReviewState::Approved {
review.approvals.push(
pr_review
.user
Expand Down

0 comments on commit 75bc6e6

Please sign in to comment.