Skip to content

Commit 85b2536

Browse files
authored
UX improvements (#28)
* Allow setting the diff theme * Read configuration from git config as well as arguments and env vars * Choose whether to display full diff or just a diffstat based on terminal height instead of a constant * Add -u alias for --default-upstream-branch
2 parents f7862bf + 94e0694 commit 85b2536

File tree

6 files changed

+175
-61
lines changed

6 files changed

+175
-61
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Unreleased
22

3+
* Allow setting the diff theme
4+
* Read configuration from git config as well as arguments and env vars
5+
* Choose whether to display full diff or just a diffstat based on terminal
6+
height instead of a constant
7+
* Add -u alias for --default-upstream-branch
8+
39
# Version 0.2.2
410

511
* Correctly retarget branches if the target of the edit is also a branch (#24)

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ clap = { version = "4.5.1", features = ["derive", "env", "wrap_help"] }
2121
console = "0.15.8"
2222
dialoguer = "0.11.0"
2323
git2 = { version = "0.18.2", default-features = false }
24+
itertools = "0.12.1"
25+
termcolor = "1.4.1"
26+
terminal_size = "0.3.0"
2427
syntect = "5.2.0"
2528

2629
[dev-dependencies]
2730
assert_cmd = "2.0.13"
2831
assert_fs = "1.1.1"
29-
itertools = "0.12.1"
3032

3133
# The profile that 'cargo dist' will build with
3234
[profile.dist]

src/lib.rs

Lines changed: 76 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::collections::HashMap;
2+
use std::io::Write as _;
23

34
use anyhow::{anyhow, bail, Context};
45
use console::style;
@@ -11,26 +12,43 @@ use syntect::easy::HighlightLines;
1112
use syntect::highlighting::ThemeSet;
1213
use syntect::parsing::SyntaxSet;
1314
use syntect::util::as_24_bit_terminal_escaped;
15+
use termcolor::{ColorChoice, StandardStream, WriteColor as _};
16+
use terminal_size::{terminal_size, Height};
1417

1518
const DEFAULT_UPSTREAM_BRANCHES: &[&str] = &["main", "master", "develop", "trunk"];
19+
pub const DEFAULT_THEME: &str = "base16-ocean.dark";
20+
21+
pub struct Config {
22+
/// Change the commit message that you amend, instead of using the original commit message
23+
pub squash: bool,
24+
/// The maximum number of commits to show when looking for your merge point
25+
pub max_commits: usize,
26+
/// Specify a commit to ammend by the subject line of the commit
27+
pub commit_message_pattern: Option<String>,
28+
pub default_upstream_branch: Option<String>,
29+
/// Require a newline when confirming y/n questions
30+
pub require_newline: bool,
31+
/// Which theme to use
32+
pub theme: String,
33+
}
1634

17-
pub fn instafix(
18-
squash: bool,
19-
max_commits: usize,
20-
message_pattern: Option<String>,
21-
upstream_branch_name: Option<&str>,
22-
require_newline: bool,
23-
) -> Result<(), anyhow::Error> {
35+
pub fn instafix(c: Config) -> Result<(), anyhow::Error> {
2436
let repo = Repository::open(".").context("opening repo")?;
25-
let diff = create_diff(&repo, require_newline).context("creating diff")?;
37+
let diff = create_diff(&repo, &c.theme, c.require_newline).context("creating diff")?;
2638
let head = repo.head().context("finding head commit")?;
2739
let head_branch = Branch::wrap(head);
28-
let upstream =
29-
get_merge_base(&repo, &head_branch, upstream_branch_name).context("creating merge base")?;
30-
let commit_to_amend = select_commit_to_amend(&repo, upstream, max_commits, &message_pattern)
31-
.context("selecting commit to amend")?;
40+
let upstream = get_merge_base(&repo, &head_branch, c.default_upstream_branch.as_deref())
41+
.context("creating merge base")?;
42+
let commit_to_amend = select_commit_to_amend(
43+
&repo,
44+
upstream,
45+
c.max_commits,
46+
c.commit_message_pattern.as_deref(),
47+
)
48+
.context("selecting commit to amend")?;
3249
eprintln!("Selected {}", disp(&commit_to_amend));
33-
do_fixup_commit(&repo, &head_branch, &commit_to_amend, squash).context("doing fixup commit")?;
50+
do_fixup_commit(&repo, &head_branch, &commit_to_amend, c.squash)
51+
.context("doing fixup commit")?;
3452
let needs_stash = worktree_is_dirty(&repo)?;
3553
if needs_stash {
3654
// TODO: is it reasonable to create a new repo to work around lifetime issues?
@@ -239,7 +257,11 @@ fn get_merge_base<'a>(
239257
}
240258

241259
/// Get a diff either from the index or the diff from the index to the working tree
242-
fn create_diff(repo: &Repository, require_newline: bool) -> Result<Diff, anyhow::Error> {
260+
fn create_diff<'a>(
261+
repo: &'a Repository,
262+
theme: &str,
263+
require_newline: bool,
264+
) -> Result<Diff<'a>, anyhow::Error> {
243265
let head = repo.head()?;
244266
let head_tree = head.peel_to_tree()?;
245267
let staged_diff = repo.diff_tree_to_index(Some(&head_tree), None, None)?;
@@ -248,11 +270,18 @@ fn create_diff(repo: &Repository, require_newline: bool) -> Result<Diff, anyhow:
248270
let diff = if diffstat.files_changed() == 0 {
249271
let dirty_workdir_stats = dirty_diff.stats()?;
250272
if dirty_workdir_stats.files_changed() > 0 {
273+
let Height(h) = terminal_size().map(|(_w, h)| h).unwrap_or(Height(24));
274+
let cutoff_height = (h - 5) as usize; // give some room for the prompt
251275
let total_change = dirty_workdir_stats.insertions() + dirty_workdir_stats.deletions();
252-
if total_change < 50 {
253-
native_diff(&dirty_diff)?;
254-
} else {
276+
if total_change >= cutoff_height {
255277
print_diffstat("Unstaged", &dirty_diff)?;
278+
} else {
279+
let diff_lines = native_diff(&dirty_diff, theme)?;
280+
if diff_lines.len() >= cutoff_height {
281+
print_diffstat("Unstaged", &dirty_diff)?;
282+
} else {
283+
print_diff_lines(&diff_lines)?;
284+
}
256285
}
257286
if !Confirm::new()
258287
.with_prompt("Nothing staged, stage and commit everything?")
@@ -312,7 +341,7 @@ fn select_commit_to_amend<'a>(
312341
repo: &'a Repository,
313342
upstream: Option<Object<'a>>,
314343
max_commits: usize,
315-
message_pattern: &Option<String>,
344+
message_pattern: Option<&str>,
316345
) -> Result<Commit<'a>, anyhow::Error> {
317346
let mut walker = repo.revwalk()?;
318347
walker.push_head()?;
@@ -417,15 +446,29 @@ fn format_ref(rf: &git2::Reference<'_>) -> Result<String, anyhow::Error> {
417446
Ok(format!("{} ({})", shorthand, &sha[..10]))
418447
}
419448

449+
/// A vec of all built-in theme names
450+
pub fn print_themes() {
451+
println!("Available themes:");
452+
for theme in ThemeSet::load_defaults().themes.keys() {
453+
println!(" {}", theme);
454+
}
455+
}
456+
420457
// diff helpers
421458

422-
fn native_diff(diff: &Diff<'_>) -> Result<(), anyhow::Error> {
459+
fn native_diff(diff: &Diff<'_>, theme: &str) -> Result<Vec<String>, anyhow::Error> {
423460
let ss = SyntaxSet::load_defaults_newlines();
424461
let ts = ThemeSet::load_defaults();
425462
let syntax = ss.find_syntax_by_extension("patch").unwrap();
426-
let mut h = HighlightLines::new(syntax, &ts.themes["base16-ocean.dark"]);
463+
let mut h = HighlightLines::new(
464+
syntax,
465+
ts.themes
466+
.get(theme)
467+
.unwrap_or_else(|| &ts.themes[DEFAULT_THEME]),
468+
);
427469

428470
let mut inner_err = None;
471+
let mut diff_lines = Vec::new();
429472

430473
diff.print(DiffFormat::Patch, |_delta, _hunk, line| {
431474
let content = std::str::from_utf8(line.content()).unwrap();
@@ -441,7 +484,7 @@ fn native_diff(diff: &Diff<'_>) -> Result<(), anyhow::Error> {
441484
}
442485
};
443486
let escaped = as_24_bit_terminal_escaped(&ranges[..], true);
444-
print!("{}", escaped);
487+
diff_lines.push(escaped);
445488
}
446489
_ => {
447490
let ranges = match h.highlight_line(content, &ss) {
@@ -452,7 +495,7 @@ fn native_diff(diff: &Diff<'_>) -> Result<(), anyhow::Error> {
452495
}
453496
};
454497
let escaped = as_24_bit_terminal_escaped(&ranges[..], true);
455-
print!("{}", escaped);
498+
diff_lines.push(escaped);
456499
}
457500
}
458501
true
@@ -461,10 +504,20 @@ fn native_diff(diff: &Diff<'_>) -> Result<(), anyhow::Error> {
461504
if let Some(err) = inner_err {
462505
Err(err.into())
463506
} else {
464-
Ok(())
507+
Ok(diff_lines)
465508
}
466509
}
467510

511+
fn print_diff_lines(diff_lines: &[String]) -> Result<(), anyhow::Error> {
512+
let mut stdout = StandardStream::stdout(ColorChoice::Auto);
513+
for line in diff_lines {
514+
write!(&mut stdout, "{}", line)?;
515+
}
516+
stdout.reset()?;
517+
writeln!(&mut stdout)?;
518+
Ok(())
519+
}
520+
468521
fn print_diffstat(prefix: &str, diff: &Diff<'_>) -> Result<(), anyhow::Error> {
469522
let buf = diff.stats()?.to_buf(DiffStatsFormat::FULL, 80)?;
470523
let stat = std::str::from_utf8(&buf).context("converting diffstat to utf-8")?;

src/main.rs

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@
1515
use std::env;
1616

1717
use clap::Parser;
18+
use git_instafix::DEFAULT_THEME;
1819

20+
const MAX_COMMITS_VAR: &str = "GIT_INSTAFIX_MAX_COMMITS";
1921
const UPSTREAM_VAR: &str = "GIT_INSTAFIX_UPSTREAM";
20-
const REQUIRE_NEWLINE: &str = "GIT_INSTAFIX_REQUIRE_NEWLINE";
22+
const REQUIRE_NEWLINE_VAR: &str = "GIT_INSTAFIX_REQUIRE_NEWLINE";
23+
const THEME_VAR: &str = "GIT_INSTAFIX_THEME";
2124

2225
#[derive(Parser, Debug)]
2326
#[clap(
@@ -39,36 +42,50 @@ When run with no arguments this will:
3942
)]
4043
struct Args {
4144
/// Change the commit message that you amend, instead of using the original commit message
42-
#[clap(short = 's', long = "squash")]
43-
squash: bool,
45+
#[clap(short = 's', long, hide = true)]
46+
squash: Option<bool>,
4447
/// The maximum number of commits to show when looking for your merge point
45-
#[clap(short = 'm', long = "max-commits", default_value = "15")]
46-
max_commits: usize,
48+
///
49+
/// [gitconfig: instafix.max-commits]
50+
#[clap(short = 'm', long = "max-commits", env = MAX_COMMITS_VAR)]
51+
max_commits: Option<usize>,
4752

4853
/// Specify a commit to ammend by the subject line of the commit
4954
#[clap(short = 'P', long)]
5055
commit_message_pattern: Option<String>,
5156

52-
#[clap(long, env = UPSTREAM_VAR)]
57+
/// The branch to not go past when looking for your merge point
58+
///
59+
/// [gitconfig: instafix.default-upstream-branch]
60+
#[clap(short = 'u', long, env = UPSTREAM_VAR)]
5361
default_upstream_branch: Option<String>,
5462

5563
/// Require a newline when confirming y/n questions
56-
#[clap(long, env = REQUIRE_NEWLINE)]
57-
require_newline: bool,
64+
///
65+
/// [gitconfig: instafix.require-newline]
66+
#[clap(long, env = REQUIRE_NEWLINE_VAR)]
67+
require_newline: Option<bool>,
68+
69+
/// Show the possible color themes for output
70+
#[clap(long)]
71+
help_themes: bool,
72+
73+
/// Use this theme
74+
#[clap(long, env = THEME_VAR)]
75+
theme: Option<String>,
5876
}
5977

6078
fn main() {
6179
let mut args = Args::parse();
6280
if env::args().next().unwrap().ends_with("squash") {
63-
args.squash = true
81+
args.squash = Some(true)
82+
}
83+
if args.help_themes {
84+
git_instafix::print_themes();
85+
return;
6486
}
65-
if let Err(e) = git_instafix::instafix(
66-
args.squash,
67-
args.max_commits,
68-
args.commit_message_pattern,
69-
args.default_upstream_branch.as_deref(),
70-
args.require_newline,
71-
) {
87+
let config = args_to_config_using_git_config(args).unwrap();
88+
if let Err(e) = git_instafix::instafix(config) {
7289
// An empty message means don't display any error message
7390
let msg = e.to_string();
7491
if !msg.is_empty() {
@@ -81,3 +98,28 @@ fn main() {
8198
std::process::exit(1);
8299
}
83100
}
101+
102+
fn args_to_config_using_git_config(args: Args) -> Result<git_instafix::Config, anyhow::Error> {
103+
let mut cfg = git2::Config::open_default()?;
104+
let repo = git2::Repository::discover(".")?;
105+
cfg.add_file(&repo.path().join("config"), git2::ConfigLevel::Local, false)?;
106+
Ok(git_instafix::Config {
107+
squash: args
108+
.squash
109+
.unwrap_or_else(|| cfg.get_bool("instafix.squash").unwrap_or(false)),
110+
max_commits: args
111+
.max_commits
112+
.unwrap_or_else(|| cfg.get_i32("instafix.max-commits").unwrap_or(15) as usize),
113+
commit_message_pattern: args.commit_message_pattern,
114+
default_upstream_branch: args
115+
.default_upstream_branch
116+
.or_else(|| cfg.get_string("instafix.default-upstream-branch").ok()),
117+
require_newline: args
118+
.require_newline
119+
.unwrap_or_else(|| cfg.get_bool("instafix.require-newline").unwrap_or(false)),
120+
theme: args.theme.unwrap_or_else(|| {
121+
cfg.get_string("instafix.theme")
122+
.unwrap_or_else(|_| DEFAULT_THEME.to_string())
123+
}),
124+
})
125+
}

0 commit comments

Comments
 (0)