Merge branch 'master' into patch-1

This commit is contained in:
Keith Hall authored and GitHub committed 2026-06-27 21:58:52 +03:00
commit 20a818fe14
7 files changed
+1673 -568

No files matched your search

+3
View File
@@ -46,6 +46,7 @@
- Fixed syntax tests path, see #3610 (@foxfromworld) - Fixed syntax tests path, see #3610 (@foxfromworld)
- Fix zsh tab completion word-splitting language names containing spaces (e.g. `HTML (Jinja2)`, `Apache Conf`), see #3693 (@YoshKoz) - Fix zsh tab completion word-splitting language names containing spaces (e.g. `HTML (Jinja2)`, `Apache Conf`), see #3693 (@YoshKoz)
- Fix zsh tab completion offering invalid `-l` arguments (file globs, paths, hidden filenames) sourced from the second column of `--list-languages`. Closes #3735, see #3737 (@truffle-dev) - Fix zsh tab completion offering invalid `-l` arguments (file globs, paths, hidden filenames) sourced from the second column of `--list-languages`. Closes #3735, see #3737 (@truffle-dev)
- Fix `usize` underflow in `--list-languages` when `--terminal-width` is smaller than the longest language name, see #3812 (@greymoth-jp)
## Other ## Other
- Use git version of cross. See #3533 (@OctopusET) - Use git version of cross. See #3533 (@OctopusET)
@@ -53,6 +54,7 @@
- Allow home and end keys to be used with builtin pager, see #3651 (@keith-hall) - Allow home and end keys to be used with builtin pager, see #3651 (@keith-hall)
- Builtin syntax mapping: cleanup matcher glob parsing logic #3652 (@cyqsimon) - Builtin syntax mapping: cleanup matcher glob parsing logic #3652 (@cyqsimon)
- Statically link the CRT for MSVC builds via Cargo config to avoid runtime DLL dependencies. Closes #3634, see #3692 (@barry3406) - Statically link the CRT for MSVC builds via Cargo config to avoid runtime DLL dependencies. Closes #3634, see #3692 (@barry3406)
- Replace `libgit2` with a pure Rust implementation of git called `gitoxide`, see PR #3703 (@blinxen)
## Syntaxes ## Syntaxes
@@ -68,6 +70,7 @@
- Add Ghostty syntax mapping, see #3759 (@injust) - Add Ghostty syntax mapping, see #3759 (@injust)
- Add syntax highlighting for `Caddyfile` #3789 (@CosmicHorrorDev) - Add syntax highlighting for `Caddyfile` #3789 (@CosmicHorrorDev)
- Include `.code-workspace` as a JSON extension #3809 (@dhruvkb) - Include `.code-workspace` as a JSON extension #3809 (@dhruvkb)
- Add syntax mapping for DNF repo configuration files, see #3814 (@injust)
## Themes ## Themes
Generated
+1445 -475
View File
File diff suppressed because it is too large. Load diff
+4 -4
View File
@@ -31,8 +31,7 @@ minimal-application = [
"regex-onig", "regex-onig",
"wild", "wild",
] ]
git = ["git2"] # Support indicating git modifications git = ["gix"] # Support indicating git modifications
vendored-libgit2 = ["git2/vendored-libgit2"]
paging = [ "shell-words", "grep-cli", "minus"] # Support applying a pager on the output paging = [ "shell-words", "grep-cli", "minus"] # Support applying a pager on the output
lessopen = ["execute"] # Support $LESSOPEN preprocessor lessopen = ["execute"] # Support $LESSOPEN preprocessor
build-assets = ["syntect/yaml-load", "syntect/plist-load", "regex", "walkdir"] build-assets = ["syntect/yaml-load", "syntect/plist-load", "regex", "walkdir"]
@@ -76,10 +75,11 @@ terminal-colorsaurus = "1.0"
unicode-segmentation = "1.13.2" unicode-segmentation = "1.13.2"
itertools = "0.14.0" itertools = "0.14.0"
[dependencies.git2] [dependencies.gix]
version = "0.20" version = "0.85"
optional = true optional = true
default-features = false default-features = false
features = ["sha1", "blob-diff"]
[dependencies.syntect] [dependencies.syntect]
version = "5.3.0" version = "5.3.0"
+8 -1
View File
@@ -152,7 +152,14 @@ pub fn get_languages(config: &Config, cache_dir: &Path) -> Result<String> {
let comma_separator = ", "; let comma_separator = ", ";
let separator = " "; let separator = " ";
// Line-wrapping for the possible file extension overflow. // Line-wrapping for the possible file extension overflow.
let desired_width = config.term_width - longest - separator.len(); // Clamp instead of subtracting: a tiny `--terminal-width` (smaller than the
// longest language name) would otherwise underflow `usize` and wrap to a huge
// value, silently disabling wrapping (and panicking in debug/overflow-checked
// builds). Mirrors the `saturating_sub` fix applied to `print_snip` in #3804.
let desired_width = config
.term_width
.saturating_sub(longest)
.saturating_sub(separator.len());
let style = if config.colored_output { let style = if config.colored_output {
Green.normal() Green.normal()
+174 -60
View File
@@ -1,12 +1,13 @@
#![cfg(feature = "git")] #![cfg(feature = "git")]
use gix::diff::blob::pipeline::{Mode, WorktreeRoots};
use gix::diff::blob::{Algorithm, HunkIter, ResourceKind};
use gix::object::tree::EntryKind;
use path_abs::PathInfo;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs;
use std::path::Path; use std::path::Path;
use git2::{DiffOptions, IntoCString, Repository}; #[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[derive(Copy, Clone, Debug)]
pub enum LineChange { pub enum LineChange {
Added, Added,
RemovedAbove, RemovedAbove,
@@ -16,68 +17,181 @@ pub enum LineChange {
pub type LineChanges = HashMap<u32, LineChange>; pub type LineChanges = HashMap<u32, LineChange>;
pub fn get_git_diff(filename: &Path) -> Option<LineChanges> { fn collect_changes_from_hunks(hunks: HunkIter) -> Option<LineChanges> {
let repo = Repository::discover(filename).ok()?; let mut changes: LineChanges = HashMap::new();
for hunk in hunks {
let repo_path_absolute = fs::canonicalize(repo.workdir()?).ok()?; if hunk.before.is_empty() && !hunk.after.is_empty() {
for line in hunk.after {
let filepath_absolute = fs::canonicalize(filename).ok()?; changes.insert(line + 1, LineChange::Added);
let filepath_relative_to_repo = filepath_absolute.strip_prefix(&repo_path_absolute).ok()?;
let mut diff_options = DiffOptions::new();
let pathspec = filepath_relative_to_repo.into_c_string().ok()?;
diff_options.pathspec(pathspec);
diff_options.context_lines(0);
let diff = repo
.diff_index_to_workdir(None, Some(&mut diff_options))
.ok()?;
let mut line_changes: LineChanges = HashMap::new();
let mark_section =
|line_changes: &mut LineChanges, start: u32, end: i32, change: LineChange| {
for line in start..=end as u32 {
line_changes.insert(line, change);
} }
}; } else if hunk.after.is_empty() && !hunk.before.is_empty() {
if hunk.after.start == 0 {
let _ = diff.foreach( changes.insert(1, LineChange::RemovedAbove);
&mut |_, _| true,
None,
Some(&mut |delta, hunk| {
let path = delta.new_file().path().unwrap_or_else(|| Path::new(""));
if filepath_relative_to_repo != path {
return false;
}
let old_lines = hunk.old_lines();
let new_start = hunk.new_start();
let new_lines = hunk.new_lines();
let new_end = (new_start + new_lines) as i32 - 1;
if old_lines == 0 && new_lines > 0 {
mark_section(&mut line_changes, new_start, new_end, LineChange::Added);
} else if new_lines == 0 && old_lines > 0 {
if new_start == 0 {
mark_section(&mut line_changes, 1, 1, LineChange::RemovedAbove);
} else { } else {
mark_section( changes.insert(hunk.after.start, LineChange::RemovedBelow);
&mut line_changes,
new_start,
new_start as i32,
LineChange::RemovedBelow,
);
} }
} else { } else {
mark_section(&mut line_changes, new_start, new_end, LineChange::Modified); for line in hunk.after {
changes.insert(line + 1, LineChange::Modified);
}
}
} }
true Some(changes)
}), }
None,
pub fn get_git_diff(filename: &Path) -> Option<LineChanges> {
let filepath_absolute = filename.canonicalize().ok()?;
let repository = gix::discover(filepath_absolute.parent().ok()?).ok()?;
let repo_path_absolute = repository.workdir()?.canonicalize().ok()?;
let filepath_relative_to_repo = gix::path::to_unix_separators_on_windows(gix::path::into_bstr(
filepath_absolute.strip_prefix(&repo_path_absolute).ok()?,
));
let index = repository.index_or_load_from_head_or_empty().ok()?;
let index_entry = index.entry_by_path(filepath_relative_to_repo.as_ref())?;
let mut cache = repository
.diff_resource_cache(
Mode::ToGit,
WorktreeRoots {
old_root: None,
new_root: repository.workdir().map(Path::to_path_buf),
},
)
.ok()?;
cache
.set_resource(
index_entry.id,
index_entry.mode.to_tree_entry_mode()?.kind(),
filepath_relative_to_repo.as_ref(),
ResourceKind::OldOrSource,
&repository,
)
.ok()?;
cache
.set_resource(
repository.object_hash().null(),
EntryKind::Blob,
filepath_relative_to_repo.as_ref(),
ResourceKind::NewOrDestination,
&repository,
)
.ok()?;
let diff = gix::diff::blob::diff_with_slider_heuristics(
Algorithm::Histogram,
&cache.prepare_diff().ok()?.interned_input(),
);
collect_changes_from_hunks(diff.hunks())
}
#[cfg(test)]
mod tests {
use super::{get_git_diff, LineChange};
use std::path::{Path, PathBuf};
use std::process::Command;
fn git(repo: &Path, args: &[&str]) {
let output = Command::new("git")
.args(args)
.current_dir(repo)
.output()
.expect("git command can run");
assert!(
output.status.success(),
"git {args:?} failed\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
); );
}
fn setup_repo() -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("can create temporary directory");
let repo = dir.path();
git(repo, &["init"]);
git(repo, &["config", "user.email", "test@test.test"]);
git(repo, &["config", "user.name", "Test"]);
dir
}
fn create_and_track_file(repo: &tempfile::TempDir, filename: &str) -> PathBuf {
let filepath = repo.path().join(filename);
std::fs::write(&filepath, "file\n").expect("can write file");
git(repo.path(), &["add", filename]);
git(repo.path(), &["commit", "-m", "initial"]);
filepath.to_owned()
}
#[test]
fn not_a_git_repository() {
let dir = tempfile::tempdir().expect("can create temporary directory");
let file = dir.path().join("file.txt");
std::fs::write(&file, "line 1\n").expect("can write file");
assert_eq!(get_git_diff(&file), None);
}
#[test]
fn diff_is_calculated_against_index() {
let repo = setup_repo();
let file = create_and_track_file(&repo, "file.txt");
Some(line_changes) std::fs::write(&file, "line 1\nline 2 modified\n").expect("can write file");
// Add modified file to index -> should find 0 changes but not none
git(repo.path(), &["add", "file.txt"]);
assert_eq!(get_git_diff(&file).expect("empty map").len(), 0);
}
#[test]
fn diff_is_calculated_against_index_2() {
let repo = setup_repo();
let file = create_and_track_file(&repo, "file.txt");
std::fs::write(&file, "line 1\nline 2 modified\n").expect("can write file");
git(repo.path(), &["add", "file.txt"]);
// modify the second line again which should show a single change
std::fs::write(&file, "line 1\nline 2 modified again\n").expect("can write file");
assert_eq!(get_git_diff(&file).expect("one change").len(), 1);
}
#[test]
fn diff_is_calculated_correctly() {
let repo = setup_repo();
create_and_track_file(&repo, "committed.txt");
let filename = "committed2.txt";
let file = create_and_track_file(&repo, filename);
std::fs::write(&file, "file\nline 2 added\n").expect("can write file");
let mut changes = get_git_diff(&file).expect("multiple changes");
assert_eq!(changes.get(&2), Some(&LineChange::Added));
git(repo.path(), &["add", filename]);
std::fs::write(&file, "file\nline 2 modified\n").expect("can write file");
changes = get_git_diff(&file).expect("multiple changes");
assert_eq!(changes.get(&2), Some(&LineChange::Modified));
git(repo.path(), &["add", filename]);
std::fs::write(&file, "line 2 modified\nline 3 added").expect("can write file");
changes = get_git_diff(&file).expect("multiple changes");
assert_eq!(changes.get(&1), Some(&LineChange::RemovedAbove));
assert_eq!(changes.get(&2), Some(&LineChange::Added));
}
#[test]
fn faulty_git_version_does_not_panic() {
let repo = setup_repo();
let file = create_and_track_file(&repo, "file.txt");
std::fs::write(&file, "line 1\nline 2 modified\n").expect("can write file");
// changes are detected
assert_eq!(get_git_diff(&file).expect("one change").len(), 2);
// write invalid repositoryformatversion
git(
repo.path(),
&["config", "core.repositoryformatversion", "one"],
);
// changes are no longer detected
assert_eq!(get_git_diff(&file), None);
}
} }
@@ -0,0 +1,2 @@
[mappings]
"INI" = ["/etc/dnf/dnf.conf", "/etc/yum.repos.d/*.repo"]
+37 -28
View File
@@ -1,15 +1,17 @@
use gix::actor::SignatureRef;
use gix::bstr::BString;
use gix::bstr::ByteSlice;
use gix::date::time::Format;
use gix::date::Time;
use gix::objs::tree;
use std::env; use std::env;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::Read; use std::io::Read;
use std::path::{Path, PathBuf}; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use tempfile::TempDir; use tempfile::TempDir;
use git2::build::CheckoutBuilder;
use git2::Repository;
use git2::Signature;
pub struct BatTester { pub struct BatTester {
/// Temporary working directory /// Temporary working directory
temp_dir: TempDir, temp_dir: TempDir,
@@ -59,35 +61,42 @@ impl Default for BatTester {
} }
} }
fn create_sample_directory() -> Result<TempDir, git2::Error> { fn create_sample_directory() -> Result<TempDir, Box<dyn std::error::Error>> {
// Create temp directory and initialize repository // Create temp directory and initialize repository
let temp_dir = TempDir::new().expect("Temp directory"); let temp_dir = TempDir::new().expect("Temp directory");
let repo = Repository::init(&temp_dir)?; let repo = gix::init(&temp_dir)?;
let mut tree = gix::objs::Tree::empty();
// Copy over `sample.rs` // Create sample.rs from snapshot file
let sample_path = temp_dir.path().join("sample.rs"); let blob_id = repo.write_blob_stream(File::open("tests/snapshots/sample.rs")?)?;
println!("{sample_path:?}"); let entry = tree::Entry {
fs::copy("tests/snapshots/sample.rs", &sample_path).expect("successful copy"); mode: tree::EntryMode::from(tree::EntryKind::Blob),
oid: blob_id.object()?.id,
filename: BString::from("sample.rs"),
};
tree.entries.push(entry);
let tree_id = repo.write_object(tree)?;
// Commit let author = SignatureRef {
let mut index = repo.index()?; name: "test".as_bytes().as_bstr(),
index.add_path(Path::new("sample.rs"))?; email: "test@test.test".as_bytes().as_bstr(),
time: &Time::now_local_or_utc().format_or_unix(Format::Raw),
let oid = index.write_tree()?; };
let signature = Signature::now("bat test runner", "bat@test.runner")?; let commit_id = repo.commit_as(
let tree = repo.find_tree(oid)?; author,
let _ = repo.commit( author,
Some("HEAD"), // point HEAD to our new commit "HEAD",
&signature, // author
&signature, // committer
"initial commit", "initial commit",
&tree, tree_id,
&[], gix::commit::NO_PARENT_IDS,
); )?;
let mut opts = CheckoutBuilder::new(); assert_eq!(commit_id, repo.head_id()?);
repo.checkout_head(Some(opts.force()))?;
fs::copy("tests/snapshots/sample.modified.rs", &sample_path).expect("successful copy"); fs::copy(
"tests/snapshots/sample.modified.rs",
temp_dir.path().join("sample.rs"),
)
.expect("successful copy");
Ok(temp_dir) Ok(temp_dir)
} }