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