review
This commit is contained in:
@@ -44,8 +44,6 @@ impl GitCache {
|
||||
}
|
||||
|
||||
/// Open the existing clone, fetching it first.
|
||||
/// Otherwise clone from the first working URL in `clone_urls`.
|
||||
/// `clone_urls` holds the announcement's `clone` tag.
|
||||
pub fn ensure_clone(&self, addr: &RepoAddr, clone_urls: &[String]) -> Result<gix::Repository> {
|
||||
let path = self.repo_path(addr);
|
||||
|
||||
@@ -66,19 +64,16 @@ impl GitCache {
|
||||
}
|
||||
|
||||
/// Maximum directory nesting depth when scanning for local repositories.
|
||||
///
|
||||
/// Pathological trees can't stall the scan.
|
||||
const SCAN_MAX_DEPTH: usize = 12;
|
||||
|
||||
/// Directories never descended into during a scan.
|
||||
///
|
||||
/// Dependency caches can be enormous without ever containing user repositories.
|
||||
const SCAN_SKIPPED_DIRS: [&str; 1] = ["node_modules"];
|
||||
|
||||
/// Walk `root` recursively and collect the paths of git repositories below it.
|
||||
/// A repository is a directory containing a `.git` entry.
|
||||
/// Hidden entries and symlinks are skipped.
|
||||
/// Repositories are not descended into.
|
||||
/// Nested ones like submodule worktrees are not reported.
|
||||
/// Results are canonicalized, deduplicated and sorted.
|
||||
pub fn find_git_repos(root: &Path) -> Vec<PathBuf> {
|
||||
let mut repos = Vec::new();
|
||||
if !root.is_dir() {
|
||||
@@ -125,10 +120,7 @@ pub fn find_git_repos(root: &Path) -> Vec<PathBuf> {
|
||||
}
|
||||
|
||||
/// Clone into `path` from the first working URL in `clone_urls`.
|
||||
/// `clone_urls` holds the announcement's `clone` tag.
|
||||
/// Then fetch the `refs/nostr/*` PR refs like the cache clone does.
|
||||
/// The destination must not exist yet.
|
||||
/// When no URL works, the last error is returned.
|
||||
///
|
||||
/// Unlike [`GitCache::ensure_clone`], the clone is not kept in any cache.
|
||||
pub fn clone_repo(clone_urls: &[String], path: &Path) -> Result<()> {
|
||||
if path.exists() {
|
||||
@@ -156,8 +148,6 @@ pub fn clone_repo(clone_urls: &[String], path: &Path) -> Result<()> {
|
||||
}
|
||||
|
||||
/// Fetch all configured refspecs from `origin`, plus the `refs/nostr/*` namespace.
|
||||
/// GRASP mirrors serve pull request branches there, one ref per PR event id.
|
||||
/// This mirrors the layout used by ngit.
|
||||
pub fn fetch_all(repo: &gix::Repository) -> Result<()> {
|
||||
let options = gix::remote::ref_map::Options {
|
||||
extra_refspecs: vec![
|
||||
@@ -176,9 +166,10 @@ pub fn fetch_all(repo: &gix::Repository) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply a `git format-patch` patch or series with `git am`.
|
||||
/// Uses the git CLI because it handles the mbox format natively.
|
||||
/// Can be replaced with a pure-Rust implementation later without changing callers.
|
||||
/// Apply a `git format-patch` patch or series with `git am`,
|
||||
/// uses the git CLI because it handles the mbox format natively.
|
||||
///
|
||||
/// TODO: Replaced with a pure-Rust implementation later without changing callers.
|
||||
pub fn apply_patch(repo_path: &Path, patch: &str) -> Result<()> {
|
||||
let mut child = Command::new("git")
|
||||
.arg("am")
|
||||
@@ -201,9 +192,11 @@ pub fn apply_patch(repo_path: &Path, patch: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The merge base of two revisions in the repository at `repo_path`.
|
||||
/// Revisions may be branch names, remote-tracking refs or commit ids.
|
||||
/// The merge base of two revisions in the repository at `repo_path`,
|
||||
/// revisions may be branch names, remote-tracking refs or commit ids.
|
||||
///
|
||||
/// `Ok(None)` when the revisions share no common ancestor.
|
||||
///
|
||||
/// Unresolvable revisions are errors.
|
||||
pub fn merge_base(repo_path: &Path, a: &str, b: &str) -> Result<Option<String>> {
|
||||
let output = Command::new("git")
|
||||
@@ -230,8 +223,8 @@ pub fn merge_base(repo_path: &Path, a: &str, b: &str) -> Result<Option<String>>
|
||||
|
||||
/// The `git format-patch` mbox series of `base..tip`, like `git format-patch --stdout`.
|
||||
/// Fails when the range has no commits.
|
||||
/// The mbox is returned untrimmed.
|
||||
/// Trailing newlines are part of the format.
|
||||
///
|
||||
/// The mbox is returned untrimmed. Trailing newlines are part of the format.
|
||||
pub fn format_patch_between(repo_path: &Path, base: &str, tip: &str) -> Result<String> {
|
||||
let output = Command::new("git")
|
||||
.arg("-C")
|
||||
@@ -256,9 +249,6 @@ pub fn format_patch_between(repo_path: &Path, base: &str, tip: &str) -> Result<S
|
||||
}
|
||||
|
||||
/// Whether `patch` applies to the working tree of `repo_path` without modifying anything.
|
||||
/// `patch` is a `git format-patch` series.
|
||||
/// Checks with `git apply --check --3way`.
|
||||
/// Best-effort, useful to surface conflicts before a patch is published or applied.
|
||||
pub fn patch_applies(repo_path: &Path, patch: &str) -> Result<()> {
|
||||
let mut child = Command::new("git")
|
||||
.arg("apply")
|
||||
@@ -282,14 +272,11 @@ pub fn patch_applies(repo_path: &Path, patch: &str) -> Result<()> {
|
||||
String::from_utf8_lossy(&output.stderr).trim()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Push `commit` to `reference` on the server at `url`, from `repo_path`.
|
||||
/// `reference` is a ref name like `refs/nostr/<event-id>`.
|
||||
/// GRASP servers host the `refs/nostr` namespace so anyone can contribute a commit.
|
||||
/// nak pushes pull request tips there before publishing the PR event.
|
||||
/// Readers fetch the ref to get the commit behind a PR's `c` tag.
|
||||
pub fn push_commit_ref(repo_path: &Path, url: &str, commit: &str, reference: &str) -> Result<()> {
|
||||
let output = Command::new("git")
|
||||
.arg("-C")
|
||||
@@ -312,8 +299,7 @@ pub fn push_commit_ref(repo_path: &Path, url: &str, commit: &str, reference: &st
|
||||
}
|
||||
|
||||
/// Split a `git format-patch` series into its individual patches, mbox messages.
|
||||
/// Each message begins with a `From <40-hex> ` boundary line.
|
||||
/// `>From` quoting inside bodies means no false positives.
|
||||
///
|
||||
/// A single patch yields one element.
|
||||
/// A malformed input yields one element covering it.
|
||||
pub fn split_patch_series(patch: &str) -> Vec<&str> {
|
||||
@@ -342,6 +328,7 @@ pub fn split_patch_series(patch: &str) -> Vec<&str> {
|
||||
}
|
||||
|
||||
/// The commit HEAD points to in the repository at `repo_path`.
|
||||
///
|
||||
/// `None` when the repository has no commits yet, an unborn HEAD.
|
||||
pub fn head_commit_id(repo_path: &Path) -> Result<Option<String>> {
|
||||
let output = Command::new("git")
|
||||
@@ -363,8 +350,8 @@ pub fn head_commit_id(repo_path: &Path) -> Result<Option<String>> {
|
||||
|
||||
/// The commits in `base..HEAD` of the repository at `repo_path`, oldest first.
|
||||
/// This is the order `git am` creates them.
|
||||
///
|
||||
/// `HEAD` alone when `base` is `None`.
|
||||
/// An empty range yields an empty list.
|
||||
pub fn commits_since(repo_path: &Path, base: Option<&str>) -> Result<Vec<String>> {
|
||||
let output = match base {
|
||||
Some(base) => git_in(
|
||||
@@ -402,9 +389,10 @@ fn clone(url: &str, path: &Path) -> Result<gix::Repository> {
|
||||
|
||||
/// Create a repository at `path` with an initial `main` branch.
|
||||
/// Write a `README.md` from `name` and `description`, then create the initial commit.
|
||||
///
|
||||
/// Returns the initial commit id.
|
||||
///
|
||||
/// Uses the git CLI, like [`apply_patch`].
|
||||
/// The CLI handles index writes, ref updates and default branch selection natively.
|
||||
pub fn init_repository(path: &Path, name: &str, description: &str) -> Result<String> {
|
||||
std::fs::create_dir_all(path)
|
||||
.with_context(|| format!("failed to create {}", path.display()))?;
|
||||
@@ -470,6 +458,7 @@ pub fn push_main(repo_path: &Path, base_url: &str, owner: &str, repo_id: &str) -
|
||||
}
|
||||
|
||||
/// Push every local branch and tag of the repository at `repo_path` to a grasp server.
|
||||
///
|
||||
/// This mirrors an initialized repository's whole history.
|
||||
pub fn push_all(repo_path: &Path, base_url: &str, owner: &str, repo_id: &str) -> Result<()> {
|
||||
let url = format!("{base_url}/{owner}/{repo_id}.git");
|
||||
@@ -496,8 +485,8 @@ pub fn push_all(repo_path: &Path, base_url: &str, owner: &str, repo_id: &str) ->
|
||||
}
|
||||
|
||||
/// The earliest unique commit of the repository at `repo_path`.
|
||||
/// A root commit, like `git rev-list --max-parents=0 HEAD`.
|
||||
/// Used as the NIP-34 announcement's `euc` marker.
|
||||
///
|
||||
/// `None` for a repository without commits.
|
||||
pub fn root_commit(repo_path: &Path) -> Result<Option<String>> {
|
||||
let output = Command::new("git")
|
||||
@@ -523,8 +512,7 @@ pub fn root_commit(repo_path: &Path) -> Result<Option<String>> {
|
||||
}
|
||||
|
||||
/// Add `origin` pointing at `url` when the repository has no remote yet.
|
||||
/// Uses the standard fetch mapping.
|
||||
/// Later `git fetch origin` and the cache's `fetch_all` update `refs/remotes/origin/*`.
|
||||
///
|
||||
/// No-op if `origin` already exists.
|
||||
pub fn ensure_origin(repo_path: &Path, url: &str) -> Result<()> {
|
||||
// `git remote get-url origin` exits non-zero when the remote is absent.
|
||||
@@ -543,8 +531,9 @@ pub fn ensure_origin(repo_path: &Path, url: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Point `origin` at `url`, replacing an existing remote.
|
||||
/// Used after a clone whose `origin` points at the cloned-from path.
|
||||
/// Point `origin` at `url`, replacing an existing remote,
|
||||
/// used after a clone whose `origin` points at the cloned-from path.
|
||||
///
|
||||
/// A working copy cloned from a local mirror is re-targeted at the grasp server.
|
||||
pub fn set_origin(repo_path: &Path, url: &str) -> Result<()> {
|
||||
// `git remote get-url origin` exits non-zero when the remote is absent.
|
||||
@@ -557,10 +546,8 @@ pub fn set_origin(repo_path: &Path, url: &str) -> Result<()> {
|
||||
}
|
||||
|
||||
/// Fetch `refspec` into `repo_path` from the first working URL in `urls`.
|
||||
/// An example refspec is `+refs/heads/*:refs/fork/<owner>/<id>/*`.
|
||||
/// Like [`clone_repo`], `grasp://` URLs are rewritten to `https://`.
|
||||
/// The terminal prompt is disabled.
|
||||
/// When no URL works, the last error is returned.
|
||||
///
|
||||
/// Never touches the checked-out refs or the worktree.
|
||||
pub fn fetch_repo_refs(repo_path: &Path, urls: &[String], refspec: &str) -> Result<()> {
|
||||
let mut last_err: Option<anyhow::Error> = None;
|
||||
@@ -599,6 +586,7 @@ pub fn fetch_repo_refs(repo_path: &Path, urls: &[String], refspec: &str) -> Resu
|
||||
|
||||
/// Full ref names under `prefix`, sorted lexicographically, like `git for-each-ref`.
|
||||
/// `prefix` is a ref namespace like `refs/fork/<owner>/<id>`.
|
||||
///
|
||||
/// Returns an empty list when nothing matches.
|
||||
pub fn refs_with_prefix(repo_path: &Path, prefix: &str) -> Result<Vec<String>> {
|
||||
// `for-each-ref` patterns match whole path components.
|
||||
@@ -629,7 +617,9 @@ pub fn refs_with_prefix(repo_path: &Path, prefix: &str) -> Result<Vec<String>> {
|
||||
|
||||
/// Delete every ref under `prefix` of the repository at `repo_path`.
|
||||
/// `prefix` is a ref namespace like `refs/fork/<owner>/<id>`.
|
||||
///
|
||||
/// Lets a stale import be pruned before a re-import.
|
||||
///
|
||||
/// No-op when nothing matches.
|
||||
pub fn delete_refs_with_prefix(repo_path: &Path, prefix: &str) -> Result<()> {
|
||||
let refs = refs_with_prefix(repo_path, prefix)?;
|
||||
@@ -666,6 +656,7 @@ pub fn delete_refs_with_prefix(repo_path: &Path, prefix: &str) -> Result<()> {
|
||||
}
|
||||
|
||||
/// The URL of the `origin` remote of the repository at `workdir`.
|
||||
///
|
||||
/// `None` when it has no `origin` yet.
|
||||
pub fn origin_url(workdir: &Path) -> Result<Option<String>> {
|
||||
let output = Command::new("git")
|
||||
@@ -685,14 +676,7 @@ pub fn origin_url(workdir: &Path) -> Result<Option<String>> {
|
||||
}
|
||||
|
||||
/// Fast-forward local branches that trail their remote-tracking counterpart.
|
||||
/// The counterpart ref is `refs/remotes/origin/<name>` in the repository at `workdir`.
|
||||
/// Like a `git pull --ff-only` on each branch.
|
||||
/// A mirror clone used for browsing catches up without rewriting history.
|
||||
/// The checked-out branch is moved with a merge so its worktree follows.
|
||||
/// A dirty worktree fails the merge cleanly and is left for the next refresh.
|
||||
/// Other branches are updated directly.
|
||||
/// Branches without a remote-tracking counterpart are left alone.
|
||||
/// Local commits of their own also keep a branch untouched.
|
||||
///
|
||||
/// Returns whether any branch moved.
|
||||
pub fn fast_forward_branches(workdir: &Path) -> Result<bool> {
|
||||
let current = git_in(workdir, &["branch", "--show-current"]).unwrap_or_default();
|
||||
@@ -734,6 +718,7 @@ pub fn fast_forward_branches(workdir: &Path) -> Result<bool> {
|
||||
}
|
||||
|
||||
/// Run a git command in `dir`, returning trimmed stdout.
|
||||
///
|
||||
/// The terminal prompt is disabled so a credential request fails instead of hanging.
|
||||
fn git_in(dir: &Path, args: &[&str]) -> Result<String> {
|
||||
let output = Command::new("git")
|
||||
@@ -757,6 +742,7 @@ fn git_in(dir: &Path, args: &[&str]) -> Result<String> {
|
||||
}
|
||||
|
||||
/// Map an untrusted repository id or display name to a safe single path component.
|
||||
///
|
||||
/// Everything outside `[A-Za-z0-9._-]` becomes `_`.
|
||||
/// An id that maps to exactly `.` or `..` becomes `_`.
|
||||
pub fn sanitize_path_component(id: &str) -> String {
|
||||
@@ -779,6 +765,7 @@ pub fn sanitize_path_component(id: &str) -> String {
|
||||
}
|
||||
|
||||
/// In-memory object cache for history walks, see [`open_with_cache`].
|
||||
///
|
||||
/// Without one, every walk re-decodes the same commit objects from the object database.
|
||||
const OBJECT_CACHE_BYTES: usize = 64 * 1024 * 1024;
|
||||
|
||||
@@ -799,7 +786,7 @@ pub struct FileCommit {
|
||||
}
|
||||
|
||||
/// Relative paths of all entries in the worktree, files and directories.
|
||||
/// Directories first, then alphabetically within each group.
|
||||
///
|
||||
/// The `.git` directory is skipped.
|
||||
pub fn worktree_entries(repo: &gix::Repository) -> Result<Vec<PathBuf>> {
|
||||
let workdir = repo.workdir().context("repository has no worktree")?;
|
||||
@@ -816,6 +803,7 @@ pub fn worktree_entries(repo: &gix::Repository) -> Result<Vec<PathBuf>> {
|
||||
}
|
||||
|
||||
/// Read a file from the worktree.
|
||||
///
|
||||
/// Returns `Ok(None)` if the path is missing or not a regular file.
|
||||
pub fn worktree_read(repo: &gix::Repository, rel: &Path) -> Result<Option<Vec<u8>>> {
|
||||
let workdir = repo.workdir().context("repository has no worktree")?;
|
||||
@@ -830,9 +818,7 @@ pub fn worktree_read(repo: &gix::Repository, rel: &Path) -> Result<Option<Vec<u8
|
||||
}
|
||||
|
||||
/// Find the README file in the repository root.
|
||||
/// Returned as a path relative to the worktree.
|
||||
/// Matching is case-insensitive.
|
||||
/// Prefers `README.md`, then `.markdown`, `.mdown`, `.mkdn`.
|
||||
///
|
||||
/// Falls back to any other file whose name starts with `readme`.
|
||||
pub fn find_readme(repo: &gix::Repository) -> Result<Option<PathBuf>> {
|
||||
let Some(workdir) = repo.workdir() else {
|
||||
@@ -878,7 +864,10 @@ fn open_with_cache(workdir: &Path) -> Result<gix::Repository> {
|
||||
|
||||
/// A [`FileCommit`] from a walk commit, with author, message title and shortened id.
|
||||
/// `include_description` controls whether the message body is copied.
|
||||
/// History lists never display it, so skipping it saves an allocation per listed commit.
|
||||
///
|
||||
/// History lists never display it,
|
||||
/// so skipping it saves an allocation per listed commit.
|
||||
///
|
||||
/// The diff panel fetches the full commit on demand.
|
||||
fn file_commit(commit: &gix::Commit<'_>, include_description: bool) -> Result<FileCommit> {
|
||||
let author = commit.author()?;
|
||||
@@ -900,9 +889,7 @@ fn file_commit(commit: &gix::Commit<'_>, include_description: bool) -> Result<Fi
|
||||
}
|
||||
|
||||
/// Find the most recent commit that changed `rel`, a path relative to the worktree.
|
||||
/// Like `git log -1 -- <rel>`.
|
||||
/// Walks newest-first from `HEAD`.
|
||||
/// Reports the first commit whose tree entry for `rel` differs from its first parent's.
|
||||
///
|
||||
/// `Ok(None)` when no commit touched the file, e.g. an untracked file.
|
||||
pub fn last_commit(repo: &gix::Repository, rel: &Path) -> Result<Option<FileCommit>> {
|
||||
let rel = rel.to_path_buf();
|
||||
@@ -914,7 +901,7 @@ pub fn last_commit(repo: &gix::Repository, rel: &Path) -> Result<Option<FileComm
|
||||
|
||||
/// Newest commit touching each of `rels`, like `git log -1 -- <rel>` per path.
|
||||
/// `rels` are paths relative to the worktree.
|
||||
/// A single walk decodes every commit once and shares it across all paths.
|
||||
///
|
||||
/// Paths without any commit, like untracked files, are absent from the result.
|
||||
pub fn worktree_last_commits(
|
||||
workdir: &Path,
|
||||
@@ -924,6 +911,7 @@ pub fn worktree_last_commits(
|
||||
}
|
||||
|
||||
/// The walk behind [`last_commit`] and [`worktree_last_commits`].
|
||||
///
|
||||
/// Stops as soon as every pending path has its commit.
|
||||
fn last_commits(repo: &gix::Repository, rels: &[PathBuf]) -> Result<Vec<(PathBuf, FileCommit)>> {
|
||||
use gix::traverse::commit::simple::CommitTimeOrder;
|
||||
@@ -984,14 +972,13 @@ fn last_commits(repo: &gix::Repository, rels: &[PathBuf]) -> Result<Vec<(PathBuf
|
||||
Ok(found)
|
||||
}
|
||||
|
||||
/// Cap on [`CommitList::commits`].
|
||||
/// The virtual list renders a window at a time, the tab badge shows the real count.
|
||||
/// Cap on [`CommitList::commits`]. The virtual list renders a window at a time,
|
||||
/// the tab badge shows the real count.
|
||||
///
|
||||
/// A huge history is never fully materialized in memory.
|
||||
pub const MAX_LISTED_COMMITS: usize = 20_000;
|
||||
|
||||
/// Commits reachable from `HEAD`, newest first, possibly capped.
|
||||
/// `commits` holds at most [`MAX_LISTED_COMMITS`] entries.
|
||||
/// `total` is the real count, for the tab badge.
|
||||
pub struct CommitList {
|
||||
/// Number of commits reachable from HEAD.
|
||||
pub total: usize,
|
||||
@@ -1000,6 +987,7 @@ pub struct CommitList {
|
||||
}
|
||||
|
||||
/// All commits reachable from `HEAD`, newest first, with author and summary.
|
||||
///
|
||||
/// Returns an empty list for a repository without any commits yet.
|
||||
pub fn all_commits(repo: &gix::Repository) -> Result<CommitList> {
|
||||
use gix::traverse::commit::simple::CommitTimeOrder;
|
||||
@@ -1029,6 +1017,7 @@ pub fn all_commits(repo: &gix::Repository) -> Result<CommitList> {
|
||||
}
|
||||
|
||||
/// Like [`all_commits`], but opens the repository at `workdir` first.
|
||||
///
|
||||
/// For non-bare clones the clone root is the worktree.
|
||||
pub fn worktree_all_commits(workdir: &Path) -> Result<CommitList> {
|
||||
all_commits(&open_with_cache(workdir)?)
|
||||
@@ -1058,7 +1047,6 @@ pub struct DiffLine {
|
||||
}
|
||||
|
||||
/// A hunk of a file diff, like `@@ -a,b +c,d @@`.
|
||||
/// Context around each change, then removals and additions.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DiffHunk {
|
||||
/// 1-based start line in the old version.
|
||||
@@ -1101,16 +1089,14 @@ pub struct FileDiff {
|
||||
}
|
||||
|
||||
/// The changes of one commit.
|
||||
/// Lists every file it added, modified, deleted or renamed.
|
||||
/// Text files carry line-level hunks.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CommitDiff {
|
||||
pub files: Vec<FileDiff>,
|
||||
}
|
||||
|
||||
/// The changes of the commit `id`, short or full, in the repository at `workdir`.
|
||||
///
|
||||
/// Compared against its first parent, the empty tree for the root commit.
|
||||
/// Like `git show`, files are sorted by path.
|
||||
pub fn worktree_commit_diff(workdir: &Path, id: &str) -> Result<CommitDiff> {
|
||||
commit_diff(&open_with_cache(workdir)?, id)
|
||||
}
|
||||
@@ -1127,7 +1113,7 @@ fn commit_diff(repo: &gix::Repository, id: &str) -> Result<CommitDiff> {
|
||||
}
|
||||
|
||||
/// The changes between two commits, `base`..`tip`, like `git diff base tip`.
|
||||
/// Same file handling as [`worktree_commit_diff`].
|
||||
///
|
||||
/// Directories and submodules are skipped, files are sorted by path.
|
||||
pub fn worktree_commit_range_diff(workdir: &Path, base: &str, tip: &str) -> Result<CommitDiff> {
|
||||
let repo = open_with_cache(workdir)?;
|
||||
@@ -1170,8 +1156,7 @@ pub fn worktree_commit_range_commits(
|
||||
Ok(commits)
|
||||
}
|
||||
|
||||
/// The changes between two trees.
|
||||
/// Used by both [`commit_diff`] and [`worktree_commit_range_diff`].
|
||||
/// The changes between two trees. Used by both [`commit_diff`] and [`worktree_commit_range_diff`].
|
||||
fn tree_diff(
|
||||
repo: &gix::Repository,
|
||||
old_tree: Option<&gix::Tree<'_>>,
|
||||
@@ -1290,12 +1275,6 @@ fn tree_diff(
|
||||
}
|
||||
|
||||
/// Parse `git format-patch` output, a single patch or a series.
|
||||
/// Produces the same [`CommitDiff`] structure used for commit diffs.
|
||||
/// The mbox envelope is skipped, the From and Subject headers, commit body and diffstat.
|
||||
/// Every `diff --git` section becomes one [`FileDiff`].
|
||||
/// Paths come from the section headers, with git's C-style quoting undone.
|
||||
/// Sections without hunks are reported without lines.
|
||||
/// That covers pure renames, mode changes and binary files.
|
||||
pub fn patch_diffs(patch: &str) -> Result<CommitDiff> {
|
||||
let lines: Vec<&str> = patch.lines().collect();
|
||||
let mut files = Vec::new();
|
||||
@@ -1315,8 +1294,7 @@ pub fn patch_diffs(patch: &str) -> Result<CommitDiff> {
|
||||
}
|
||||
|
||||
/// Commits of a `git format-patch` output, a single patch or a series.
|
||||
/// Parsed from each patch's mbox envelope headers.
|
||||
/// Yields the commit id, author, summary and author time.
|
||||
///
|
||||
/// Entries appear in patch order, oldest first as `git format-patch` produces them.
|
||||
pub fn patch_commits(patch: &str) -> Vec<FileCommit> {
|
||||
let lines: Vec<&str> = patch.lines().collect();
|
||||
@@ -1379,6 +1357,7 @@ fn name_from_address(from: &str) -> String {
|
||||
}
|
||||
|
||||
/// Strip the patch prefix from a `Subject:` header.
|
||||
///
|
||||
/// Examples are `[PATCH]`, `[PATCH 1/2]` and `[RFC PATCH]`.
|
||||
fn strip_patch_prefix(subject: &str) -> String {
|
||||
let trimmed = subject.trim();
|
||||
@@ -1396,7 +1375,7 @@ fn strip_patch_prefix(subject: &str) -> String {
|
||||
}
|
||||
|
||||
/// Parse one file's diff section.
|
||||
/// Everything after the `diff --git` header up to the next section or the end of the patch.
|
||||
///
|
||||
/// Returns the section and the index of the first unconsumed line.
|
||||
fn parse_diff_section(header: &str, lines: &[&str], start: usize) -> Result<(FileDiff, usize)> {
|
||||
let (header_old, header_new) = header_paths(header)?;
|
||||
@@ -1484,7 +1463,7 @@ fn parse_diff_section(header: &str, lines: &[&str], start: usize) -> Result<(Fil
|
||||
}
|
||||
|
||||
/// Parse one hunk, the `@@ -a,b +c,d @@` header plus every body line.
|
||||
/// Lines end at the next hunk header, `diff --git` section or the end of the patch.
|
||||
///
|
||||
/// Returns the hunk and the index of the first unconsumed line.
|
||||
fn parse_hunk(lines: &[&str], start: usize) -> Result<(DiffHunk, usize)> {
|
||||
let (old_start, old_lines, new_start, new_lines) = hunk_header(lines[start])?;
|
||||
@@ -1543,6 +1522,7 @@ fn parse_hunk(lines: &[&str], start: usize) -> Result<(DiffHunk, usize)> {
|
||||
}
|
||||
|
||||
/// The kind of a hunk body line, from its first character.
|
||||
///
|
||||
/// Lines outside a hunk, headers, `\ No newline...` and the next section, yield `None`.
|
||||
fn line_prefix_kind(line: &str) -> Option<DiffLineKind> {
|
||||
match line.as_bytes().first()? {
|
||||
@@ -1554,6 +1534,7 @@ fn line_prefix_kind(line: &str) -> Option<DiffLineKind> {
|
||||
}
|
||||
|
||||
/// Parse a unified-diff hunk header, `@@ -a,b +c,d @@`.
|
||||
///
|
||||
/// Omitted line counts default to 1.
|
||||
fn hunk_header(header: &str) -> Result<(u32, u32, u32, u32)> {
|
||||
let rest = header
|
||||
@@ -1577,9 +1558,6 @@ fn hunk_header(header: &str) -> Result<(u32, u32, u32, u32)> {
|
||||
}
|
||||
|
||||
/// The old and new paths of a `diff --git a/X b/Y` header.
|
||||
/// Git's C-style quoting is undone.
|
||||
/// Git only quotes paths that need escaping, non-ASCII bytes, `"` and `\`.
|
||||
/// Plain spaces stay unquoted, so an unquoted header splits at the last ` b/`.
|
||||
fn header_paths(header: &str) -> Result<(String, String)> {
|
||||
if header.starts_with('"') {
|
||||
// Quoted paths include the `a/` / `b/` prefix inside the quotes.
|
||||
@@ -1609,9 +1587,6 @@ fn header_paths(header: &str) -> Result<(String, String)> {
|
||||
}
|
||||
|
||||
/// The path of a `--- a/X` or `+++ b/Y` line.
|
||||
/// The prefix is stripped, the trailing tab removed, C-style quoting undone.
|
||||
/// Git adds a trailing padding tab for paths containing spaces.
|
||||
/// These lines name the two sides unambiguously, unlike the `diff --git` header.
|
||||
fn diff_line_path(line: &str, prefix: &str) -> Result<String> {
|
||||
let line = line.trim_end_matches('\t');
|
||||
if line.starts_with('"') {
|
||||
@@ -1630,8 +1605,8 @@ fn diff_line_path(line: &str, prefix: &str) -> Result<String> {
|
||||
|
||||
/// The content of a git C-style quoted path and the rest of the input.
|
||||
/// The path spans the opening `"`, escaped content and closing `"`.
|
||||
///
|
||||
/// `None` if unterminated.
|
||||
/// Iterates by character so slices land on UTF-8 boundaries even for non-ASCII paths.
|
||||
fn take_quoted(input: &str) -> Option<(&str, &str)> {
|
||||
let mut end = 1; // byte after the opening quote
|
||||
let mut rest = &input[1..];
|
||||
@@ -1708,10 +1683,6 @@ fn unquote_path(path: &str) -> Result<String> {
|
||||
}
|
||||
|
||||
/// Collects the hunks of one blob diff while tracking per-line numbers.
|
||||
/// The unified-diff headers give the 1-based start line of the hunk in each file.
|
||||
/// Context lines advance both counters.
|
||||
/// Removals advance only the old counter, additions only the new one.
|
||||
/// Each line then carries its real numbers in both versions.
|
||||
struct HunkCollector<'a> {
|
||||
hunks: &'a mut Vec<DiffHunk>,
|
||||
insertions: &'a mut usize,
|
||||
@@ -1784,6 +1755,7 @@ impl ConsumeHunk for HunkCollector<'_> {
|
||||
}
|
||||
|
||||
/// The commit HEAD points to, like `git log -1`.
|
||||
///
|
||||
/// `Ok(None)` for a repository without commits yet, an unborn HEAD.
|
||||
pub fn head_commit(repo: &gix::Repository) -> Result<Option<FileCommit>> {
|
||||
let Some(head) = repo.head_id().ok() else {
|
||||
@@ -1795,9 +1767,8 @@ pub fn head_commit(repo: &gix::Repository) -> Result<Option<FileCommit>> {
|
||||
|
||||
/// Full metadata of the commit `id`, short or full, in the repository at `workdir`.
|
||||
/// Like [`head_commit`] for an arbitrary commit.
|
||||
///
|
||||
/// `Ok(None)` when the id cannot be resolved.
|
||||
/// The commit list, [`all_commits`], omits message bodies to keep the walk cheap.
|
||||
/// The diff panel uses this to fetch the full commit on demand.
|
||||
pub fn worktree_commit(workdir: &Path, id: &str) -> Result<Option<FileCommit>> {
|
||||
let repo = open_with_cache(workdir)?;
|
||||
match repo.rev_parse_single(id.as_bytes()) {
|
||||
@@ -1842,6 +1813,7 @@ pub fn worktree_tags(workdir: &Path) -> Result<Vec<String>> {
|
||||
}
|
||||
|
||||
/// Short name of the branch HEAD points to, or `None` when detached.
|
||||
///
|
||||
/// Detached after checking out a tag or a commit directly.
|
||||
pub fn current_branch(repo: &gix::Repository) -> Result<Option<String>> {
|
||||
let head = repo.head()?;
|
||||
@@ -1852,6 +1824,7 @@ pub fn current_branch(repo: &gix::Repository) -> Result<Option<String>> {
|
||||
}
|
||||
|
||||
/// Branch, tag and HEAD refs of a repository.
|
||||
///
|
||||
/// Ready for a NIP-34 kind-30618 repository state announcement.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RepoRefState {
|
||||
@@ -1862,6 +1835,7 @@ pub struct RepoRefState {
|
||||
}
|
||||
|
||||
/// Collect the refs of `repo`.
|
||||
///
|
||||
/// Local branches and tags become `(refname, commit-id)` pairs.
|
||||
/// Also reports the branch HEAD points to.
|
||||
pub fn repo_ref_state(repo: &gix::Repository) -> Result<RepoRefState> {
|
||||
@@ -1914,8 +1888,8 @@ pub struct WorktreeSnapshot {
|
||||
}
|
||||
|
||||
/// Snapshot the worktree after a branch or tag switch.
|
||||
///
|
||||
/// Collects entries, the README, the branch HEAD points to and its commit.
|
||||
/// Opens the repository once.
|
||||
pub fn worktree_snapshot(workdir: &Path) -> Result<WorktreeSnapshot> {
|
||||
let repo = open_with_cache(workdir)?;
|
||||
let readme_path = find_readme(&repo)?;
|
||||
@@ -1933,6 +1907,7 @@ pub fn worktree_snapshot(workdir: &Path) -> Result<WorktreeSnapshot> {
|
||||
}
|
||||
|
||||
/// Switch the checked-out ref and update the worktree, like `git checkout --force`.
|
||||
///
|
||||
/// Local modifications are discarded, these clones are read-only browser copies.
|
||||
fn checkout(workdir: &Path, args: &[&str]) -> Result<()> {
|
||||
let output = Command::new("git")
|
||||
|
||||
Reference in New Issue
Block a user