pub(crate) mod command;
mod completion;
mod package_manager;
mod render;
mod update;
mod watch;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fs;
use std::io::{self, IsTerminal, Write};
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::time::Duration;
use anyhow::{Context, Result};
use clap::{Args, CommandFactory, Parser, Subcommand, ValueHint};
use clap_complete::Shell;
use clap_mangen::Man;
pub(crate) use command::{Command, Outcome, Session};
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
use serde::Serialize;
use crate::git::diff::{DiffDocument, DiffIndex};
use crate::git::github::{
CheckRunLog, GitHubRepository, PullRequest, PullRequestCheck, PullRequestCheckStatus,
PullRequestDiffIndex, PullRequestMergeMethod, PullRequestOperation, PullRequestSnapshot,
};
use crate::git::status::{Change, ChangeArea};
use crate::git::{ConflictChoice, GitOperation, LocalDiffRequest, Repository};
use crate::theme::{AppearanceChoice, ThemeName};
pub(crate) const EXIT_FAILURE: u8 = 1;
pub(crate) const EXIT_NOT_FOUND: u8 = 3;
pub(crate) const EXIT_UNAVAILABLE: u8 = 4;
const PROGRAM: &str = "quinjet";
const ROOT_HELP: &str = "Examples:
quinjet
quinjet status --json
quinjet -C ../project diff --staged
quinjet pr checks 42 --watch
Documentation: https://github.com/pulkitxm/quinjet/wiki/Command-Line";
const CHECK_WATCH_INTERVAL: u64 = 5;
const CHECK_WATCH_FLOOR: u64 = 2;
const LOG_WATCH_INTERVAL: u64 = 8;
const LOG_WATCH_FLOOR: u64 = 3;
#[derive(Debug)]
pub(crate) struct Failure {
pub code: u8,
pub message: String,
pub hint: Option<String>,
}
impl Failure {
pub(crate) fn new(code: u8, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
hint: None,
}
}
pub(crate) fn hint(mut self, hint: impl Into<String>) -> Self {
self.hint = Some(hint.into());
self
}
}
impl std::fmt::Display for Failure {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "{}", self.message)
}
}
impl std::error::Error for Failure {}
pub(crate) struct TerminalOptions {
pub path: PathBuf,
pub no_mouse: bool,
pub webhook_listen: Option<String>,
pub theme: ThemeName,
pub appearance: AppearanceChoice,
pub pull_request: Option<u64>,
}
#[expect(
variant_size_differences,
reason = "the terminal options are already boxed; the only way to level this is to box one byte"
)]
pub(crate) enum Launch {
Terminal(Box<TerminalOptions>),
Finished(u8),
}
#[derive(Debug, Parser)]
#[command(name = "quinjet", version, about)]
#[command(subcommand_negates_reqs = true)]
#[command(propagate_version = true)]
#[command(after_long_help = ROOT_HELP)]
struct Cli {
#[command(subcommand)]
command: Option<Verb>,
#[arg(
short = 'C',
long = "path",
value_name = "DIR",
default_value = ".",
global = true,
value_hint = ValueHint::DirPath
)]
repository: PathBuf,
#[arg(long, global = true)]
json: bool,
#[arg(long = "pr", value_name = "NUMBER")]
pull_request: Option<u64>,
}
#[derive(Debug, Subcommand)]
enum Verb {
Tui(TuiArgs),
Status(WatchableArgs),
Diff(DiffArgs),
Stage(SelectionArgs),
Unstage(SelectionArgs),
Discard(DiscardArgs),
#[command(visible_alias = "rm")]
Remove(RemoveArgs),
Commit(CommitArgs),
Fetch,
Pull,
Push,
Sync,
Log(LogArgs),
Show(ShowArgs),
Branch {
#[command(subcommand)]
command: BranchVerb,
},
Stash {
#[command(subcommand)]
command: StashVerb,
},
Worktree {
#[command(subcommand)]
command: WorktreeVerb,
},
CherryPick(RevisionArgs),
Revert(RevisionArgs),
Resolve(ResolveArgs),
Repos(ReposArgs),
Pr {
#[command(subcommand)]
command: PrVerb,
},
#[command(visible_alias = "completion")]
Completions(CompletionsArgs),
Man(ManArgs),
Capabilities,
Update(UpdateArgs),
}
impl Verb {
const fn progress_label(&self) -> Option<&'static str> {
match self {
Self::Tui(_) | Self::Completions(_) | Self::Man(_) | Self::Capabilities => None,
Self::Status(args) if args.watch => None,
Self::Status(_) => Some("Reading repository status"),
Self::Diff(_) => Some("Loading the working-tree diff"),
Self::Stage(_) => Some("Staging changes"),
Self::Unstage(_) => Some("Unstaging changes"),
Self::Discard(_) => Some("Reading changes to discard"),
Self::Remove(_) => Some("Reading files to remove"),
Self::Commit(_) => Some("Creating commit"),
Self::Fetch => Some("Fetching remotes"),
Self::Pull => Some("Pulling changes"),
Self::Push => Some("Pushing changes"),
Self::Sync => Some("Synchronizing changes"),
Self::Log(_) => Some("Reading commit history"),
Self::Show(_) => Some("Loading commit patch"),
Self::Branch { .. } => Some("Reading branch state"),
Self::Stash { .. } => Some("Reading stash state"),
Self::Worktree { .. } => Some("Reading worktrees"),
Self::CherryPick(_) => Some("Resolving commit to cherry-pick"),
Self::Revert(_) => Some("Resolving commit to revert"),
Self::Resolve(_) => Some("Resolving conflict"),
Self::Repos(_) => Some("Discovering GitHub repositories"),
Self::Pr {
command: PrVerb::View(args) | PrVerb::Conversation(args),
} if args.watch => None,
Self::Pr {
command: PrVerb::Checks(args),
} if args.watch => None,
Self::Pr {
command: PrVerb::Logs(args),
} if args.watch => None,
Self::Pr {
command: PrVerb::Merge(_) | PrVerb::Close(_) | PrVerb::Reopen(_),
} => Some("Updating pull request"),
Self::Pr { .. } => Some("Loading pull request"),
Self::Update(_) => Some("Checking for updates"),
}
}
}
#[derive(Debug, Args)]
struct CompletionsArgs {
#[arg(value_enum, required_unless_present = "install")]
shell: Option<Shell>,
#[arg(long)]
install: bool,
#[arg(long, hide = true, requires = "install")]
automatic: bool,
}
#[derive(Debug, Args)]
struct ManArgs {
#[arg(long, value_name = "DIR", value_hint = ValueHint::DirPath)]
dir: Option<PathBuf>,
}
#[derive(Debug, Args)]
struct UpdateArgs {
#[arg(long)]
check: bool,
}
#[derive(Debug, Args)]
struct TuiArgs {
#[arg(default_value = ".", value_hint = ValueHint::DirPath)]
path: PathBuf,
#[arg(long)]
no_mouse: bool,
#[arg(long, value_name = "ADDRESS")]
webhook_listen: Option<String>,
#[arg(long, value_enum, default_value_t)]
theme: ThemeName,
#[arg(long, value_enum, default_value_t)]
appearance: AppearanceChoice,
#[arg(long = "pr", value_name = "NUMBER")]
pull_request: Option<u64>,
}
#[derive(Debug, Args)]
struct WatchableArgs {
#[arg(long)]
watch: bool,
#[arg(
long,
value_name = "SECONDS",
default_value_t = 2,
requires = "watch",
value_parser = clap::value_parser!(u64).range(1..),
value_hint = ValueHint::Other
)]
interval: u64,
}
#[derive(Debug, Args)]
struct DiffArgs {
#[arg(value_name = "PATH", value_hint = ValueHint::AnyPath)]
paths: Vec<PathBuf>,
#[arg(long)]
staged: bool,
#[arg(long, conflicts_with = "staged")]
unstaged: bool,
#[arg(long)]
expanded: bool,
}
#[derive(Debug, Args)]
#[group(required = true, multiple = false)]
struct SelectionArgs {
#[arg(value_name = "PATH", value_hint = ValueHint::AnyPath)]
paths: Vec<PathBuf>,
#[arg(long, conflicts_with = "paths")]
all: bool,
}
#[derive(Debug, Args)]
struct DiscardArgs {
#[command(flatten)]
selection: SelectionArgs,
#[arg(long)]
yes: bool,
}
#[derive(Debug, Args)]
struct RemoveArgs {
#[command(flatten)]
selection: SelectionArgs,
#[arg(long)]
yes: bool,
}
#[derive(Debug, Args)]
struct CommitArgs {
#[arg(short, long)]
message: String,
#[arg(long)]
amend: bool,
}
#[derive(Debug, Args)]
struct LogArgs {
#[arg(default_value = "HEAD", value_name = "REVISION", value_hint = ValueHint::Other)]
revision: String,
#[arg(long, default_value_t = 0, value_hint = ValueHint::Other)]
skip: usize,
#[arg(long, short = 'n', default_value_t = 30, value_hint = ValueHint::Other)]
limit: usize,
}
#[derive(Debug, Args)]
struct ShowArgs {
#[arg(default_value = "HEAD", value_name = "REVISION", value_hint = ValueHint::Other)]
revision: String,
#[arg(long)]
expanded: bool,
}
#[derive(Debug, Args)]
struct RevisionArgs {
#[arg(value_name = "REVISION", value_hint = ValueHint::Other)]
revision: String,
#[arg(long)]
yes: bool,
}
#[derive(Debug, Args)]
struct ResolveArgs {
#[arg(value_name = "PATH", value_hint = ValueHint::AnyPath)]
path: PathBuf,
#[command(flatten)]
side: ConflictSide,
}
#[derive(Debug, Args)]
#[group(required = true, multiple = false)]
struct ConflictSide {
#[arg(long)]
ours: bool,
#[arg(long)]
theirs: bool,
#[arg(long)]
stage: bool,
}
mod entry;
mod output;
mod pull_request;
mod repository;
pub(crate) mod support;
mod verbs;
pub(crate) use entry::dispatch;
#[cfg_attr(not(test), expect(clippy::wildcard_imports, reason = "shared"))]
use output::*;
#[cfg_attr(not(test), expect(clippy::wildcard_imports, reason = "shared"))]
use pull_request::*;
#[cfg_attr(not(test), expect(clippy::wildcard_imports, reason = "shared"))]
use repository::*;
#[cfg_attr(not(test), expect(clippy::wildcard_imports, reason = "shared"))]
use support::*;
pub(crate) use support::{open_url, report, stdout_is_terminal};
#[cfg_attr(not(test), expect(clippy::wildcard_imports, reason = "shared"))]
use verbs::*;
#[cfg(test)]
#[expect(
unused_results,
reason = "test helpers return values the assertions do not use"
)]
mod tests;