Replace gix with configurable libgit2 diffs
This commit is contained in:
308
src/app/git.rs
308
src/app/git.rs
@@ -1,5 +1,7 @@
|
||||
use super::*;
|
||||
use std::collections::BTreeMap;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(super) enum GitChangeKind {
|
||||
@@ -181,7 +183,7 @@ impl App {
|
||||
self.error = Some("The selected Git file is no longer available.".into());
|
||||
return;
|
||||
};
|
||||
match load_diff(&worktree.root, file) {
|
||||
match load_diff(&worktree.root, file, &self.config.git) {
|
||||
Ok(diff) => {
|
||||
self.git_diff = Some(diff);
|
||||
self.error = None;
|
||||
@@ -420,25 +422,22 @@ impl App {
|
||||
}
|
||||
|
||||
fn read_worktree(project_path: &Path) -> Result<GitWorktree, String> {
|
||||
let repository = gix::discover(project_path).map_err(|error| error.to_string())?;
|
||||
let repository = git2::Repository::discover(project_path).map_err(|error| error.to_string())?;
|
||||
let root = repository
|
||||
.workdir()
|
||||
.ok_or_else(|| "bare repositories do not have a worktree".to_owned())?
|
||||
.to_path_buf();
|
||||
let dirwalk = repository
|
||||
.dirwalk_options()
|
||||
.map_err(|error| error.to_string())?;
|
||||
let mut options = git2::StatusOptions::new();
|
||||
options.include_untracked(true).recurse_untracked_dirs(true);
|
||||
let mut files = BTreeMap::<PathBuf, GitFile>::new();
|
||||
let changes = repository
|
||||
.status(gix::progress::Discard)
|
||||
.map_err(|error| error.to_string())?
|
||||
.index_worktree_options_mut(|options| options.dirwalk_options = Some(dirwalk))
|
||||
.untracked_files(gix::status::UntrackedFiles::Files)
|
||||
.into_iter(Vec::<gix::bstr::BString>::new())
|
||||
let statuses = repository
|
||||
.statuses(Some(&mut options))
|
||||
.map_err(|error| error.to_string())?;
|
||||
for change in changes {
|
||||
let change = change.map_err(|error| error.to_string())?;
|
||||
let path = gix::path::from_bstr(change.location()).into_owned();
|
||||
for entry in statuses.iter() {
|
||||
#[cfg(unix)]
|
||||
let path = PathBuf::from(std::ffi::OsString::from_vec(entry.path_bytes().to_vec()));
|
||||
#[cfg(not(unix))]
|
||||
let path = PathBuf::from(String::from_utf8_lossy(entry.path_bytes()).into_owned());
|
||||
let display_path = path.to_string_lossy().into_owned();
|
||||
let file = files.entry(path.clone()).or_insert_with(|| GitFile {
|
||||
path,
|
||||
@@ -446,18 +445,30 @@ fn read_worktree(project_path: &Path) -> Result<GitWorktree, String> {
|
||||
staged_kind: None,
|
||||
worktree_kind: None,
|
||||
});
|
||||
match change {
|
||||
gix::status::Item::IndexWorktree(change) => {
|
||||
file.worktree_kind = change.summary().map(worktree_kind);
|
||||
}
|
||||
gix::status::Item::TreeIndex(change) => {
|
||||
file.staged_kind = Some(match change {
|
||||
gix::diff::index::Change::Addition { .. } => GitChangeKind::Added,
|
||||
gix::diff::index::Change::Deletion { .. } => GitChangeKind::Deleted,
|
||||
gix::diff::index::Change::Modification { .. }
|
||||
| gix::diff::index::Change::Rewrite { .. } => GitChangeKind::Modified,
|
||||
});
|
||||
}
|
||||
let status = entry.status();
|
||||
if status.intersects(git2::Status::INDEX_NEW) {
|
||||
file.staged_kind = Some(GitChangeKind::Added);
|
||||
} else if status.intersects(git2::Status::INDEX_DELETED) {
|
||||
file.staged_kind = Some(GitChangeKind::Deleted);
|
||||
} else if status.intersects(
|
||||
git2::Status::INDEX_MODIFIED
|
||||
| git2::Status::INDEX_RENAMED
|
||||
| git2::Status::INDEX_TYPECHANGE,
|
||||
) {
|
||||
file.staged_kind = Some(GitChangeKind::Modified);
|
||||
}
|
||||
if status.intersects(git2::Status::WT_NEW) {
|
||||
file.worktree_kind = Some(GitChangeKind::Added);
|
||||
} else if status.intersects(git2::Status::WT_DELETED) {
|
||||
file.worktree_kind = Some(GitChangeKind::Deleted);
|
||||
} else if status.intersects(
|
||||
git2::Status::WT_MODIFIED | git2::Status::WT_RENAMED | git2::Status::WT_TYPECHANGE,
|
||||
) {
|
||||
file.worktree_kind = Some(GitChangeKind::Modified);
|
||||
}
|
||||
if status.intersects(git2::Status::CONFLICTED) {
|
||||
file.staged_kind = Some(GitChangeKind::Modified);
|
||||
file.worktree_kind = Some(GitChangeKind::Modified);
|
||||
}
|
||||
}
|
||||
files.retain(|_, file| file.staged_kind.is_some() || file.worktree_kind.is_some());
|
||||
@@ -467,76 +478,29 @@ fn read_worktree(project_path: &Path) -> Result<GitWorktree, String> {
|
||||
})
|
||||
}
|
||||
|
||||
fn worktree_kind(summary: gix::status::index_worktree::iter::Summary) -> GitChangeKind {
|
||||
use gix::status::index_worktree::iter::Summary;
|
||||
match summary {
|
||||
Summary::Added | Summary::IntentToAdd => GitChangeKind::Added,
|
||||
Summary::Removed => GitChangeKind::Deleted,
|
||||
Summary::Modified
|
||||
| Summary::TypeChange
|
||||
| Summary::Renamed
|
||||
| Summary::Copied
|
||||
| Summary::Conflict => GitChangeKind::Modified,
|
||||
}
|
||||
}
|
||||
|
||||
fn load_diff(root: &Path, file: &GitFile) -> Result<GitDiff, String> {
|
||||
let repository = gix::discover(root).map_err(|error| error.to_string())?;
|
||||
let index = repository
|
||||
.index_or_empty()
|
||||
.map_err(|error| error.to_string())?;
|
||||
let index_path =
|
||||
gix::path::to_unix_separators_on_windows(gix::path::into_bstr(file.path.as_path()));
|
||||
let index_blob = index
|
||||
.entry_by_path(index_path.as_ref())
|
||||
.filter(|entry| {
|
||||
entry
|
||||
.mode
|
||||
.to_tree_entry_mode()
|
||||
.is_some_and(|mode| mode.is_blob_or_symlink())
|
||||
})
|
||||
.map(|entry| {
|
||||
repository
|
||||
.find_blob(entry.id)
|
||||
.map(|mut blob| blob.take_data())
|
||||
.map_err(|error| error.to_string())
|
||||
})
|
||||
.transpose()?;
|
||||
fn load_diff(root: &Path, file: &GitFile, settings: &GitConfig) -> Result<GitDiff, String> {
|
||||
let repository = git2::Repository::discover(root).map_err(|error| error.to_string())?;
|
||||
let index = repository.index().map_err(|error| error.to_string())?;
|
||||
let mut lines = Vec::new();
|
||||
if file.staged_kind.is_some() {
|
||||
let tree = repository
|
||||
.find_tree(
|
||||
repository
|
||||
.head_tree_id_or_empty()
|
||||
.map_err(|error| error.to_string())?,
|
||||
)
|
||||
let head_tree = repository
|
||||
.head()
|
||||
.ok()
|
||||
.and_then(|head| head.peel_to_tree().ok());
|
||||
let mut options = diff_options(file, settings, false);
|
||||
let diff = repository
|
||||
.diff_tree_to_index(head_tree.as_ref(), Some(&index), Some(&mut options))
|
||||
.map_err(|error| error.to_string())?;
|
||||
let head_blob = tree
|
||||
.lookup_entry_by_path(&file.path)
|
||||
.map_err(|error| error.to_string())?
|
||||
.filter(|entry| entry.mode().is_blob_or_symlink())
|
||||
.map(|entry| {
|
||||
repository
|
||||
.find_blob(entry.object_id())
|
||||
.map(|mut blob| blob.take_data())
|
||||
.map_err(|error| error.to_string())
|
||||
})
|
||||
.transpose()?;
|
||||
lines.push(diff_section("Staged changes"));
|
||||
lines.extend(diff_blobs(
|
||||
head_blob.as_deref(),
|
||||
index_blob.as_deref(),
|
||||
&file.display_path,
|
||||
)?);
|
||||
lines.extend(render_diff(&diff)?);
|
||||
}
|
||||
if file.worktree_kind.is_some() {
|
||||
let worktree_blob = read_worktree_blob(&root.join(&file.path))?;
|
||||
let mut options = diff_options(file, settings, true);
|
||||
let diff = repository
|
||||
.diff_index_to_workdir(Some(&index), Some(&mut options))
|
||||
.map_err(|error| error.to_string())?;
|
||||
lines.push(diff_section("Worktree changes"));
|
||||
lines.extend(diff_blobs(
|
||||
index_blob.as_deref(),
|
||||
worktree_blob.as_deref(),
|
||||
&file.display_path,
|
||||
)?);
|
||||
lines.extend(render_diff(&diff)?);
|
||||
}
|
||||
if lines.is_empty() {
|
||||
lines.push(diff_message("No textual diff is available."));
|
||||
@@ -547,6 +511,67 @@ fn load_diff(root: &Path, file: &GitFile) -> Result<GitDiff, String> {
|
||||
})
|
||||
}
|
||||
|
||||
fn diff_options(file: &GitFile, settings: &GitConfig, worktree: bool) -> git2::DiffOptions {
|
||||
let mut options = git2::DiffOptions::new();
|
||||
options
|
||||
.pathspec(&file.path)
|
||||
.context_lines(settings.context_lines)
|
||||
.interhunk_lines(settings.interhunk_lines)
|
||||
.indent_heuristic(settings.indent_heuristic)
|
||||
.ignore_blank_lines(settings.ignore_blank_lines);
|
||||
match settings.diff_algorithm {
|
||||
GitDiffAlgorithm::Default => {}
|
||||
GitDiffAlgorithm::Patience => {
|
||||
options.patience(true);
|
||||
}
|
||||
GitDiffAlgorithm::Minimal => {
|
||||
options.minimal(true);
|
||||
}
|
||||
}
|
||||
match settings.whitespace {
|
||||
GitDiffWhitespace::ShowAll => {}
|
||||
GitDiffWhitespace::IgnoreAll => {
|
||||
options.ignore_whitespace(true);
|
||||
}
|
||||
GitDiffWhitespace::IgnoreChanges => {
|
||||
options.ignore_whitespace_change(true);
|
||||
}
|
||||
GitDiffWhitespace::IgnoreEndOfLine => {
|
||||
options.ignore_whitespace_eol(true);
|
||||
}
|
||||
}
|
||||
if worktree {
|
||||
options
|
||||
.include_untracked(true)
|
||||
.recurse_untracked_dirs(true)
|
||||
.show_untracked_content(true);
|
||||
}
|
||||
options
|
||||
}
|
||||
|
||||
fn render_diff(diff: &git2::Diff<'_>) -> Result<Vec<GitDiffLine>, String> {
|
||||
let mut output = Vec::new();
|
||||
diff.print(git2::DiffFormat::Patch, |_, _, line| {
|
||||
if matches!(
|
||||
line.origin_value(),
|
||||
git2::DiffLineType::Context
|
||||
| git2::DiffLineType::Addition
|
||||
| git2::DiffLineType::Deletion
|
||||
) {
|
||||
output.push(line.origin() as u8);
|
||||
}
|
||||
output.extend_from_slice(line.content());
|
||||
true
|
||||
})
|
||||
.map_err(|error| error.to_string())?;
|
||||
let lines = parse_unified_diff(&String::from_utf8_lossy(&output));
|
||||
Ok(if lines.is_empty() {
|
||||
vec![diff_message("No textual diff is available.")]
|
||||
} else {
|
||||
lines
|
||||
})
|
||||
}
|
||||
|
||||
fn diff_section(text: &str) -> GitDiffLine {
|
||||
GitDiffLine {
|
||||
kind: GitDiffLineKind::Section,
|
||||
@@ -565,58 +590,6 @@ fn diff_message(text: &str) -> GitDiffLine {
|
||||
}
|
||||
}
|
||||
|
||||
fn read_worktree_blob(path: &Path) -> Result<Option<Vec<u8>>, String> {
|
||||
match fs::symlink_metadata(path) {
|
||||
Ok(metadata) if metadata.file_type().is_symlink() => fs::read_link(path)
|
||||
.map(|target| Some(target.as_os_str().as_encoded_bytes().to_vec()))
|
||||
.map_err(|error| error.to_string()),
|
||||
Ok(metadata) if metadata.is_file() => {
|
||||
fs::read(path).map(Some).map_err(|error| error.to_string())
|
||||
}
|
||||
Ok(_) => Ok(None),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||
Err(error) => Err(error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn diff_blobs(
|
||||
before: Option<&[u8]>,
|
||||
after: Option<&[u8]>,
|
||||
path: &str,
|
||||
) -> Result<Vec<GitDiffLine>, String> {
|
||||
let before_label = before.map_or_else(|| "/dev/null".into(), |_| format!("a/{path}"));
|
||||
let after_label = after.map_or_else(|| "/dev/null".into(), |_| format!("b/{path}"));
|
||||
let before = before.unwrap_or_default();
|
||||
let after = after.unwrap_or_default();
|
||||
let mut lines = vec![
|
||||
diff_message(&format!("--- {before_label}")),
|
||||
diff_message(&format!("+++ {after_label}")),
|
||||
];
|
||||
if before == after {
|
||||
lines.push(diff_message("No textual diff is available."));
|
||||
return Ok(lines);
|
||||
}
|
||||
if before.iter().take(8_000).any(|byte| *byte == 0)
|
||||
|| after.iter().take(8_000).any(|byte| *byte == 0)
|
||||
{
|
||||
lines.push(diff_message("Binary file changed."));
|
||||
return Ok(lines);
|
||||
}
|
||||
let input = gix::diff::blob::InternedInput::new(before, after);
|
||||
let diff =
|
||||
gix::diff::blob::diff_with_slider_heuristics(gix::diff::blob::Algorithm::Histogram, &input);
|
||||
let output = gix::diff::blob::UnifiedDiff::new(
|
||||
&diff,
|
||||
&input,
|
||||
gix::diff::blob::unified_diff::ConsumeBinaryHunk::new(Vec::new(), "\n"),
|
||||
Default::default(),
|
||||
)
|
||||
.consume()
|
||||
.map_err(|error| error.to_string())?;
|
||||
lines.extend(parse_unified_diff(&String::from_utf8_lossy(&output)));
|
||||
Ok(lines)
|
||||
}
|
||||
|
||||
fn parse_unified_diff(diff: &str) -> Vec<GitDiffLine> {
|
||||
let mut old_number = None;
|
||||
let mut new_number = None;
|
||||
@@ -924,7 +897,7 @@ mod tests {
|
||||
static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
#[test]
|
||||
fn gitoxide_status_merges_staged_and_worktree_changes() {
|
||||
fn libgit2_status_merges_staged_and_worktree_changes() {
|
||||
let directory = repository_fixture();
|
||||
std::fs::write(directory.join("added.txt"), "added\n").unwrap();
|
||||
git2::Repository::open(&directory)
|
||||
@@ -952,7 +925,7 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(changed.staged_kind, Some(GitChangeKind::Modified));
|
||||
assert_eq!(changed.worktree_kind, Some(GitChangeKind::Modified));
|
||||
let diff = load_diff(&worktree.root, changed).unwrap();
|
||||
let diff = load_diff(&worktree.root, changed, &GitConfig::default()).unwrap();
|
||||
assert_eq!(
|
||||
diff.lines
|
||||
.iter()
|
||||
@@ -1118,6 +1091,55 @@ mod tests {
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_git_diff_options_change_the_rendered_patch() {
|
||||
let directory = empty_repository_fixture();
|
||||
std::fs::write(
|
||||
directory.join("changed.txt"),
|
||||
"first\nsecond\nthird\nfourth\nfifth\n",
|
||||
)
|
||||
.unwrap();
|
||||
stage_paths(&directory, &["changed.txt".into()]).unwrap();
|
||||
commit_index(&directory, "Initial").unwrap();
|
||||
std::fs::write(
|
||||
directory.join("changed.txt"),
|
||||
"first\nsecond changed\nthird\nfourth changed\nfifth\n",
|
||||
)
|
||||
.unwrap();
|
||||
let worktree = read_worktree(&directory).unwrap();
|
||||
let changed = worktree
|
||||
.files
|
||||
.iter()
|
||||
.find(|file| file.display_path == "changed.txt")
|
||||
.unwrap();
|
||||
let settings = GitConfig {
|
||||
diff_algorithm: GitDiffAlgorithm::Patience,
|
||||
context_lines: 0,
|
||||
interhunk_lines: 0,
|
||||
indent_heuristic: true,
|
||||
whitespace: GitDiffWhitespace::ShowAll,
|
||||
ignore_blank_lines: false,
|
||||
};
|
||||
|
||||
let diff = load_diff(&worktree.root, changed, &settings).unwrap();
|
||||
assert_eq!(
|
||||
diff.lines
|
||||
.iter()
|
||||
.filter(|line| line.kind == GitDiffLineKind::Context)
|
||||
.count(),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
diff.lines
|
||||
.iter()
|
||||
.filter(|line| line.kind == GitDiffLineKind::Hunk)
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
fn repository_fixture() -> PathBuf {
|
||||
let directory = empty_repository_fixture();
|
||||
std::fs::write(directory.join("changed.txt"), "original\n").unwrap();
|
||||
|
||||
@@ -12,6 +12,12 @@ pub(super) struct PreferenceDraft {
|
||||
pub(super) endpoint_cors: bool,
|
||||
pub(super) dev_brain_enabled: bool,
|
||||
pub(super) dev_brain_vault_path: String,
|
||||
pub(super) git_diff_algorithm: GitDiffAlgorithm,
|
||||
pub(super) git_context_lines: String,
|
||||
pub(super) git_interhunk_lines: String,
|
||||
pub(super) git_indent_heuristic: bool,
|
||||
pub(super) git_whitespace: GitDiffWhitespace,
|
||||
pub(super) git_ignore_blank_lines: bool,
|
||||
pub(super) context_tokens: String,
|
||||
pub(super) max_generated_tokens: String,
|
||||
pub(super) system_prompt: text_editor::Content,
|
||||
@@ -64,6 +70,12 @@ impl PreferenceDraft {
|
||||
endpoint_cors: config.endpoint.cors,
|
||||
dev_brain_enabled: config.dev_brain.enabled,
|
||||
dev_brain_vault_path: config.dev_brain.vault_path.clone().unwrap_or_default(),
|
||||
git_diff_algorithm: config.git.diff_algorithm,
|
||||
git_context_lines: config.git.context_lines.to_string(),
|
||||
git_interhunk_lines: config.git.interhunk_lines.to_string(),
|
||||
git_indent_heuristic: config.git.indent_heuristic,
|
||||
git_whitespace: config.git.whitespace,
|
||||
git_ignore_blank_lines: config.git.ignore_blank_lines,
|
||||
context_tokens: generation.context_tokens.to_string(),
|
||||
max_generated_tokens: generation.max_generated_tokens.to_string(),
|
||||
system_prompt: text_editor::Content::with_text(&generation.system_prompt),
|
||||
@@ -126,6 +138,17 @@ impl PreferenceDraft {
|
||||
Ok(preferences)
|
||||
}
|
||||
|
||||
pub(super) fn git(&self) -> Result<GitConfig, String> {
|
||||
Ok(GitConfig {
|
||||
diff_algorithm: self.git_diff_algorithm,
|
||||
context_lines: parse_u32("Git diff context lines", &self.git_context_lines)?,
|
||||
interhunk_lines: parse_u32("Git diff interhunk lines", &self.git_interhunk_lines)?,
|
||||
indent_heuristic: self.git_indent_heuristic,
|
||||
whitespace: self.git_whitespace,
|
||||
ignore_blank_lines: self.git_ignore_blank_lines,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn reset(&mut self) {
|
||||
*self = Self::from_saved(&Config::default());
|
||||
}
|
||||
@@ -248,6 +271,13 @@ fn parse_optional_u32(name: &str, value: &str) -> Result<Option<u32>, String> {
|
||||
parse_optional_number(name, value)
|
||||
}
|
||||
|
||||
fn parse_u32(name: &str, value: &str) -> Result<u32, String> {
|
||||
value
|
||||
.trim()
|
||||
.parse()
|
||||
.map_err(|_| format!("{name} must be a non-negative whole number."))
|
||||
}
|
||||
|
||||
fn parse_optional_u8(name: &str, value: &str) -> Result<Option<u8>, String> {
|
||||
parse_optional_number(name, value)
|
||||
}
|
||||
@@ -363,6 +393,13 @@ impl App {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let git = match self.preference_draft.git() {
|
||||
Ok(git) => git,
|
||||
Err(error) => {
|
||||
self.preference_error = Some(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let config = Config {
|
||||
model: self.preference_draft.model,
|
||||
idle_timeout_minutes,
|
||||
@@ -378,6 +415,7 @@ impl App {
|
||||
},
|
||||
generation,
|
||||
runtime,
|
||||
git,
|
||||
interface: self.config.interface.clone(),
|
||||
};
|
||||
if let Err(error) = config.validate() {
|
||||
|
||||
@@ -368,31 +368,31 @@ impl App {
|
||||
}
|
||||
|
||||
fn read_git_state(path: &Path) -> Option<GitState> {
|
||||
let repository = gix::discover(path).ok()?;
|
||||
let repository = git2::Repository::discover(path).ok()?;
|
||||
let mut branches = repository
|
||||
.references()
|
||||
.branches(Some(git2::BranchType::Local))
|
||||
.ok()?
|
||||
.local_branches()
|
||||
.ok()?
|
||||
.map(|reference| {
|
||||
reference
|
||||
.map(|reference| String::from_utf8_lossy(reference.name().shorten()).into_owned())
|
||||
.map(|branch| {
|
||||
branch.and_then(|(branch, _)| {
|
||||
branch
|
||||
.name_bytes()
|
||||
.map(|name| String::from_utf8_lossy(name).into_owned())
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.ok()?;
|
||||
branches.sort();
|
||||
branches.dedup();
|
||||
|
||||
let current = repository
|
||||
.head_name()
|
||||
.ok()?
|
||||
.map(|name| String::from_utf8_lossy(name.shorten()).into_owned());
|
||||
let head = repository.find_reference("HEAD").ok()?;
|
||||
let current = head.symbolic_target_bytes().and_then(|target| {
|
||||
target
|
||||
.strip_prefix(b"refs/heads/")
|
||||
.map(|name| String::from_utf8_lossy(name).into_owned())
|
||||
});
|
||||
let label = current.clone().unwrap_or_else(|| {
|
||||
repository
|
||||
.head_id()
|
||||
.ok()
|
||||
.and_then(|id| id.shorten().ok())
|
||||
.map(|id| format!("detached @ {id}"))
|
||||
head.target()
|
||||
.map(|id| format!("detached @ {}", &id.to_string()[..7]))
|
||||
.unwrap_or_else(|| "No branch".to_owned())
|
||||
});
|
||||
Some(GitState {
|
||||
|
||||
@@ -12,6 +12,7 @@ use super::{
|
||||
ModelDownload, ModelOperation, PreferenceSection, ProjectChoice, chat_scroll_id, composer_id,
|
||||
models_path, preferences_scroll_id,
|
||||
};
|
||||
use crate::config::{GIT_DIFF_ALGORITHMS, GIT_DIFF_WHITESPACE_MODES};
|
||||
use crate::database::{ProjectWithSessions, Session, SessionState};
|
||||
use crate::model::{
|
||||
self, DownloadPhase, MODEL_CHOICES, ManagedArtifact, ManagedArtifactState, ModelChoice,
|
||||
|
||||
@@ -183,6 +183,65 @@ impl App {
|
||||
]
|
||||
.spacing(10),
|
||||
);
|
||||
let git_group = preference_group(
|
||||
PreferenceSection::Git,
|
||||
"GIT DIFFS",
|
||||
column![
|
||||
row![
|
||||
hint(
|
||||
text("Algorithm").size(13).width(Length::Fill),
|
||||
"Default uses libgit2's Myers diff. Patience favors unique matching lines; Minimal spends more time finding the smallest edit script.",
|
||||
),
|
||||
pick_list(
|
||||
&GIT_DIFF_ALGORITHMS[..],
|
||||
Some(self.preference_draft.git_diff_algorithm),
|
||||
Message::PreferenceGitDiffAlgorithmChanged,
|
||||
)
|
||||
.width(240),
|
||||
]
|
||||
.spacing(12)
|
||||
.align_y(Alignment::Center),
|
||||
preference_input_row(
|
||||
"Context lines",
|
||||
"Unchanged lines shown before and after each changed block. libgit2 defaults to 3.",
|
||||
text_input("3", &self.preference_draft.git_context_lines)
|
||||
.on_input(Message::PreferenceGitContextLinesChanged),
|
||||
),
|
||||
preference_input_row(
|
||||
"Interhunk lines",
|
||||
"Merge nearby changed blocks when no more than this many unchanged lines separate them. Zero keeps libgit2's default separation.",
|
||||
text_input("0", &self.preference_draft.git_interhunk_lines)
|
||||
.on_input(Message::PreferenceGitInterhunkLinesChanged),
|
||||
),
|
||||
row![
|
||||
hint(
|
||||
text("Whitespace").size(13).width(Length::Fill),
|
||||
"Controls which whitespace-only edits libgit2 omits from the displayed diff.",
|
||||
),
|
||||
pick_list(
|
||||
&GIT_DIFF_WHITESPACE_MODES[..],
|
||||
Some(self.preference_draft.git_whitespace),
|
||||
Message::PreferenceGitWhitespaceChanged,
|
||||
)
|
||||
.width(240),
|
||||
]
|
||||
.spacing(12)
|
||||
.align_y(Alignment::Center),
|
||||
hint(
|
||||
checkbox(self.preference_draft.git_indent_heuristic)
|
||||
.label("Use indentation heuristic")
|
||||
.on_toggle(Message::PreferenceGitIndentHeuristicChanged),
|
||||
"Shift ambiguous hunk boundaries toward indentation changes, which usually makes source-code diffs easier to read.",
|
||||
),
|
||||
hint(
|
||||
checkbox(self.preference_draft.git_ignore_blank_lines)
|
||||
.label("Ignore blank-line changes")
|
||||
.on_toggle(Message::PreferenceGitIgnoreBlankLinesChanged),
|
||||
"Hide hunks whose changed lines are all blank.",
|
||||
),
|
||||
]
|
||||
.spacing(10),
|
||||
);
|
||||
let generation_group = preference_group(
|
||||
PreferenceSection::Generation,
|
||||
"GENERATION",
|
||||
@@ -580,6 +639,7 @@ impl App {
|
||||
model_group,
|
||||
endpoint_group,
|
||||
dev_brain_group,
|
||||
git_group,
|
||||
generation_group,
|
||||
execution_group,
|
||||
acceleration_group,
|
||||
|
||||
Reference in New Issue
Block a user