-
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.
- Loading branch information
Showing
8 changed files
with
624 additions
and
0 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 |
---|---|---|
@@ -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" |
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,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, | ||
} |
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,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; | ||
} | ||
} | ||
} |
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,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(()) | ||
} |
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,5 @@ | ||
pub mod config; | ||
pub mod func; | ||
pub mod git; | ||
pub mod raider; | ||
pub mod structs; |
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,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"); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.