Skip to main content

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    needs_terminal(io::stdout().is_terminal())?;
98    // Before anything slower, and before reading the input: the git process on
99    // the other end of a pipe exits as soon as its output fits in the pipe, and
100    // cannot be found once it has. Reading to EOF is always too late — EOF is
101    // git closing the pipe.
102    let paging = pages(args, io::stdin().is_terminal());
103    let git_argv = paging.then(calling_process::paging_for).flatten();
104    log::debug!("Paging the output of {git_argv:?}");
105
106    log::debug!("Opening repo");
107    let repo = open_repo_from_env()?;
108
109    let piped = paging
110        .then(|| read_piped_input(git_argv, repo.workdir(), config.general.max_input_bytes))
111        .transpose()?;
112
113    let mut app = app::App::create(
114        Rc::new(repo),
115        term.size().map_err(Error::Term)?,
116        args,
117        Arc::clone(&config),
118        true,
119        piped,
120    )?;
121
122    if args.quit_if_one_screen && paging && app.print_if_one_screen(term)? {
123        return Ok(app.state.exit_code);
124    }
125
126    if args.print {
127        return run_app(&mut app, args, term);
128    }
129
130    term.setup_term(&config).map_err(Error::Term)?;
131    let result = run_app(&mut app, args, term);
132    term.reset_term(&config).map_err(Error::Term)?;
133    result
134}
135
136/// gitu draws on its output, which must be a terminal.
137pub(crate) fn needs_terminal(stdout_is_terminal: bool) -> Res<()> {
138    if stdout_is_terminal {
139        Ok(())
140    } else {
141        Err(Error::NotATerminal)
142    }
143}
144
145/// Whether gitu browses a patch on stdin, as git's pager, rather than opening
146/// the status view or a subcommand's screen.
147pub(crate) fn pages(args: &cli::Args, stdin_is_terminal: bool) -> bool {
148    args.command.is_none() && !stdin_is_terminal
149}
150
151fn run_app(app: &mut app::App, args: &cli::Args, term: &mut Term) -> Res<i32> {
152    if let Some(keys_string) = &args.keys {
153        let ("", keys) = key_parser::parse_keys(keys_string).expect("Couldn't parse keys") else {
154            panic!("Couldn't parse keys");
155        };
156
157        for event in keys_to_events(&keys) {
158            app.handle_event(term, event)?;
159        }
160    }
161
162    app.redraw_now(term)?;
163
164    if args.print {
165        return Ok(app.state.exit_code);
166    }
167
168    app.run(term, Duration::from_millis(100))?;
169
170    Ok(app.state.exit_code)
171}
172
173/// What git piped to us as its pager, alongside the command already found to
174/// have produced it.
175fn read_piped_input(
176    git_argv: Option<Vec<String>>,
177    workdir: Option<&Path>,
178    limit: u64,
179) -> Res<Paged> {
180    paged(io::stdin(), git_argv, workdir, limit)
181}
182
183/// What git piped, unless gitu found the command that produced it: then gitu
184/// asks git again itself (see [`screen::pager`]), and reading the output would
185/// only wait for git to write what is discarded. git, left blocked on a full
186/// pipe, ends with SIGPIPE when gitu exits, as it does when less quits early.
187fn paged(
188    input: impl io::Read,
189    git_argv: Option<Vec<String>>,
190    workdir: Option<&Path>,
191    limit: u64,
192) -> Res<Paged> {
193    if git_argv.clone().and_then(GitCommand::of).is_some() {
194        return Ok(Paged {
195            text: String::new(),
196            truncated: false,
197            git_argv,
198            prefix: command_prefix(workdir),
199        });
200    }
201
202    let input = Capped::read(input, limit).map_err(Error::ReadPipedInput)?;
203    Ok(Paged {
204        text: input.text,
205        truncated: input.truncated,
206        git_argv,
207        prefix: String::new(),
208    })
209}
210
211/// Where git ran the command it is paging (see [`GitCommand::prefix`]). git
212/// runs its pager from the repository's root and says in `GIT_PREFIX` where it
213/// was run; in a shell pipeline, gitu runs where git did, in `workdir`.
214fn command_prefix(workdir: Option<&Path>) -> String {
215    if let Ok(prefix) = env::var("GIT_PREFIX") {
216        return prefix;
217    }
218
219    let relative = || {
220        let dir = env::current_dir().ok()?.canonicalize().ok()?;
221        let root = workdir?.canonicalize().ok()?;
222        Some(dir.strip_prefix(root).ok()?.to_str()?.to_owned())
223    };
224    match relative() {
225        Some(dir) if !dir.is_empty() => format!("{dir}/"),
226        _ => String::new(),
227    }
228}
229
230fn open_repo(dir: &Path) -> Res<Repository> {
231    log::debug!("Opening repo");
232    let repo = open_repo_from_env()?;
233    repo.set_workdir(dir, false).map_err(Error::OpenRepo)?;
234    Ok(repo)
235}
236
237fn open_repo_from_env() -> Res<Repository> {
238    match Repository::open_from_env() {
239        Ok(repo) => Ok(repo),
240        Err(err) => Err(Error::OpenRepo(err)),
241    }
242}
243
244fn keys_to_events(keys: &[(KeyModifiers, KeyCode)]) -> Vec<Event> {
245    keys.iter()
246        .map(|(mods, key)| {
247            Event::Key(KeyEvent {
248                code: *key,
249                modifiers: *mods,
250                kind: event::KeyEventKind::Press,
251                state: KeyEventState::NONE,
252            })
253        })
254        .collect::<Vec<_>>()
255}