|
1 | 1 | //! Git operations that run in process, via [gix]. |
2 | 2 | //! |
3 | | -//! This module is a placeholder. Xvc's Git operations currently all run the `git` binary (see |
4 | | -//! [`super::subprocess`]); they are being moved here one at a time, lowest risk first: |
| 3 | +//! Xvc's Git operations are being moved here from [`super::subprocess`] one at a time, lowest |
| 4 | +//! risk first: |
5 | 5 | //! |
6 | | -//! 1. `tracked_files` — iterate the index instead of parsing `git ls-files --full-name`. |
| 6 | +//! 1. [tracked_files] — iterate the index instead of parsing `git ls-files --full-name`. **Done.** |
7 | 7 | //! 2. `xvc_paths_dirty` — [`gix::Repository::status`] instead of `git status --porcelain`. |
8 | 8 | //! 3. `stage_xvc_paths` — write blobs through the filter pipeline and patch the index. |
9 | 9 | //! 4. `commit_xvc_paths` — build the tree from `HEAD^{tree}` with [`gix::Repository::edit_tree`] |
|
15 | 15 | //! checkout orchestration; reimplementing it risks silently destroying uncommitted user work. |
16 | 16 | //! It stays on [`super::subprocess::git_checkout_ref`], and so does the stash it needs. |
17 | 17 |
|
| 18 | +use std::path::Path; |
| 19 | + |
| 20 | +use crate::{Error, Result}; |
| 21 | + |
| 22 | +/// List the files Git tracks under `xvc_directory`, as paths relative to `xvc_directory`. |
| 23 | +/// |
| 24 | +/// This replaces `git ls-files --full-name`, which Xvc used to shell out for. Index entries are |
| 25 | +/// raw bytes, so reading them directly avoids the quoting `ls-files` applies to its output: |
| 26 | +/// |
| 27 | +/// - Non-ASCII paths. The subprocess version passed `-c core.quotepath=off` to stop Git |
| 28 | +/// octal-escaping them (`"\303\274n..."`); there is nothing to escape here, so the workaround |
| 29 | +/// is gone. |
| 30 | +/// - Paths containing control characters, such as a newline in a filename. Git C-quotes these |
| 31 | +/// *regardless* of `core.quotepath`, yielding the literal `"two\nlines.txt"` — quotes, escape |
| 32 | +/// and all. That never matches an [`crate::XvcPath`], so those files silently escaped the |
| 33 | +/// caller's filter. Index entries are unquoted, so they now match. |
| 34 | +/// - Non-UTF-8 paths. With `core.quotepath=off` Git emits the raw bytes, which the subprocess |
| 35 | +/// crate lossily replaces with U+FFFD. They are skipped here instead, since the caller compares |
| 36 | +/// against `String`s. Either way such a path goes unfiltered; skipping just avoids inventing a |
| 37 | +/// path that does not exist. |
| 38 | +/// |
| 39 | +/// # Relative to what |
| 40 | +/// |
| 41 | +/// Index paths are relative to the *repository* root, which is not necessarily `xvc_directory` — |
| 42 | +/// `xvc init` can run in a subdirectory of a Git repository. Callers compare these against |
| 43 | +/// [`crate::XvcPath`]s, which are relative to the Xvc root, so entries outside `xvc_directory` |
| 44 | +/// are dropped and the rest are re-based onto it. |
| 45 | +pub fn tracked_files(xvc_directory: &Path) -> Result<Vec<String>> { |
| 46 | + let repo = gix::discover(xvc_directory).map_err(|e| Error::GixError { |
| 47 | + cause: e.to_string(), |
| 48 | + })?; |
| 49 | + |
| 50 | + let workdir = repo.workdir().ok_or_else(|| Error::GixError { |
| 51 | + cause: format!( |
| 52 | + "{} is inside a bare Git repository, which tracks no worktree files", |
| 53 | + xvc_directory.display() |
| 54 | + ), |
| 55 | + })?; |
| 56 | + |
| 57 | + // Both sides are canonicalized before comparison: `gix` reports the worktree as configured, |
| 58 | + // which may traverse symlinks differently than the path Xvc was given. |
| 59 | + let prefix = subdirectory_prefix(&workdir.canonicalize()?, &xvc_directory.canonicalize()?)?; |
| 60 | + |
| 61 | + let index = repo.index_or_empty().map_err(|e| Error::GixIndexError { |
| 62 | + cause: e.to_string(), |
| 63 | + })?; |
| 64 | + |
| 65 | + let files = index |
| 66 | + .entries() |
| 67 | + .iter() |
| 68 | + // Conflicted paths appear once per stage. `git ls-files` prints them all; we keep only |
| 69 | + // stage 0, so a path is reported at most once. |
| 70 | + .filter(|entry| entry.stage() == gix::index::entry::Stage::Unconflicted) |
| 71 | + .filter_map(|entry| { |
| 72 | + let path = std::str::from_utf8(entry.path(&index)).ok()?; |
| 73 | + match prefix.as_deref() { |
| 74 | + None => Some(path.to_string()), |
| 75 | + Some(prefix) => path.strip_prefix(prefix).map(ToString::to_string), |
| 76 | + } |
| 77 | + }) |
| 78 | + .collect(); |
| 79 | + |
| 80 | + Ok(files) |
| 81 | +} |
| 82 | + |
| 83 | +/// The slash-separated path from `root` down to `dir`, with a trailing slash, or `None` when they |
| 84 | +/// are the same directory. |
| 85 | +/// |
| 86 | +/// Index paths are slash-separated on every platform, so the components are joined with `/` |
| 87 | +/// rather than the host separator. |
| 88 | +fn subdirectory_prefix(root: &Path, dir: &Path) -> Result<Option<String>> { |
| 89 | + let relative = dir.strip_prefix(root)?; |
| 90 | + let joined = relative |
| 91 | + .components() |
| 92 | + .map(|component| component.as_os_str().to_string_lossy()) |
| 93 | + .collect::<Vec<_>>() |
| 94 | + .join("/"); |
| 95 | + |
| 96 | + Ok((!joined.is_empty()).then(|| format!("{joined}/"))) |
| 97 | +} |
| 98 | + |
18 | 99 | /// Compile-time guard for the `tree-editor` feature in `core/Cargo.toml`. |
19 | 100 | /// |
20 | 101 | /// [`gix::Repository::edit_tree`] is gated behind it and is not enabled by any of `gix`'s default |
|
0 commit comments