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