Skip to content

Commit

Permalink
add project
Browse files Browse the repository at this point in the history
  • Loading branch information
mbrav committed Apr 1, 2023
1 parent 1674e4c commit db0fdd7
Show file tree
Hide file tree
Showing 8 changed files with 624 additions and 0 deletions.
29 changes: 29 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[package]
name = "git_raider"
version = "0.0.1"
descripion = "Mass git repository search, replace and commit tool"
authors = ["mbrav <[email protected]>"]
edition = "2021"

[features]
ref_debug = []

[profile.dev]
opt-level = 1

# Build optimizations: https://github.com/johnthagen/min-sized-rust
[profile.release]
panic = "abort"
strip = true # Strip symbols from binary
opt-level = "z" # Optimize for size
lto = true # Enable link time optimization
codegen-units = 1 # Maximize size reduction optimizations (takes longer)

[[bin]]
name = "git-raider"
path = "src/main.rs"

[dependencies]
clap = { version = "4", features = ["derive", "env"] }
git2 = "0.16"
regex = "1"
64 changes: 64 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use clap::{ArgAction, Parser};

/// Mass git repository search, replace and commit tool
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
pub struct Config {
/// Path to repositories
#[arg(
long,
short,
value_name = "PATH",
default_value = "../repos",
env = "REPO_PATH"
)]
pub path: String,

/// Specify Regex pattern for branches to checkout
#[arg(
short = 'b',
long = "branch",
value_name = "REGEX",
default_value = r".*",
env = "REPO_BRANCH"
)]
pub branch_pattern: String,

/// Specify Regex pattern for filename
#[arg(short = 'f', long = "file", value_name = "REGEX", env = "FILE_PATTERN")]
pub file_pattern: Option<String>,

/// Specify Regex patterns for selecting lines
#[arg(short = 'l', long = "line", value_name = "REGEX", env = "LINE_PATTERN")]
pub line_pattern: Option<String>,

/// Specify Regex select patterns for selecting parts of a line
#[arg(
short = 's',
long = "select",
value_name = "REGEX",
env = "LINE_SELECT"
)]
pub line_select_pattern: Option<String>,

/// Specify Replace pattern patterns in files
#[arg(
short = 'r',
long = "replace",
value_name = "REGEX",
env = "LINE_REPLACE"
)]
pub line_replace_pattern: Option<String>,

/// Specify commit message
#[arg(short = 'm', long = "message", value_name = "TXT", env = "COMMIT_MSG")]
pub commit_message: Option<String>,

/// Display results at the end of program execution
#[arg(short = 'd', long = "display", action=ArgAction::SetTrue, env = "DISPLAY_RES")]
pub display_results: bool,

/// Run program in dry mode without altering files and writing to git history
#[arg(short = 'y', long = "dry", action=ArgAction::SetTrue, env = "DRY_RUN")]
pub dry_run: bool,
}
82 changes: 82 additions & 0 deletions src/func.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use regex::Regex;
use std::fs;
use std::path::PathBuf;

#[cfg(target_os = "linux")]
pub fn are_you_on_linux() {
println!("You are running linux!");
if cfg!(target_os = "linux") {
println!("Yes. It's definitely linux!");
} else {
println!("Yes. It's definitely *not* linux!");
}
}

// And this function only gets compiled if the target OS is *not* linux
#[cfg(not(target_os = "linux"))]
pub fn are_you_on_linux() {
println!("You are not using Linux. Do you seriously call yourself a developer?");
println!("Do yourself a favor and install Linux");
println!("I use arch btw");
}

/// Recursively find directories
pub fn find_dirs(dir: &PathBuf, name: &str, parent: &bool) -> Vec<PathBuf> {
let mut result = Vec::new();

if let Ok(entries) = fs::read_dir(dir) {
for entry in entries {
let path = entry.expect("Error unpacking path").path();
if path.is_dir() {
if fs::read_dir(&path.join(name)).is_ok() {
match parent {
// Get path the directory itself
false => result.push(path.clone()),
// Get path of parent of the directory instead of the directory itself
true => {
if let Some(parent) = path.parent() {
result.push(parent.to_path_buf());
}
}
}
} else {
result.append(&mut find_dirs(&path, name, parent));
}
}
}
}
result
}

/// Recursively find files
pub fn find_files(dir: &str, pattern: &str) -> Vec<PathBuf> {
let re = Regex::new(pattern).unwrap();
let mut found_files = Vec::new();
let dir_entries = fs::read_dir(dir).unwrap();

for entry in dir_entries {
let entry = entry.unwrap();
let path = entry.path();
let file_name = path.file_name().unwrap().to_str().unwrap();
if path.is_file() && re.is_match(file_name) {
// If path is a file and is a match
// Push to found files
found_files.push(path.clone());
} else if path.is_dir() {
// Otherwise proceed to recursion
found_files.append(&mut find_files(path.to_str().unwrap(), pattern));
}
}
found_files
}

pub fn paths_info_print(list: &Vec<PathBuf>, msg: &str, elements: usize) {
println!("First {} ({}) {}:", elements, list.len(), msg);
for f in 0..elements {
if let Some(val) = list.get(f) {
println!("{}", val.display());
} else {
break;
}
}
}
66 changes: 66 additions & 0 deletions src/git.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use git2::{Branch, BranchType, Branches, Oid, Repository};
use std::path::PathBuf;

/// Get repo from path
pub fn get_repo(path: &PathBuf) -> Result<Repository, git2::Error> {
let repo = Repository::open(path)?;
Ok(repo)
}

/// Get all branches in a repo
pub fn get_branches(repo: &Repository) -> Result<Branches, git2::Error> {
let branches = repo.branches(Some(BranchType::Local))?;
Ok(branches)
}

/// Get a branches refname
pub fn get_ref<'a>(branch: &'a Branch) -> &'a str {
let refname = branch
.name()
.ok()
.unwrap()
.expect("Error getting branch's ref");
refname
}

/// Checkout a branch in a repo using Branch struct
pub fn checkout_branch(repo: &Repository, branch: &Branch) -> Result<Oid, git2::Error> {
let refname = get_ref(branch);
println!(" Checking out {}", &refname);

let (object, reference) = repo.revparse_ext(refname).expect(" Object not found");

repo.checkout_tree(&object, None)
.expect(" Failed to checkout");

match reference {
// gref is an actual reference like branches or tags
Some(gref) => repo.set_head(gref.name().unwrap()),
// this is a commit, not a reference
None => repo.set_head_detached(object.id()),
}
.expect(" Failed to set HEAD");

let head = repo.head().unwrap().target().unwrap();
println!(" Success checkout {} {}", refname, &head);

Ok(head)
}

/// Stage all changes
pub fn stage_all(repo: &mut Repository) -> Result<(), git2::Error> {
let mut index = repo.index()?;
index.add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None)?;
index.write()?;
Ok(())
}

/// Commit stages changes
/// TODO: Fix `{ code: -15, klass: 11, message: "failed to create commit: current tip is not the first parent" }'`
pub fn commit(repo: &mut Repository, msg: &str) -> Result<(), git2::Error> {
let signature = repo.signature().unwrap();
let oid = repo.index().unwrap().write_tree().unwrap();
let tree = repo.find_tree(oid).unwrap();
repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])?;
Ok(())
}
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub mod config;
pub mod func;
pub mod git;
pub mod raider;
pub mod structs;
76 changes: 76 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use std::time::Instant;

use clap::Parser;
use git_raider::config::Config;
use git_raider::func;
use git_raider::raider::RepoRaider;

fn main() {
func::are_you_on_linux();
let conf = Config::parse();
let start: Instant = Instant::now();

let mut raider = RepoRaider::new(conf.path);

raider.find_repos();
raider.checkout_branch(conf.branch_pattern.as_str());

// Match files
if let Some(file_pattern) = conf.file_pattern {
raider.match_files(file_pattern.as_str());
}

// Match lines in files
if let Some(content_pattern) = conf.line_pattern {
raider.match_lines(content_pattern.as_str());
}

// Generate replace patterns for each pattern
if let Some(line_select_pattern) = conf.line_select_pattern {
if let Some(line_replace_pattern) = conf.line_replace_pattern {
raider.replace(line_select_pattern.as_str(), line_replace_pattern.as_str());
} else {
panic!("Replace file pattern required for line select pattern");
}
}

// If dry run is not set
// Do not alter files and stage changes
// TODO: Make dry run simulate altering files and staging changes
if !conf.dry_run {
// Apply replace patterns
raider.apply();

// Stage matches
raider.stage();
}

// Commit changes with message
if let Some(commit_message) = conf.commit_message {
raider.commit(commit_message.as_str());
}

// Print results for found directories, Pages and matches
if conf.display_results {
results(&raider);
}

println!("Elapsed: {:.3?}", start.elapsed());
}

fn results(raider: &RepoRaider) {
func::paths_info_print(&raider.get_dirs(), "found directories (repos)", 5);

println!("Found pages:");
for p in &raider.get_pages() {
println!("M {}: {}", p.matches.len(), p.relative_path.display());
for m in &p.matches {
println!(" O {:<3} {}", m.line, m.content);
if let Some(r) = m.replace.as_ref() {
println!(" R {:<3} {}", m.line, r);
} else {
println!(" R None");
}
}
}
}
Loading

0 comments on commit db0fdd7

Please sign in to comment.