gitu/lib.rs
1pub mod app;
2mod bindings;
3mod calling_process;
4mod capped_input;
5pub mod cli;
6mod cmd_log;
7pub mod config;
8mod config_edit;
9mod config_git;
10mod diff_renderer;
11pub mod error;
12mod file_watcher;
13mod git;
14mod gitu_diff;
15mod item_data;
16mod items;
17mod key_parser;
18mod menu;
19mod ops;
20mod picker;
21mod prompt;
22mod rebase_todo;
23mod screen;
24mod search;
25mod stream;
26mod style;
27pub mod term;
28#[cfg(test)]
29mod tests;
30mod text_input;
31mod ui;
32
33use bindings::Bindings;
34use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventState, KeyModifiers};
35use error::Error;
36use git2::Repository;
37use items::Item;
38use ops::Action;
39use std::{
40 env,
41 io::{self, IsTerminal},
42 path::Path,
43 rc::Rc,
44 sync::Arc,
45 time::Duration,
46};
47use term::Term;
48
49use crate::calling_process::GitCommand;
50use crate::capped_input::Capped;
51use crate::config::Config;
52use crate::screen::pager::Paged;
53
54pub const LOG_FILE_NAME: &str = "gitu.log";
55
56// An overview of Gitu's ui and terminology:
57//
58// Screen (src/screen/*)
59// │
60// ▼
61// ┌──────────────────────────────────────────────────────────────────┐
62// Item───┬─► Branch: master │
63// Item └─► Your branch is up to date with 'origin/master'. │
64// ... │ │
65// │ Untracked files │
66// │ src/tests/rebase.rs │
67// │ │
68// │ Unstaged changes (4) │
69// │▌modified src/keybinds.rs… │
70// │ modified src/ops/mod.rs… │
71// │ modified src/ops/rebase.rs… │
72// │ modified src/tests/mod.rs… │
73// │ │
74// │ Stashes │
75// │ stash@0 On master: scroll │
76// │ stash@1 WIP on fix/run-cmd-error-on-bad-exit: 098d14a feat: prom…│
77// │ │
78// │ Recent commits │
79// Ops (src/ops/*) ├──────────────────────────────────────────────────────────────────┤
80// │ │Help Submenu modified src/keybinds.r│
81// └─────┬───►g Refresh h Help ret Show │
82// └───►tab Toggle section b Branch K Discard │
83// │k p ↑ Move up c Commit s Stage │
84// │j n ↓ Move down f Fetch u Unstage │
85// Submenu ───────►│C-k C-p C-↑ Move up line l Log │
86// │C-j C-n C-↓ Move down line F Pull │
87// │C-u Half page up P Push │
88// │C-d Half page down r Rebase │
89// │y Show refs X Reset │
90// │ z Stash │
91// └──────────────────────────────────────────────────────────────────┘
92
93pub type Res<T> = Result<T, Error>;
94
95/// Run gitu, returning the status to exit with.
96pub fn run(config: Arc<Config>, args: &cli::Args, term: &mut Term) -> Res<i32> {
97 // Before anything slower, and before reading the input: the git process on
98 // the other end of a pipe exits as soon as its output fits in the pipe, and
99 // cannot be found once it has. Reading to EOF is always too late — EOF is
100 // git closing the pipe.
101 let paging = pages(args, io::stdin().is_terminal());
102 let git_argv = paging.then(calling_process::paging_for).flatten();
103 log::debug!("Paging the output of {git_argv:?}");
104
105 log::debug!("Opening repo");
106 let repo = open_repo_from_env()?;
107
108 let piped = paging
109 .then(|| read_piped_input(git_argv, repo.workdir(), config.general.max_input_bytes))
110 .transpose()?;
111
112 let mut app = app::App::create(
113 Rc::new(repo),
114 term.size().map_err(Error::Term)?,
115 args,
116 Arc::clone(&config),
117 true,
118 piped,
119 )?;
120
121 if args.quit_if_one_screen && paging && app.print_if_one_screen(term)? {
122 return Ok(app.state.exit_code);
123 }
124
125 if args.print {
126 return run_app(&mut app, args, term);
127 }
128
129 term.setup_term(&config).map_err(Error::Term)?;
130 let result = run_app(&mut app, args, term);
131 term.reset_term(&config).map_err(Error::Term)?;
132 result
133}
134
135/// Whether gitu browses a patch on stdin, as git's pager, rather than opening
136/// the status view or a subcommand's screen.
137pub(crate) fn pages(args: &cli::Args, stdin_is_terminal: bool) -> bool {
138 args.command.is_none() && !stdin_is_terminal
139}
140
141fn run_app(app: &mut app::App, args: &cli::Args, term: &mut Term) -> Res<i32> {
142 if let Some(keys_string) = &args.keys {
143 let ("", keys) = key_parser::parse_keys(keys_string).expect("Couldn't parse keys") else {
144 panic!("Couldn't parse keys");
145 };
146
147 for event in keys_to_events(&keys) {
148 app.handle_event(term, event)?;
149 }
150 }
151
152 app.redraw_now(term)?;
153
154 if args.print {
155 return Ok(app.state.exit_code);
156 }
157
158 app.run(term, Duration::from_millis(100))?;
159
160 Ok(app.state.exit_code)
161}
162
163/// What git piped to us as its pager, alongside the command already found to
164/// have produced it.
165fn read_piped_input(
166 git_argv: Option<Vec<String>>,
167 workdir: Option<&Path>,
168 limit: u64,
169) -> Res<Paged> {
170 paged(io::stdin(), git_argv, workdir, limit)
171}
172
173/// What git piped, unless gitu found the command that produced it: then gitu
174/// asks git again itself (see [`screen::pager`]), and reading the output would
175/// only wait for git to write what is discarded. git, left blocked on a full
176/// pipe, ends with SIGPIPE when gitu exits, as it does when less quits early.
177fn paged(
178 input: impl io::Read,
179 git_argv: Option<Vec<String>>,
180 workdir: Option<&Path>,
181 limit: u64,
182) -> Res<Paged> {
183 if git_argv.clone().and_then(GitCommand::of).is_some() {
184 return Ok(Paged {
185 text: String::new(),
186 truncated: false,
187 git_argv,
188 prefix: command_prefix(workdir),
189 });
190 }
191
192 let input = Capped::read(input, limit).map_err(Error::ReadPipedInput)?;
193 Ok(Paged {
194 text: input.text,
195 truncated: input.truncated,
196 git_argv,
197 prefix: String::new(),
198 })
199}
200
201/// Where git ran the command it is paging (see [`GitCommand::prefix`]). git
202/// runs its pager from the repository's root and says in `GIT_PREFIX` where it
203/// was run; in a shell pipeline, gitu runs where git did, in `workdir`.
204fn command_prefix(workdir: Option<&Path>) -> String {
205 if let Ok(prefix) = env::var("GIT_PREFIX") {
206 return prefix;
207 }
208
209 let relative = || {
210 let dir = env::current_dir().ok()?.canonicalize().ok()?;
211 let root = workdir?.canonicalize().ok()?;
212 Some(dir.strip_prefix(root).ok()?.to_str()?.to_owned())
213 };
214 match relative() {
215 Some(dir) if !dir.is_empty() => format!("{dir}/"),
216 _ => String::new(),
217 }
218}
219
220fn open_repo(dir: &Path) -> Res<Repository> {
221 log::debug!("Opening repo");
222 let repo = open_repo_from_env()?;
223 repo.set_workdir(dir, false).map_err(Error::OpenRepo)?;
224 Ok(repo)
225}
226
227fn open_repo_from_env() -> Res<Repository> {
228 match Repository::open_from_env() {
229 Ok(repo) => Ok(repo),
230 Err(err) => Err(Error::OpenRepo(err)),
231 }
232}
233
234fn keys_to_events(keys: &[(KeyModifiers, KeyCode)]) -> Vec<Event> {
235 keys.iter()
236 .map(|(mods, key)| {
237 Event::Key(KeyEvent {
238 code: *key,
239 modifiers: *mods,
240 kind: event::KeyEventKind::Press,
241 state: KeyEventState::NONE,
242 })
243 })
244 .collect::<Vec<_>>()
245}