Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ toml = { version = "1.1.3" }
[dependencies]
clap = { features = ["derive"], version = "4.6.4" }
crossbeam-queue = "0.3.13"
crossterm = { version = "0.28" }
futures = { version = "0.3.33" }
globset = "0.4.19"
num_cpus = { version = "1.17.0" }
once_cell = { version = "1.21.4" }
rayon = "1.12.0"
ratatui = { version = "0.29" }
tokio = { version = "1.53.1", features = ["full"] }
walkdir = { version = "2.5.0" }

Expand All @@ -43,7 +45,7 @@ autobins = false
autoexamples = false
autotests = false
default-run = "Run"
description = "Run🍺"
description = "Run 🍺"
edition = "2024"
include = [
"Source/**/*",
Expand Down
27 changes: 13 additions & 14 deletions Source/Fn/Binary/Command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,10 @@ pub mod Index;
pub mod Parallel;
pub mod Sequential;

/// Defines and configures the command-line interface for the "Run" utility.
///
/// This function uses the `clap` crate to specify all possible arguments,
/// flags, and options, including their help messages, default values, and
/// relationships.
///
/// # Returns
///
/// An `ArgMatches` object containing the parsed values from the command line.
pub fn Fn() -> ArgMatches {
ClapCommand::new("Run")
.version(env!("CARGO_PKG_VERSION"))
.author("Source ✍🏻 Open👐🏻 <Source/Open@PlayForm.Cloud>")
.author("Source ✍🏻 Open 👐🏻 <Source/Open@PlayForm.Cloud>")
.about("A utility to run commands in directories matching a pattern.")
.arg(
Arg::new("File")
Expand All @@ -35,11 +26,19 @@ pub fn Fn() -> ArgMatches {
.display_order(2)
.help("Execute commands in parallel across all found directories."),
)
.arg(
Arg::new("Tui")
.short('T')
.long("Tui")
.action(ArgAction::SetTrue)
.display_order(3)
.help("Launch the interactive TUI panel dashboard."),
)
.arg(
Arg::new("Root")
.short('R')
.long("Root")
.display_order(3)
.display_order(4)
.value_name("DIRECTORY")
.help("The root directory to start the search from.")
.default_value("."),
Expand All @@ -49,14 +48,14 @@ pub fn Fn() -> ArgMatches {
.short('E')
.long("Exclude")
.action(ArgAction::Append)
.display_order(4)
.display_order(5)
.value_name("PATTERNS")
.help("A space-separated list of glob patterns to exclude from the search.")
.default_value("**/{node_modules,.git,target,dist,vendor}/**/*"),
)
.arg(
Arg::new("Pattern")
.display_order(5)
.display_order(6)
.value_name("PATTERN")
.required(true)
.help("The file or directory name that identifies a target directory."),
Expand All @@ -66,7 +65,7 @@ pub fn Fn() -> ArgMatches {
.short('C')
.long("Command")
.action(ArgAction::Append)
.display_order(6)
.display_order(7)
.value_name("COMMAND")
.required(true)
.allow_hyphen_values(true)
Expand Down
145 changes: 61 additions & 84 deletions Source/Fn/Binary/Command/Parallel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,61 +6,43 @@
use crossbeam_queue::ArrayQueue;
use once_cell::sync::Lazy;
use rayon::prelude::*;
use tokio::sync::{Mutex, mpsc};
use tokio::sync::{Mutex, mpsc::Sender};

use crate::{
Fn::Binary::Command::Index,
Struct::Binary::Command::Entry::Struct as ExecutionOption,
Struct::{
Binary::Command::Entry::Struct as ExecutionOption,
Event::Struct as Event,
},
};

pub mod GPG;
pub mod Process;

/// A global, asynchronous mutex to ensure that only one GPG-related git command
/// runs at any given time, preventing conflicts with the GPG agent.
static GPG_MUTEX:Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
static GPG_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));

Check failure on line 22 in Source/Fn/Binary/Command/Parallel.rs

View workflow job for this annotation

GitHub Actions / Build (stable)

unknown start of token: \u{3000}

/// Represents a command that has been pre-processed for efficient execution.
///
/// This struct holds the command string and booleans indicating whether the
/// command requires a GPG lock or an index-lock wait, preventing redundant
/// classification work inside the main execution loop.
struct ProcessedCommand {
Command:String,
RequiresGpgLock:bool,
RequiresIndexLock:bool,
Command: String,

Check failure on line 25 in Source/Fn/Binary/Command/Parallel.rs

View workflow job for this annotation

GitHub Actions / Build (stable)

unknown start of token: \u{3000}
RequiresGpgLock: bool,

Check failure on line 26 in Source/Fn/Binary/Command/Parallel.rs

View workflow job for this annotation

GitHub Actions / Build (stable)

unknown start of token: \u{3000}
RequiresIndexLock: bool,

Check failure on line 27 in Source/Fn/Binary/Command/Parallel.rs

View workflow job for this annotation

GitHub Actions / Build (stable)

unknown start of token: \u{3000}
}

/// Executes commands in parallel across multiple directories.
///
/// This function orchestrates a complex workflow:
/// 1. Pre-classifies all user-provided commands for lock requirements.
/// 2. Filters the candidate paths to identify target execution directories.
/// 3. Sets up a multi-producer, single-consumer channel for work distribution.
/// 4. Spawns a pool of Tokio worker tasks.
/// 5. Each worker pulls a directory from the queue and executes all commands
/// **sequentially** within it via `sh -c`, preserving order and preventing
/// index-lock conflicts between chained git commands.
/// 6. Before any index-modifying git command the worker waits for
/// `.git/index.lock` to be released, handling both active locks from other
/// processes and stale locks left by previously killed processes.
/// 7. A dedicated output task prints results to stdout as they arrive.
pub async fn Fn(Option:ExecutionOption) {
// 1. Pre-process commands: classify lock requirements once.
let ProcessedCommands:Arc<Vec<ProcessedCommand>> = Arc::new(
pub async fn Fn(Option: ExecutionOption, Tx: Sender<Event>) {

Check failure on line 30 in Source/Fn/Binary/Command/Parallel.rs

View workflow job for this annotation

GitHub Actions / Build (stable)

unknown start of token: \u{3000}

Check failure on line 30 in Source/Fn/Binary/Command/Parallel.rs

View workflow job for this annotation

GitHub Actions / Build (stable)

unknown start of token: \u{3000}
let TotalCommands = Option.Command.len();

let ProcessedCommands: Arc<Vec<ProcessedCommand>> = Arc::new(

Check failure on line 33 in Source/Fn/Binary/Command/Parallel.rs

View workflow job for this annotation

GitHub Actions / Build (stable)

unknown start of token: \u{3000}
Option
.Command
.par_iter()
.map(|CommandString| {
let RequiresGpgLock = GPG::Fn(CommandString);
let RequiresIndexLock = Index::Fn(CommandString);
ProcessedCommand { Command:CommandString.clone(), RequiresGpgLock, RequiresIndexLock }
ProcessedCommand { Command: CommandString.clone(), RequiresGpgLock, RequiresIndexLock }

Check failure on line 40 in Source/Fn/Binary/Command/Parallel.rs

View workflow job for this annotation

GitHub Actions / Build (stable)

unknown start of token: \u{3000}
})
.collect(),
);

// 2. Identify target directories based on the pattern.
let TargetDirs:Vec<PathBuf> = Option
let TargetDirs: Vec<PathBuf> = Option

Check failure on line 45 in Source/Fn/Binary/Command/Parallel.rs

View workflow job for this annotation

GitHub Actions / Build (stable)

unknown start of token: \u{3000}
.Entry
.into_par_iter()
.filter_map(|CandidatePath| {
Expand All @@ -73,63 +55,44 @@
.collect();

if TargetDirs.is_empty() {
let _ = Tx.send(Event::AllDone).await;
return;
}

// 3. Set up the work queue and the results channel.
let (Tx, mut Rx) = mpsc::unbounded_channel::<String>();
let WorkQueue = Arc::new(ArrayQueue::new(TargetDirs.len()));
for Dir in TargetDirs {
WorkQueue
.push(Dir)
.expect("Queue should have enough capacity for all target directories.");
WorkQueue.push(Dir).expect("Queue capacity pre-allocated");
}

// 4. Spawn the output task to print results from the channel.
let OutputTask = tokio::spawn(async move {
while let Some(Output) = Rx.recv().await {
if !Output.trim().is_empty() {
println!("{}", Output);
}
}
});

// 5. Spawn worker tasks, one for each available CPU core.
let WorkerCount = rayon::current_num_threads();
let mut WorkerHandles = Vec::with_capacity(WorkerCount);

for _ in 0..WorkerCount {
let Queue = Arc::clone(&WorkQueue);
let Commands = Arc::clone(&ProcessedCommands);
let Producer = Tx.clone();

let WorkerHandle = tokio::spawn(async move {
while let Some(Directory) = Queue.pop() {
let DirectoryString = Directory.to_string_lossy();

// Commands are executed sequentially within each directory.
// This preserves the user-supplied order (e.g. `git add` before
// `git commit`) and ensures only one command at a time holds
// the git index lock for this repository.
//
// All output for this directory is collected into a single
// buffer and sent atomically through the channel so that
// results from different directories do not interleave.
let mut DirectoryOutput = String::new();

'commands: for Cmd in Commands.iter() {
// Wait for any in-flight index lock before writing to the index.
let DirectoryString = Directory.to_string_lossy().to_string();

let _ = Producer.send(Event::JobStarted {
Directory: DirectoryString.clone(),

Check failure on line 80 in Source/Fn/Binary/Command/Parallel.rs

View workflow job for this annotation

GitHub Actions / Build (stable)

unknown start of token: \u{3000}
Total: TotalCommands,
}).await;

let mut AllSuccess = true;

'commands: for (CmdIdx, Cmd) in Commands.iter().enumerate() {
if Cmd.RequiresIndexLock
&& !Index::Lock::Fn(&DirectoryString).await
{
eprintln!(
"Skipping remaining commands in '{}': git index lock timed out.",
DirectoryString
);
let _ = Producer.send(Event::IndexLockTimeout {
Directory: DirectoryString.clone(),
}).await;
break 'commands;
}

// Hold the GPG mutex for the entire duration of the process
// so the GPG agent is not shared concurrently.
let Result = if Cmd.RequiresGpgLock {
let _GpgLock = GPG_MUTEX.lock().await;
Process::Fn(&Cmd.Command, &DirectoryString).await
Expand All @@ -138,33 +101,47 @@
};

match Result {
Ok(Output) => DirectoryOutput.push_str(&Output),
Ok(Output) => {
for Line in Output.lines() {
if !Line.trim().is_empty() {
let _ = Producer.send(Event::Line {
Directory: DirectoryString.clone(),
Text: Line.to_owned(),
IsStderr: false,
}).await;
}
}
}
Err(Error) => {
eprintln!(
"Error executing command in '{}': {}",
DirectoryString, Error
)
},
let _ = Producer.send(Event::Line {
Directory: DirectoryString.clone(),
Text: format!("Error: {}", Error),
IsStderr: true,
}).await;
AllSuccess = false;
}
}
}

if !DirectoryOutput.trim().is_empty()
&& Producer.send(DirectoryOutput).is_err()
{
// Receiver dropped - stop processing entirely.
break;
let _ = Producer.send(Event::JobProgress {
Directory: DirectoryString.clone(),
Done: CmdIdx + 1,
Total: TotalCommands,
Success: AllSuccess,
}).await;
}

let _ = Producer.send(Event::JobFinished {
Directory: DirectoryString.clone(),
Success: AllSuccess,
}).await;
}
});
WorkerHandles.push(WorkerHandle);
}

// Wait for all workers to complete their tasks.
for Handle in WorkerHandles {
Handle.await.expect("Worker task panicked.");
}

// Drop the original producer to signal the output task to terminate.
drop(Tx);
OutputTask.await.expect("Output task panicked.");
let _ = Tx.send(Event::AllDone).await;
}
Loading
Loading