Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added
* support global options file and `GITUI_CONFIG_DIR` / `--config-dir` [[@amaanq](https://github.com/amaanq)] ([#2852](https://github.com/gitui-org/gitui/pull/2852))

## [0.28.0] - 2025-12-14

**discard changes on checkout**
Expand Down
25 changes: 17 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@
9. [Diagnostics](#diagnostics)
10. [Color Theme](#theme)
11. [Key Bindings](#bindings)
12. [Sponsoring](#sponsoring)
13. [Inspiration](#inspiration)
14. [Contributing](#contributing)
15. [Contributors](#contributors)
12. [Options](#options)
13. [Sponsoring](#sponsoring)
14. [Inspiration](#inspiration)
15. [Contributing](#contributing)
16. [Contributors](#contributors)

## 1. <a name="features"></a> Features <small><sup>[Top ▲](#table-of-contents)</sup></small>

Expand Down Expand Up @@ -268,23 +269,31 @@ However, you can customize everything to your liking: See [Themes](THEMES.md).

The key bindings can be customized: See [Key Config](KEY_CONFIG.md) on how to set them to `vim`-like bindings.

## 12. <a name="sponsoring"></a> Sponsoring <small><sup>[Top ▲](#table-of-contents)</sup></small>
## 12. <a name="options"></a> Options <small><sup>[Top ▲](#table-of-contents)</sup></small>

All config files (theme, key bindings, options) are loaded from `~/.config/gitui/` (Linux/macOS) or `%APPDATA%/gitui/` (Windows).

Use `--config-dir` / `-c` or set `GITUI_CONFIG_DIR` to use a custom config directory (e.g., `/etc/gitui`).

Options are stored per-repo in `.git/gitui.ron`. If no local config exists, gitui looks for `gitui.ron` in the config directory.

## 13. <a name="sponsoring"></a> Sponsoring <small><sup>[Top ▲](#table-of-contents)</sup></small>

[![github](https://img.shields.io/badge/-GitHub%20Sponsors-fafbfc?logo=GitHub%20Sponsors)](https://github.com/sponsors/extrawurst)

## 13. <a name="inspiration"></a> Inspiration <small><sup>[Top ▲](#table-of-contents)</sup></small>
## 14. <a name="inspiration"></a> Inspiration <small><sup>[Top ▲](#table-of-contents)</sup></small>

- [lazygit](https://github.com/jesseduffield/lazygit)
- [tig](https://github.com/jonas/tig)
- [GitUp](https://github.com/git-up/GitUp)
- It would be nice to come up with a way to have the map view available in a terminal tool
- [git-brunch](https://github.com/andys8/git-brunch)

## 14. <a name="contributing"></a> Contributing <small><sup>[Top ▲](#table-of-contents)</sup></small>
## 15. <a name="contributing"></a> Contributing <small><sup>[Top ▲](#table-of-contents)</sup></small>

See [CONTRIBUTING.md](CONTRIBUTING.md).

## 15. <a name="contributors"></a> Contributors <small><sup>[Top ▲](#table-of-contents)</sup></small>
## 16. <a name="contributors"></a> Contributors <small><sup>[Top ▲](#table-of-contents)</sup></small>

Thanks goes to all the contributors that help make GitUI amazing! ❤️

Expand Down
1 change: 1 addition & 0 deletions asyncgit/src/sync/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ pub struct FileDiff {
#[derive(
Debug, Hash, Clone, Copy, PartialEq, Eq, Serialize, Deserialize,
)]
#[serde(default)]
pub struct DiffOptions {
/// see <https://libgit2.org/libgit2/#HEAD/type/git_diff_options>
pub ignore_whitespace: bool,
Expand Down
52 changes: 49 additions & 3 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const GIT_DIR_FLAG_ID: &str = "directory";
const WATCHER_FLAG_ID: &str = "watcher";
const KEY_BINDINGS_FLAG_ID: &str = "key_bindings";
const KEY_SYMBOLS_FLAG_ID: &str = "key_symbols";
const CONFIG_DIR_FLAG_ID: &str = "config_dir";
const DEFAULT_THEME: &str = "theme.ron";
const DEFAULT_GIT_DIR: &str = ".";

Expand Down Expand Up @@ -68,6 +69,13 @@ pub fn process_cmdline() -> Result<CliArgs> {
RepoPath::Path(gitdir)
};

// Set GITUI_CONFIG_DIR env var early so get_app_config_path() picks it up
if let Some(config_dir) =
arg_matches.get_one::<String>(CONFIG_DIR_FLAG_ID)
{
env::set_var("GITUI_CONFIG_DIR", config_dir);
}

let arg_theme = arg_matches
.get_one::<String>(THEME_FLAG_ID)
.map_or_else(|| PathBuf::from(DEFAULT_THEME), PathBuf::from);
Expand Down Expand Up @@ -134,6 +142,15 @@ fn app() -> ClapApp {
.value_name("KEY_SYMBOLS_FILENAME")
.num_args(1),
)
.arg(
Arg::new(CONFIG_DIR_FLAG_ID)
.help("Use a custom config directory")
.short('c')
.long("config-dir")
.env("GITUI_CONFIG_DIR")
.value_name("CONFIG_DIR")
.num_args(1),
)
.arg(
Arg::new(THEME_FLAG_ID)
.help("Set color theme filename loaded from config directory")
Expand Down Expand Up @@ -227,6 +244,11 @@ fn get_app_cache_path() -> Result<PathBuf> {
}

pub fn get_app_config_path() -> Result<PathBuf> {
// Check GITUI_CONFIG_DIR first, then fall back to default
if let Ok(config_dir) = env::var("GITUI_CONFIG_DIR") {
return Ok(PathBuf::from(config_dir));
}

let mut path = if cfg!(target_os = "macos") {
dirs::home_dir().map(|h| h.join(".config"))
} else {
Expand All @@ -238,7 +260,31 @@ pub fn get_app_config_path() -> Result<PathBuf> {
Ok(path)
}

#[test]
fn verify_app() {
app().debug_assert();
#[cfg(test)]
mod tests {
use super::*;
use std::env;

#[test]
fn verify_app() {
app().debug_assert();
}

// env var tests must be serial since they mutate process state
#[test]
fn config_path_env_var_and_fallback() {
// with env var set, should return the custom path
let custom = "/tmp/gitui-test-config";
env::set_var("GITUI_CONFIG_DIR", custom);
let path = get_app_config_path().unwrap();
assert_eq!(path, PathBuf::from(custom));

// without env var, should fall back to default
env::remove_var("GITUI_CONFIG_DIR");
let path = get_app_config_path().unwrap();
assert!(
path.ends_with("gitui"),
"expected path ending in 'gitui', got: {path:?}"
);
}
}
62 changes: 59 additions & 3 deletions src/options.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use crate::args::get_app_config_path;

use anyhow::Result;
use asyncgit::sync::{
diff::DiffOptions, repo_dir, RepoPathRef,
Expand All @@ -17,6 +19,7 @@ use std::{
};

#[derive(Default, Clone, Serialize, Deserialize)]
#[serde(default)]
struct OptionsData {
pub tab: usize,
pub diff: DiffOptions,
Expand All @@ -26,6 +29,8 @@ struct OptionsData {

const COMMIT_MSG_HISTORY_LENGTH: usize = 20;

const OPTIONS_FILENAME: &str = "gitui.ron";

#[derive(Clone)]
pub struct Options {
repo: RepoPathRef,
Expand Down Expand Up @@ -144,9 +149,18 @@ impl Options {
}

fn read(repo: &RepoPathRef) -> Result<OptionsData> {
let dir = Self::options_file(repo)?;
let local_file = Self::options_file(repo)?;

// Precedence: local -> global (respects GITUI_CONFIG_DIR)
let mut f = match File::open(&local_file) {
Ok(file) => file,
Err(_) => {
let app_home = get_app_config_path()?;
let global_file = app_home.join(OPTIONS_FILENAME);
File::open(global_file)?
}
};

let mut f = File::open(dir)?;
let mut buffer = Vec::new();
f.read_to_end(&mut buffer)?;
Ok(from_bytes(&buffer)?)
Expand All @@ -165,7 +179,49 @@ impl Options {

fn options_file(repo: &RepoPathRef) -> Result<PathBuf> {
let dir = repo_dir(&repo.borrow())?;
let dir = dir.join("gitui");
let dir = dir.join(OPTIONS_FILENAME);
Ok(dir)
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::{env, fs};

#[test]
fn read_falls_back_to_global_config() {
let global_dir = tempfile::tempdir().unwrap();
let global_file =
global_dir.path().join(OPTIONS_FILENAME);
fs::write(
&global_file,
"(diff: (ignore_whitespace: true))",
)
.unwrap();

env::set_var(
"GITUI_CONFIG_DIR",
global_dir.path().to_str().unwrap(),
);

// Init a real git repo so repo_dir() works
let repo_dir = tempfile::tempdir().unwrap();
std::process::Command::new("git")
.args(["init"])
.current_dir(repo_dir.path())
.output()
.unwrap();
let git_dir = repo_dir.path().join(".git");
let repo = RefCell::new(
asyncgit::sync::RepoPath::Path(
git_dir.to_path_buf(),
),
);

let data = Options::read(&repo).unwrap();
assert!(data.diff.ignore_whitespace);

env::remove_var("GITUI_CONFIG_DIR");
}
}