Skip to main content

branchless/git/
repo.rs

1//! Operations on the Git repository. This module exists for a few reasons:
2//!
3//! - To ensure that every call to a Git operation has an associated `wrap_err`
4//!   for use with `Try`.
5//! - To improve the interface in some cases. In particular, some operations in
6//!   `git2` return an `Error` with code `ENOTFOUND`, but we should really return
7//!   an `Option` in those cases.
8//! - To make it possible to audit all the Git operations carried out in the
9//!   codebase.
10//! - To collect some different helper Git functions.
11
12use std::borrow::Borrow;
13use std::collections::{HashMap, HashSet};
14use std::num::TryFromIntError;
15use std::ops::Add;
16#[cfg(unix)]
17use std::os::unix::ffi::OsStrExt;
18use std::path::{Path, PathBuf};
19use std::str::FromStr;
20use std::time::{Duration, SystemTime};
21use std::{io, time};
22
23use bstr::ByteVec;
24use chrono::{DateTime, Utc};
25use cursive::theme::BaseColor;
26use cursive::utils::markup::StyledString;
27use git2::DiffOptions;
28use itertools::Itertools;
29use thiserror::Error;
30use tracing::{instrument, warn};
31
32use crate::core::effects::{Effects, OperationType};
33use crate::core::eventlog::EventTransactionId;
34use crate::core::formatting::Glyphs;
35use crate::git::config::{Config, ConfigRead};
36use crate::git::object::Blob;
37use crate::git::oid::{MaybeZeroOid, NonZeroOid, make_non_zero_oid};
38use crate::git::reference::ReferenceNameError;
39use crate::git::run::GitRunInfo;
40use crate::git::tree::{Tree, dehydrate_tree, get_changed_paths_between_trees, hydrate_tree};
41use crate::git::{Branch, BranchType, Commit, Reference, ReferenceName};
42
43use super::index::{Index, IndexEntry};
44use super::snapshot::WorkingCopySnapshot;
45use super::status::FileMode;
46use super::{Diff, StatusEntry, tree};
47
48#[allow(missing_docs)]
49#[derive(Debug, Error)]
50pub enum Error {
51    #[error("could not open repository: {0}")]
52    OpenRepo(#[source] git2::Error),
53
54    #[error("could not find repository to open for worktree {path:?}")]
55    OpenParentWorktreeRepository { path: PathBuf },
56
57    #[error("could not open repository: {0}")]
58    UnsupportedExtensionWorktreeConfig(#[source] git2::Error),
59
60    #[error("could not read index: {0}")]
61    ReadIndex(#[source] git2::Error),
62
63    #[error("could not create .git/branchless directory at {path}: {source}")]
64    CreateBranchlessDir { source: io::Error, path: PathBuf },
65
66    #[error("could not open database connection at {path}: {source}")]
67    OpenDatabase {
68        source: rusqlite::Error,
69        path: PathBuf,
70    },
71
72    #[error("this repository does not have an associated working copy")]
73    NoWorkingCopyPath,
74
75    #[error("could not read config: {0}")]
76    ReadConfig(#[source] git2::Error),
77
78    #[error("could not set HEAD (detached) to {oid}: {source}")]
79    SetHead {
80        source: git2::Error,
81        oid: NonZeroOid,
82    },
83
84    #[error("could not find object {oid}")]
85    FindObject { oid: NonZeroOid },
86
87    #[error("could not calculate merge-base between {lhs} and {rhs}: {source}")]
88    FindMergeBase {
89        source: git2::Error,
90        lhs: NonZeroOid,
91        rhs: NonZeroOid,
92    },
93
94    #[error("could not find blob {oid}: {source} ")]
95    FindBlob {
96        source: git2::Error,
97        oid: NonZeroOid,
98    },
99
100    #[error("could not create blob: {0}")]
101    CreateBlob(#[source] git2::Error),
102
103    #[error("could not create blob from {path}: {source}")]
104    CreateBlobFromPath { source: eyre::Error, path: PathBuf },
105
106    #[error("could not find commit {oid}: {source}")]
107    FindCommit {
108        source: git2::Error,
109        oid: NonZeroOid,
110    },
111
112    #[error("could not create commit: {0}")]
113    CreateCommit(#[source] git2::Error),
114
115    #[error("could not cherry-pick commit {commit} onto {onto}: {source}")]
116    CherryPickCommit {
117        source: git2::Error,
118        commit: NonZeroOid,
119        onto: NonZeroOid,
120    },
121
122    #[error("could not fast-cherry-pick commit {commit} onto {onto}: {source}")]
123    CherryPickFast {
124        source: git2::Error,
125        commit: NonZeroOid,
126        onto: NonZeroOid,
127    },
128
129    #[error("could not amend the current commit: {0}")]
130    Amend(#[source] git2::Error),
131
132    #[error("could not find tree {oid}: {source}")]
133    FindTree {
134        source: git2::Error,
135        oid: MaybeZeroOid,
136    },
137
138    #[error(transparent)]
139    ReadTree(tree::Error),
140
141    #[error(transparent)]
142    ReadTreeEntry(tree::Error),
143
144    #[error(transparent)]
145    HydrateTree(tree::Error),
146
147    #[error("could not write index as tree: {0}")]
148    WriteIndexToTree(#[source] git2::Error),
149
150    #[error("could not read branch information: {0}")]
151    ReadBranch(#[source] git2::Error),
152
153    #[error("could not find branch with name '{name}': {source}")]
154    FindBranch { source: git2::Error, name: String },
155
156    #[error("could not find upstream branch for branch with name '{name}': {source}")]
157    FindUpstreamBranch { source: git2::Error, name: String },
158
159    #[error("could not create branch with name '{name}': {source}")]
160    CreateBranch { source: git2::Error, name: String },
161
162    #[error("could not read reference information: {0}")]
163    ReadReference(#[source] git2::Error),
164
165    #[error("could not find reference '{}': {source}", name.as_str())]
166    FindReference {
167        source: git2::Error,
168        name: ReferenceName,
169    },
170
171    #[error("could not rename branch to '{new_name}': {source}")]
172    RenameBranch {
173        source: git2::Error,
174        new_name: String,
175    },
176
177    #[error("could not delete branch: {0}")]
178    DeleteBranch(#[source] git2::Error),
179
180    #[error("could not delete reference: {0}")]
181    DeleteReference(#[source] git2::Error),
182
183    #[error("could not resolve reference: {0}")]
184    ResolveReference(#[source] git2::Error),
185
186    #[error("could not diff trees {old_tree} and {new_tree}: {source}")]
187    DiffTreeToTree {
188        source: git2::Error,
189        old_tree: MaybeZeroOid,
190        new_tree: MaybeZeroOid,
191    },
192
193    #[error("could not diff tree {tree} and index: {source}")]
194    DiffTreeToIndex {
195        source: git2::Error,
196        tree: NonZeroOid,
197    },
198
199    #[error(transparent)]
200    DehydrateTree(tree::Error),
201
202    #[error("could not create working copy snapshot: {0}")]
203    CreateSnapshot(#[source] eyre::Error),
204
205    #[error("could not create reference: {0}")]
206    CreateReference(#[source] git2::Error),
207
208    #[error("could not calculate changed paths: {0}")]
209    GetChangedPaths(#[source] super::tree::Error),
210
211    #[error("could not get paths touched by commit {commit}")]
212    GetPatch { commit: NonZeroOid },
213
214    #[error("compute patch ID: {0}")]
215    GetPatchId(#[source] git2::Error),
216
217    #[error("could not get references: {0}")]
218    GetReferences(#[source] git2::Error),
219
220    #[error("could not get branches: {0}")]
221    GetBranches(#[source] git2::Error),
222
223    #[error("could not get remote names: {0}")]
224    GetRemoteNames(#[source] git2::Error),
225
226    #[error("HEAD is unborn (try making a commit?)")]
227    UnbornHead,
228
229    #[error("could not create commit signature: {0}")]
230    CreateSignature(#[source] git2::Error),
231
232    #[error("could not execute git: {0}")]
233    ExecGit(#[source] eyre::Error),
234
235    #[error("unsupported spec: {0} (ends with @, which is buggy in libgit2")]
236    UnsupportedRevParseSpec(String),
237
238    #[error("could not parse git version output: {0}")]
239    ParseGitVersionOutput(String),
240
241    #[error("could not parse git version specifier: {0}")]
242    ParseGitVersionSpecifier(String),
243
244    #[error("comment char was not ASCII: {char}")]
245    CommentCharNotAscii { source: TryFromIntError, char: u32 },
246
247    #[error("unknown status line prefix ASCII character: {prefix}")]
248    UnknownStatusLinePrefix { prefix: u8 },
249
250    #[error("could not parse status line: {0}")]
251    ParseStatusEntry(#[source] eyre::Error),
252
253    #[error("could not decode UTF-8 value for {item}")]
254    DecodeUtf8 { item: &'static str },
255
256    #[error("could not decode UTF-8 value for reference name: {0}")]
257    DecodeReferenceName(#[from] ReferenceNameError),
258
259    #[error("could not read message trailers: {0}")]
260    ReadMessageTrailer(#[source] git2::Error),
261
262    #[error("could not describe commit {commit}: {source}")]
263    DescribeCommit {
264        source: eyre::Error,
265        commit: NonZeroOid,
266    },
267
268    #[error(transparent)]
269    IntegerConvert(TryFromIntError),
270
271    #[error(transparent)]
272    SystemTime(time::SystemTimeError),
273
274    #[error(transparent)]
275    Git(git2::Error),
276
277    #[error(transparent)]
278    Io(io::Error),
279
280    #[error("miscellaneous error: {0}")]
281    Other(String),
282}
283
284/// Result type.
285pub type Result<T> = std::result::Result<T, Error>;
286
287pub use git2::ErrorCode as GitErrorCode;
288
289/// Convert a `git2::Error` into an `eyre::Error` with an auto-generated message.
290pub(super) fn wrap_git_error(error: git2::Error) -> eyre::Error {
291    eyre::eyre!("Git error {:?}: {}", error.code(), error.message())
292}
293
294/// Clean up a message, removing extraneous whitespace plus comment lines starting with
295/// `comment_char`, and ensure that the message ends with a newline.
296#[instrument]
297pub fn message_prettify(message: &str, comment_char: Option<char>) -> Result<String> {
298    let comment_char = match comment_char {
299        Some(ch) => {
300            let ch = u32::from(ch);
301            let ch = u8::try_from(ch).map_err(|err| Error::CommentCharNotAscii {
302                source: err,
303                char: ch,
304            })?;
305            Some(ch)
306        }
307        None => None,
308    };
309    let message = git2::message_prettify(message, comment_char).map_err(Error::Git)?;
310    Ok(message)
311}
312
313/// A snapshot of information about a certain reference. Updates to the
314/// reference after this value is obtained are not reflected.
315///
316/// `HEAD` is typically a symbolic reference, which means that it's a reference
317/// that points to another reference. Usually, the other reference is a branch.
318/// In this way, you can check out a branch and move the branch (e.g. by
319/// committing) and `HEAD` is also effectively updated (you can traverse the
320/// pointed-to reference and get the current commit OID).
321///
322/// There are a couple of interesting edge cases to worry about:
323///
324/// - `HEAD` is detached. This means that it's pointing directly to a commit and
325///   is not a symbolic reference for the time being. This is uncommon in normal
326///   Git usage, but very common in `git-branchless` usage.
327/// - `HEAD` is unborn. This means that it doesn't even exist yet. This happens
328///   when a repository has been freshly initialized, but no commits have been
329///   made, for example.
330#[derive(Debug, PartialEq, Eq)]
331pub struct ResolvedReferenceInfo {
332    /// The OID of the commit that `HEAD` points to. If `HEAD` is unborn, then
333    /// this is `None`.
334    pub oid: Option<NonZeroOid>,
335
336    /// The name of the reference that `HEAD` points to symbolically. If `HEAD`
337    /// is detached, then this is `None`.
338    pub reference_name: Option<ReferenceName>,
339}
340
341impl ResolvedReferenceInfo {
342    /// Get the name of the branch, if any. Returns `None` if `HEAD` is
343    /// detached. The `refs/heads/` prefix, if any, is stripped.
344    pub fn get_branch_name(&self) -> Result<Option<&str>> {
345        let reference_name = match &self.reference_name {
346            Some(reference_name) => reference_name.as_str(),
347            None => return Ok(None),
348        };
349        Ok(Some(
350            reference_name
351                .strip_prefix("refs/heads/")
352                .unwrap_or(reference_name),
353        ))
354    }
355}
356
357/// The parsed version of Git.
358#[derive(Debug, PartialEq, PartialOrd, Eq)]
359pub struct GitVersion(pub isize, pub isize, pub isize);
360
361impl FromStr for GitVersion {
362    type Err = Error;
363
364    #[instrument]
365    fn from_str(output: &str) -> Result<GitVersion> {
366        let output = output.trim();
367        let words = output.split(&[' ', '-'][..]).collect::<Vec<&str>>();
368        let version_str: &str = match &words.as_slice() {
369            [_git, _version, version_str, ..] => version_str,
370            _ => return Err(Error::ParseGitVersionOutput(output.to_owned())),
371        };
372        match version_str.split('.').collect::<Vec<&str>>().as_slice() {
373            [major, minor, patch, ..] => {
374                let major = major
375                    .parse()
376                    .map_err(|_| Error::ParseGitVersionSpecifier(version_str.to_owned()))?;
377                let minor = minor
378                    .parse()
379                    .map_err(|_| Error::ParseGitVersionSpecifier(version_str.to_owned()))?;
380
381                // Example version without a real patch number: `2.33.GIT`.
382                let patch: isize = patch.parse().unwrap_or_default();
383
384                Ok(GitVersion(major, minor, patch))
385            }
386            _ => Err(Error::ParseGitVersionSpecifier(version_str.to_owned())),
387        }
388    }
389}
390
391/// Options for `Repo::cherry_pick_fast`.
392#[derive(Clone, Debug)]
393pub struct CherryPickFastOptions {
394    /// Detect if a commit is being applied onto a parent with the same tree,
395    /// and skip applying the patch in that case.
396    pub reuse_parent_tree_if_possible: bool,
397}
398
399/// An error raised when attempting to create create a commit via
400/// `Repo::cherry_pick_fast`.
401#[allow(missing_docs)]
402#[derive(Debug, Error)]
403pub enum CreateCommitFastError {
404    /// A merge conflict occurred, so the cherry-pick could not continue.
405    #[error("merge conflict in {} paths", conflicting_paths.len())]
406    MergeConflict {
407        /// The paths that were in conflict.
408        conflicting_paths: HashSet<PathBuf>,
409    },
410
411    #[error("could not get conflicts generated by cherry-pick of {commit} onto {onto}: {source}")]
412    GetConflicts {
413        source: git2::Error,
414        commit: NonZeroOid,
415        onto: NonZeroOid,
416    },
417
418    #[error("invalid UTF-8 for {item} path: {source}")]
419    DecodePath {
420        source: bstr::FromUtf8Error,
421        item: &'static str,
422    },
423
424    #[error(transparent)]
425    HydrateTree(tree::Error),
426
427    #[error(transparent)]
428    Repo(#[from] Error),
429
430    #[error(transparent)]
431    Git(git2::Error),
432}
433
434/// Options for `Repo::amend_fast`
435#[derive(Debug)]
436pub enum AmendFastOptions<'repo> {
437    /// Amend a set of paths from the current state of the working copy.
438    FromWorkingCopy {
439        /// The status entries for the files to amend.
440        status_entries: Vec<StatusEntry>,
441    },
442    /// Amend a set of paths from the current state of the index.
443    FromIndex {
444        /// The paths to amend.
445        paths: Vec<PathBuf>,
446    },
447    /// Amend a set of paths from a different commit.
448    FromCommit {
449        /// The commit whose contents will be amended.
450        commit: Commit<'repo>,
451    },
452}
453
454impl AmendFastOptions<'_> {
455    /// Returns whether there are any paths to be amended.
456    pub fn is_empty(&self) -> bool {
457        match &self {
458            AmendFastOptions::FromIndex { paths } => paths.is_empty(),
459            AmendFastOptions::FromWorkingCopy { status_entries } => status_entries.is_empty(),
460            AmendFastOptions::FromCommit { commit } => commit.is_empty(),
461        }
462    }
463}
464
465/// Wrapper around `git2::Repository`.
466pub struct Repo {
467    pub(super) inner: git2::Repository,
468}
469
470impl std::fmt::Debug for Repo {
471    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
472        write!(f, "<Git repository at: {:?}>", self.get_path())
473    }
474}
475
476impl Repo {
477    /// Get the Git repository associated with the given directory.
478    #[instrument]
479    pub fn from_dir(path: &Path) -> Result<Self> {
480        let repo = match git2::Repository::discover(path) {
481            Ok(repo) => repo,
482            Err(err)
483                if err.code() == git2::ErrorCode::GenericError
484                    && err
485                        .message()
486                        .contains("unsupported extension name extensions.worktreeconfig") =>
487            {
488                return Err(Error::UnsupportedExtensionWorktreeConfig(err));
489            }
490            Err(err) => return Err(Error::OpenRepo(err)),
491        };
492        Ok(Repo { inner: repo })
493    }
494
495    /// Get the Git repository associated with the current directory.
496    #[instrument]
497    pub fn from_current_dir() -> Result<Self> {
498        let path = std::env::current_dir().map_err(Error::Io)?;
499        Repo::from_dir(&path)
500    }
501
502    /// Open a new copy of the repository.
503    #[instrument]
504    pub fn try_clone(&self) -> Result<Self> {
505        let path = self.get_path();
506        let repo = git2::Repository::open(path).map_err(Error::OpenRepo)?;
507        Ok(Repo { inner: repo })
508    }
509
510    /// Get the path to the `.git` directory for the repository.
511    pub fn get_path(&self) -> &Path {
512        self.inner.path()
513    }
514
515    /// Get the path to the `packed-refs` file for the repository.
516    pub fn get_packed_refs_path(&self) -> PathBuf {
517        self.inner.path().join("packed-refs")
518    }
519
520    /// Get the path to the directory inside the `.git` directory which contains
521    /// state used for the current rebase (if any).
522    pub fn get_rebase_state_dir_path(&self) -> PathBuf {
523        self.inner.path().join("rebase-merge")
524    }
525
526    /// Get the path to the working copy for this repository. If the repository
527    /// is bare (has no working copy), returns `None`.
528    pub fn get_working_copy_path(&self) -> Option<PathBuf> {
529        let workdir = self.inner.workdir()?;
530        if !self.inner.is_worktree() {
531            return Some(workdir.to_owned());
532        }
533
534        // Under some circumstances (not sure exactly when),
535        // `git2::Repository::workdir` on a worktree returns a path like
536        // `/path/to/repo/.git/worktrees/worktree-name/` instead of
537        // `/path/to/worktree/`.
538        let gitdir_file = workdir.join("gitdir");
539        let gitdir = match std::fs::read_to_string(&gitdir_file) {
540            Ok(gitdir) => gitdir,
541            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
542                return Some(workdir.to_path_buf());
543            }
544            Err(err) => {
545                warn!(
546                    ?workdir,
547                    ?gitdir_file,
548                    ?err,
549                    "gitdir file for worktree could not be read; cannot get workdir path"
550                );
551                return None;
552            }
553        };
554        let gitdir = match gitdir.strip_suffix('\n') {
555            Some(gitdir) => gitdir,
556            None => gitdir.as_str(),
557        };
558        let gitdir = PathBuf::from(gitdir);
559        let workdir = gitdir.parent()?; // remove `.git` suffix
560        std::fs::canonicalize(workdir).ok().or_else(|| {
561            warn!(?workdir, "Failed to canonicalize workdir");
562            None
563        })
564    }
565
566    /// Get the index file for this repository.
567    pub fn get_index(&self) -> Result<Index> {
568        let mut index = self.inner.index().map_err(Error::ReadIndex)?;
569        // If we call `get_index` twice in a row, it seems to return the same index contents, even if the on-disk index has changed.
570        index.read(false).map_err(Error::ReadIndex)?;
571        Ok(Index { inner: index })
572    }
573
574    /// If this repository is a worktree for another "parent" repository, return a [`Repo`] object
575    /// corresponding to that repository.
576    #[instrument]
577    pub fn open_worktree_parent_repo(&self) -> Result<Option<Self>> {
578        if !self.inner.is_worktree() {
579            return Ok(None);
580        }
581
582        let parent_repo_path = self.inner.commondir();
583        let parent_repo = Self::from_dir(parent_repo_path)?;
584        Ok(Some(parent_repo))
585    }
586
587    /// Get the configuration object for the repository.
588    ///
589    /// **Warning**: This object should only be used for read operations. Write
590    /// operations should go to the `config` file under the `.git/branchless`
591    /// directory.
592    #[instrument]
593    pub fn get_readonly_config(&self) -> Result<impl ConfigRead> {
594        let config = self.inner.config().map_err(Error::ReadConfig)?;
595        Ok(Config::from(config))
596    }
597
598    /// Get the directory where all repo-specific git-branchless state is stored.
599    pub fn get_branchless_dir(&self) -> Result<PathBuf> {
600        let maybe_worktree_parent_repo = self.open_worktree_parent_repo()?;
601        let repo = match maybe_worktree_parent_repo.as_ref() {
602            Some(repo) => repo,
603            None => self,
604        };
605        let dir = repo.get_path().join("branchless");
606        std::fs::create_dir_all(&dir).map_err(|err| Error::CreateBranchlessDir {
607            source: err,
608            path: dir.clone(),
609        })?;
610        Ok(dir)
611    }
612
613    /// Get the file where git-branchless-specific Git configuration is stored.
614    #[instrument]
615    pub fn get_config_path(&self) -> Result<PathBuf> {
616        Ok(self.get_branchless_dir()?.join("config"))
617    }
618
619    /// Get the directory where the DAG for the repository is stored.
620    #[instrument]
621    pub fn get_dag_dir(&self) -> Result<PathBuf> {
622        // Updated from `dag` to `dag2` for `sapling-dag==0.1.0`, since it may
623        // not be backwards-compatible.
624        Ok(self.get_branchless_dir()?.join("dag2"))
625    }
626
627    /// Get the directory to store man-pages. Note that this is the `man`
628    /// directory, and not a subsection thereof. `git-branchless` man-pages must
629    /// go into the `man/man1` directory to be found by `man`.
630    #[instrument]
631    pub fn get_man_dir(&self) -> Result<PathBuf> {
632        Ok(self.get_branchless_dir()?.join("man"))
633    }
634
635    /// Get a directory suitable for storing temporary files.
636    ///
637    /// In particular, this directory is guaranteed to be on the same filesystem
638    /// as the Git repository itself, so you can move files between them
639    /// atomically. See
640    /// <https://github.com/arxanas/git-branchless/discussions/120>.
641    #[instrument]
642    pub fn get_tempfile_dir(&self) -> Result<PathBuf> {
643        Ok(self.get_branchless_dir()?.join("tmp"))
644    }
645
646    /// Get the connection to the SQLite database for this repository.
647    #[instrument]
648    pub fn get_db_conn(&self) -> Result<rusqlite::Connection> {
649        let dir = self.get_branchless_dir()?;
650        let path = dir.join("db.sqlite3");
651        let conn = rusqlite::Connection::open(&path).map_err(|err| Error::OpenDatabase {
652            source: err,
653            path: path.clone(),
654        })?;
655        Ok(conn)
656    }
657
658    /// Get a snapshot of information about a given reference.
659    #[instrument]
660    pub fn resolve_reference(&self, reference: &Reference) -> Result<ResolvedReferenceInfo> {
661        let oid = reference.peel_to_commit()?.map(|commit| commit.get_oid());
662        let reference_name: Option<ReferenceName> = match reference.inner.kind() {
663            Some(git2::ReferenceType::Direct) => None,
664            Some(git2::ReferenceType::Symbolic) => match reference.inner.symbolic_target_bytes() {
665                Some(name) => Some(ReferenceName::from_bytes(name.to_vec())?),
666                None => {
667                    return Err(Error::DecodeUtf8 { item: "reference" });
668                }
669            },
670            None => return Err(Error::Other("Unknown `HEAD` reference type".to_string())),
671        };
672        Ok(ResolvedReferenceInfo {
673            oid,
674            reference_name,
675        })
676    }
677
678    /// Get the OID for the repository's `HEAD` reference.
679    #[instrument]
680    pub fn get_head_info(&self) -> Result<ResolvedReferenceInfo> {
681        match self.find_reference(&"HEAD".into())? {
682            Some(reference) => self.resolve_reference(&reference),
683            None => Ok(ResolvedReferenceInfo {
684                oid: None,
685                reference_name: None,
686            }),
687        }
688    }
689
690    /// Get the OID for a given [ReferenceName] if it exists.
691    #[instrument]
692    pub fn reference_name_to_oid(&self, name: &ReferenceName) -> Result<MaybeZeroOid> {
693        match self.inner.refname_to_id(name.as_str()) {
694            Ok(git2_oid) => Ok(MaybeZeroOid::from(git2_oid)),
695            Err(source) => Err(Error::FindReference {
696                source,
697                name: name.clone(),
698            }),
699        }
700    }
701
702    /// Set the `HEAD` reference directly to the provided `oid`. Does not touch
703    /// the working copy.
704    #[instrument]
705    pub fn set_head(&self, oid: NonZeroOid) -> Result<()> {
706        self.inner
707            .set_head_detached(oid.inner)
708            .map_err(|err| Error::SetHead { source: err, oid })?;
709        Ok(())
710    }
711
712    /// Detach `HEAD` by making it point directly to its current OID, rather
713    /// than to a branch. If `HEAD` is unborn, logs a warning.
714    #[instrument]
715    pub fn detach_head(&self, head_info: &ResolvedReferenceInfo) -> Result<()> {
716        match head_info.oid {
717            Some(oid) => self
718                .inner
719                .set_head_detached(oid.inner)
720                .map_err(|err| Error::SetHead { source: err, oid }),
721            None => {
722                warn!("Attempted to detach `HEAD` while `HEAD` is unborn");
723                Ok(())
724            }
725        }
726    }
727
728    /// Detect if an interactive rebase has started but not completed.
729    ///
730    /// Git will send us spurious `post-rewrite` events marked as `amend` during an
731    /// interactive rebase, indicating that some of the commits have been rewritten
732    /// as part of the rebase plan, but not all of them. This function attempts to
733    /// detect when an interactive rebase is underway, and if the current
734    /// `post-rewrite` event is spurious.
735    ///
736    /// There are two practical issues for users as a result of this Git behavior:
737    ///
738    ///   * During an interactive rebase, we may see many "processing 1 rewritten
739    ///   commit" messages, and then a final "processing X rewritten commits" message
740    ///   once the rebase has concluded. This is potentially confusing for users, since
741    ///   the operation logically only rewrote the commits once, but we displayed the
742    ///   message multiple times.
743    ///
744    ///   * During an interactive rebase, we may warn about abandoned commits, when the
745    ///   next operation in the rebase plan fixes up the abandoned commit. This can
746    ///   happen even if no conflict occurred and the rebase completed successfully
747    ///   without any user intervention.
748    #[instrument]
749    pub fn is_rebase_underway(&self) -> Result<bool> {
750        use git2::RepositoryState::*;
751        match self.inner.state() {
752            Rebase | RebaseInteractive | RebaseMerge => Ok(true),
753
754            // Possibly some of these states should also be treated as `true`?
755            Clean | Merge | Revert | RevertSequence | CherryPick | CherryPickSequence | Bisect
756            | ApplyMailbox | ApplyMailboxOrRebase => Ok(false),
757        }
758    }
759
760    /// Get the type current multi-step operation (such as `rebase` or
761    /// `cherry-pick`) which is underway. Returns `None` if there is no such
762    /// operation.
763    pub fn get_current_operation_type(&self) -> Option<&str> {
764        use git2::RepositoryState::*;
765        match self.inner.state() {
766            Clean | Bisect => None,
767            Merge => Some("merge"),
768            Revert | RevertSequence => Some("revert"),
769            CherryPick | CherryPickSequence => Some("cherry-pick"),
770            Rebase | RebaseInteractive | RebaseMerge => Some("rebase"),
771            ApplyMailbox | ApplyMailboxOrRebase => Some("am"),
772        }
773    }
774
775    /// Find the merge-base between two commits. Returns `None` if a merge-base
776    /// could not be found.
777    #[instrument]
778    pub fn find_merge_base(&self, lhs: NonZeroOid, rhs: NonZeroOid) -> Result<Option<NonZeroOid>> {
779        match self.inner.merge_base(lhs.inner, rhs.inner) {
780            Ok(merge_base_oid) => Ok(Some(make_non_zero_oid(merge_base_oid))),
781            Err(err) if err.code() == git2::ErrorCode::NotFound => Ok(None),
782            Err(err) => Err(Error::FindMergeBase {
783                source: err,
784                lhs,
785                rhs,
786            }),
787        }
788    }
789
790    /// Get the patch for a commit, i.e. the diff between that commit and its
791    /// parent.
792    ///
793    /// If the commit has more than one parent, returns `None`.
794    #[instrument]
795    pub fn get_patch_for_commit(
796        &self,
797        effects: &Effects,
798        commit: &Commit,
799    ) -> Result<Option<Diff<'_>>> {
800        let changed_paths = self.get_paths_touched_by_commit(commit)?;
801        let dehydrated_commit = self.dehydrate_commit(
802            commit,
803            changed_paths
804                .iter()
805                .map(|x| x.as_path())
806                .collect_vec()
807                .as_slice(),
808            true,
809        )?;
810
811        let parent = dehydrated_commit.get_parents();
812        let parent_tree = match parent.as_slice() {
813            [] => None,
814            [parent] => Some(parent.get_tree()?),
815            [..] => return Ok(None),
816        };
817        let current_tree = dehydrated_commit.get_tree()?;
818        let diff = self.get_diff_between_trees(effects, parent_tree.as_ref(), &current_tree, 3)?;
819        Ok(Some(diff))
820    }
821
822    /// Get the diff between two trees. This is more performant than calling
823    /// libgit2's `diff_tree_to_tree` directly since it dehydrates commits
824    /// before diffing them.
825    #[instrument]
826    pub fn get_diff_between_trees(
827        &self,
828        effects: &Effects,
829        old_tree: Option<&Tree>,
830        new_tree: &Tree,
831        num_context_lines: usize,
832    ) -> Result<Diff<'_>> {
833        let (effects, _progress) = effects.start_operation(OperationType::CalculateDiff);
834        let _effects = effects;
835
836        let old_tree = old_tree.map(|tree| &tree.inner);
837        let new_tree = Some(&new_tree.inner);
838
839        let diff = self
840            .inner
841            .diff_tree_to_tree(
842                old_tree,
843                new_tree,
844                Some(DiffOptions::new().context_lines(num_context_lines.try_into().unwrap())),
845            )
846            .map_err(|err| Error::DiffTreeToTree {
847                source: err,
848                old_tree: old_tree
849                    .map(|tree| MaybeZeroOid::from(tree.id()))
850                    .unwrap_or(MaybeZeroOid::Zero),
851                new_tree: new_tree
852                    .map(|tree| MaybeZeroOid::from(tree.id()))
853                    .unwrap_or(MaybeZeroOid::Zero),
854            })?;
855        Ok(Diff { inner: diff })
856    }
857
858    /// Returns the set of paths currently staged to the repository's index.
859    #[instrument]
860    pub fn get_staged_paths(&self) -> Result<HashSet<PathBuf>> {
861        let head_commit_oid = match self.get_head_info()?.oid {
862            Some(oid) => oid,
863            None => return Err(Error::UnbornHead),
864        };
865        let head_commit = self.find_commit_or_fail(head_commit_oid)?;
866        let head_tree = self.find_tree_or_fail(head_commit.get_tree()?.get_oid())?;
867
868        let diff = self
869            .inner
870            .diff_tree_to_index(Some(&head_tree.inner), Some(&self.get_index()?.inner), None)
871            .map_err(|err| Error::DiffTreeToIndex {
872                source: err,
873                tree: head_tree.get_oid(),
874            })?;
875        let paths = diff
876            .deltas()
877            .flat_map(|delta| vec![delta.old_file().path(), delta.new_file().path()])
878            .flat_map(|p| p.map(PathBuf::from))
879            .collect();
880        Ok(paths)
881    }
882
883    /// Get the file paths which were added, removed, or changed by the given
884    /// commit.
885    ///
886    /// If the commit has no parents, returns all of the file paths in that
887    /// commit's tree.
888    ///
889    /// If the commit has more than one parent, returns all file paths changed
890    /// with respect to any parent.
891    #[instrument]
892    pub fn get_paths_touched_by_commit(&self, commit: &Commit) -> Result<HashSet<PathBuf>> {
893        let current_tree = commit.get_tree()?;
894        let parent_commits = commit.get_parents();
895        let changed_paths = if parent_commits.is_empty() {
896            get_changed_paths_between_trees(self, None, Some(&current_tree))
897                .map_err(Error::GetChangedPaths)?
898        } else {
899            let mut result: HashSet<PathBuf> = Default::default();
900            for parent_commit in parent_commits {
901                let parent_tree = parent_commit.get_tree()?;
902                let changed_paths =
903                    get_changed_paths_between_trees(self, Some(&parent_tree), Some(&current_tree))
904                        .map_err(Error::GetChangedPaths)?;
905                result.extend(changed_paths);
906            }
907            result
908        };
909        Ok(changed_paths)
910    }
911
912    /// Get the patch ID for this commit.
913    #[instrument]
914    pub fn get_patch_id(&self, effects: &Effects, commit: &Commit) -> Result<Option<PatchId>> {
915        let patch = match self.get_patch_for_commit(effects, commit)? {
916            None => return Ok(None),
917            Some(diff) => diff,
918        };
919        let patch_id = {
920            let (_effects, _progress) = effects.start_operation(OperationType::CalculatePatchId);
921            patch.inner.patchid(None).map_err(Error::GetPatchId)?
922        };
923        Ok(Some(PatchId { patch_id }))
924    }
925
926    /// Attempt to parse the user-provided object descriptor.
927    pub fn revparse_single_commit(&self, spec: &str) -> Result<Option<Commit<'_>>> {
928        if spec.ends_with('@') && spec.len() > 1 {
929            // Weird bug in `libgit2`; it seems that it treats a name like
930            // `foo-@` the same as `@`, and ignores the leading `foo`.
931            return Err(Error::UnsupportedRevParseSpec(spec.to_owned()));
932        }
933
934        // `libgit2` doesn't understand that `-` is short for `@{-1}`
935        let spec = if spec == "-" { "@{-1}" } else { spec };
936
937        match self.inner.revparse_single(spec) {
938            Ok(object) => match object.into_commit() {
939                Ok(commit) => Ok(Some(Commit { inner: commit })),
940                Err(_) => Ok(None),
941            },
942            Err(err) if err.code() == git2::ErrorCode::NotFound => Ok(None),
943            Err(err) => Err(Error::Git(err)),
944        }
945    }
946
947    /// Find all references in the repository.
948    #[instrument]
949    pub fn get_all_references(&self) -> Result<Vec<Reference<'_>>> {
950        let mut all_references = Vec::new();
951        for reference in self.inner.references().map_err(Error::GetReferences)? {
952            let reference = reference.map_err(Error::ReadReference)?;
953            all_references.push(Reference { inner: reference });
954        }
955        Ok(all_references)
956    }
957
958    /// Check if the repository has staged or unstaged changes. Untracked files
959    /// are not included. This operation may take a while.
960    #[instrument]
961    pub fn has_changed_files(&self, effects: &Effects, git_run_info: &GitRunInfo) -> Result<bool> {
962        match git_run_info
963            .run(
964                effects,
965                // This is not a mutating operation, so we don't need a transaction ID.
966                None,
967                &["diff", "--quiet"],
968            )
969            .map_err(Error::ExecGit)?
970        {
971            Ok(()) => Ok(false),
972            Err(_exit_code) => Ok(true),
973        }
974    }
975
976    /// Returns the current status of the repo index and working copy.
977    pub fn get_status(
978        &self,
979        effects: &Effects,
980        git_run_info: &GitRunInfo,
981        index: &Index,
982        head_info: &ResolvedReferenceInfo,
983        event_tx_id: Option<EventTransactionId>,
984    ) -> Result<(WorkingCopySnapshot<'_>, Vec<StatusEntry>)> {
985        let (effects, _progress) = effects.start_operation(OperationType::QueryWorkingCopy);
986        let _effects = effects;
987
988        let output = git_run_info
989            .run_silent(
990                self,
991                event_tx_id,
992                &["status", "--porcelain=v2", "--untracked-files=no", "-z"],
993                Default::default(),
994            )
995            .map_err(Error::ExecGit)?
996            .stdout;
997
998        let not_null_terminator = |c: &u8| *c != 0_u8;
999        let mut statuses = Vec::new();
1000        let mut status_bytes = output.into_iter().peekable();
1001
1002        // Iterate over the status entries in the output.
1003        // This takes some care, because NUL bytes are both used to delimit
1004        // between entries, and as a separator between paths in the case
1005        // of renames.
1006        // See https://git-scm.com/docs/git-status#_porcelain_format_version_2
1007        while let Some(line_prefix) = status_bytes.peek() {
1008            let line = match line_prefix {
1009                // Ordinary change entry or unmerged entry.
1010                b'1' | b'u' => {
1011                    let line = status_bytes
1012                        .by_ref()
1013                        .take_while(not_null_terminator)
1014                        .collect_vec();
1015                    line
1016                }
1017                // Rename or copy change entry.
1018                b'2' => {
1019                    let mut line = status_bytes
1020                        .by_ref()
1021                        .take_while(not_null_terminator)
1022                        .collect_vec();
1023                    line.push(0_u8); // Persist first null terminator in the line.
1024                    line.extend(status_bytes.by_ref().take_while(not_null_terminator));
1025                    line
1026                }
1027                // Skip header lines
1028                b'#' => continue,
1029                _ => {
1030                    return Err(Error::UnknownStatusLinePrefix {
1031                        prefix: *line_prefix,
1032                    });
1033                }
1034            };
1035            let entry: StatusEntry = line
1036                .as_slice()
1037                .try_into()
1038                .map_err(Error::ParseStatusEntry)?;
1039            statuses.push(entry);
1040        }
1041
1042        let snapshot = WorkingCopySnapshot::create(self, index, head_info, &statuses)
1043            .map_err(Error::CreateSnapshot)?;
1044        Ok((snapshot, statuses))
1045    }
1046
1047    /// Create a new branch or update an existing one. The provided name should
1048    /// be a branch name and not a reference name, i.e. it should not start with
1049    /// `refs/heads/`.
1050    #[instrument]
1051    pub fn create_branch(
1052        &self,
1053        branch_name: &str,
1054        commit: &Commit,
1055        force: bool,
1056    ) -> Result<Branch<'_>> {
1057        if branch_name.starts_with("refs/heads/") {
1058            warn!(
1059                ?branch_name,
1060                "Branch name starts with refs/heads/; this is probably not what you intended."
1061            );
1062        }
1063
1064        let branch = self
1065            .inner
1066            .branch(branch_name, &commit.inner, force)
1067            .map_err(|err| Error::CreateBranch {
1068                source: err,
1069                name: branch_name.to_owned(),
1070            })?;
1071        Ok(Branch {
1072            repo: self,
1073            inner: branch,
1074        })
1075    }
1076
1077    /// Create a new reference or update an existing one.
1078    #[instrument]
1079    pub fn create_reference(
1080        &self,
1081        name: &ReferenceName,
1082        oid: NonZeroOid,
1083        force: bool,
1084        log_message: &str,
1085    ) -> Result<Reference<'_>> {
1086        let reference = self
1087            .inner
1088            .reference(name.as_str(), oid.inner, force, log_message)
1089            .map_err(Error::CreateReference)?;
1090        Ok(Reference { inner: reference })
1091    }
1092
1093    /// Get a list of all remote names.
1094    #[instrument]
1095    pub fn get_all_remote_names(&self) -> Result<Vec<String>> {
1096        let remotes = self.inner.remotes().map_err(Error::GetRemoteNames)?;
1097        Ok(remotes
1098            .into_iter()
1099            .enumerate()
1100            .filter_map(|(i, remote_name)| match remote_name {
1101                Some(remote_name) => Some(remote_name.to_owned()),
1102                None => {
1103                    warn!(remote_index = i, "Remote name could not be decoded");
1104                    None
1105                }
1106            })
1107            .sorted()
1108            .collect())
1109    }
1110
1111    /// Look up a reference with the given name. Returns `None` if not found.
1112    #[instrument]
1113    pub fn find_reference(&self, name: &ReferenceName) -> Result<Option<Reference<'_>>> {
1114        match self.inner.find_reference(name.as_str()) {
1115            Ok(reference) => Ok(Some(Reference { inner: reference })),
1116            Err(err) if err.code() == git2::ErrorCode::NotFound => Ok(None),
1117            Err(err) => Err(Error::FindReference {
1118                source: err,
1119                name: name.clone(),
1120            }),
1121        }
1122    }
1123
1124    /// Get all local branches in the repository.
1125    #[instrument]
1126    pub fn get_all_local_branches(&self) -> Result<Vec<Branch<'_>>> {
1127        let mut all_branches = Vec::new();
1128        for branch in self
1129            .inner
1130            .branches(Some(git2::BranchType::Local))
1131            .map_err(Error::GetBranches)?
1132        {
1133            let (branch, _branch_type) = branch.map_err(Error::ReadBranch)?;
1134            all_branches.push(Branch {
1135                repo: self,
1136                inner: branch,
1137            });
1138        }
1139        Ok(all_branches)
1140    }
1141
1142    /// Look up the branch with the given name. Returns `None` if not found.
1143    #[instrument]
1144    pub fn find_branch(&self, name: &str, branch_type: BranchType) -> Result<Option<Branch<'_>>> {
1145        match self.inner.find_branch(name, branch_type) {
1146            Ok(branch) => Ok(Some(Branch {
1147                repo: self,
1148                inner: branch,
1149            })),
1150            Err(err) if err.code() == git2::ErrorCode::NotFound => Ok(None),
1151            Err(err) => Err(Error::FindBranch {
1152                source: err,
1153                name: name.to_owned(),
1154            }),
1155        }
1156    }
1157
1158    /// Look up a commit with the given OID. Returns `None` if not found.
1159    #[instrument]
1160    pub fn find_commit(&self, oid: NonZeroOid) -> Result<Option<Commit<'_>>> {
1161        match self.inner.find_commit(oid.inner) {
1162            Ok(commit) => Ok(Some(Commit { inner: commit })),
1163            Err(err) if err.code() == git2::ErrorCode::NotFound => Ok(None),
1164            Err(err) => Err(Error::FindCommit { source: err, oid }),
1165        }
1166    }
1167
1168    /// Like `find_commit`, but raises a generic error if the commit could not
1169    /// be found.
1170    #[instrument]
1171    pub fn find_commit_or_fail(&self, oid: NonZeroOid) -> Result<Commit<'_>> {
1172        match self.inner.find_commit(oid.inner) {
1173            Ok(commit) => Ok(Commit { inner: commit }),
1174            Err(err) => Err(Error::FindCommit { source: err, oid }),
1175        }
1176    }
1177
1178    /// Look up a blob with the given OID. Returns `None` if not found.
1179    #[instrument]
1180    pub fn find_blob(&self, oid: NonZeroOid) -> Result<Option<Blob<'_>>> {
1181        match self.inner.find_blob(oid.inner) {
1182            Ok(blob) => Ok(Some(Blob { inner: blob })),
1183            Err(err) if err.code() == git2::ErrorCode::NotFound => Ok(None),
1184            Err(err) => Err(Error::FindBlob { source: err, oid }),
1185        }
1186    }
1187
1188    /// Like `find_blob`, but raises a generic error if the blob could not be
1189    /// found.
1190    #[instrument]
1191    pub fn find_blob_or_fail(&self, oid: NonZeroOid) -> Result<Blob<'_>> {
1192        match self.inner.find_blob(oid.inner) {
1193            Ok(blob) => Ok(Blob { inner: blob }),
1194            Err(err) => Err(Error::FindBlob { source: err, oid }),
1195        }
1196    }
1197
1198    /// Look up the commit with the given OID and render a friendly description
1199    /// of it, or render an error message if not found.
1200    pub fn friendly_describe_commit_from_oid(
1201        &self,
1202        glyphs: &Glyphs,
1203        oid: NonZeroOid,
1204    ) -> Result<StyledString> {
1205        match self.find_commit(oid)? {
1206            Some(commit) => Ok(commit.friendly_describe(glyphs)?),
1207            None => {
1208                let NonZeroOid { inner: oid } = oid;
1209                Ok(StyledString::styled(
1210                    format!("<commit not available: {oid}>"),
1211                    BaseColor::Red.light(),
1212                ))
1213            }
1214        }
1215    }
1216
1217    /// Read a file from disk and create a blob corresponding to its contents.
1218    /// If the file doesn't exist on disk, returns `None` instead.
1219    #[instrument]
1220    pub fn create_blob_from_path(&self, path: &Path) -> Result<Option<NonZeroOid>> {
1221        // Can't use `self.inner.blob_path`, because it will read the file from
1222        // the main repository instead of from the current worktree.
1223        let path = self
1224            .get_working_copy_path()
1225            .ok_or_else(|| Error::CreateBlobFromPath {
1226                source: eyre::eyre!(
1227                    "Repository at {:?} has no working copy path (is bare)",
1228                    self.get_path()
1229                ),
1230                path: path.to_path_buf(),
1231            })?
1232            .join(path);
1233        let contents = match std::fs::read(&path) {
1234            Ok(contents) => contents,
1235            Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
1236            Err(err) => {
1237                return Err(Error::CreateBlobFromPath {
1238                    source: err.into(),
1239                    path,
1240                });
1241            }
1242        };
1243        let blob = self.create_blob_from_contents(&contents)?;
1244        Ok(Some(blob))
1245    }
1246
1247    /// Read a symlink and create a blob corresponding to its target path.
1248    /// If the symlink doesn't exist on disk, returns `None` instead.
1249    #[instrument]
1250    pub fn create_blob_from_symlink_path(&self, path: &Path) -> Result<Option<NonZeroOid>> {
1251        let path = self
1252            .get_working_copy_path()
1253            .ok_or_else(|| Error::CreateBlobFromPath {
1254                source: eyre::eyre!(
1255                    "Repository at {:?} has no working copy path (is bare)",
1256                    self.get_path()
1257                ),
1258                path: path.to_path_buf(),
1259            })?
1260            .join(path);
1261        let link_target = match std::fs::read_link(&path) {
1262            Ok(link_target) => link_target,
1263            Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
1264            Err(err) => {
1265                return Err(Error::CreateBlobFromPath {
1266                    source: err.into(),
1267                    path,
1268                });
1269            }
1270        };
1271
1272        #[cfg(unix)]
1273        let link_target_bytes = link_target.as_os_str().as_bytes().to_vec();
1274        #[cfg(not(unix))]
1275        let link_target_bytes = link_target.to_string_lossy().into_owned().into_bytes();
1276
1277        let blob_oid = self.create_blob_from_contents(&link_target_bytes)?;
1278        Ok(Some(blob_oid))
1279    }
1280
1281    /// Create an object for the given path in the working copy according to its file mode.
1282    #[instrument]
1283    pub fn create_blob_from_path_for_mode(
1284        &self,
1285        path: &Path,
1286        file_mode: FileMode,
1287        index: &Index,
1288    ) -> Result<Option<NonZeroOid>> {
1289        match file_mode {
1290            FileMode::Blob | FileMode::BlobExecutable | FileMode::BlobGroupWritable => {
1291                self.create_blob_from_path(path)
1292            }
1293            FileMode::Link => self.create_blob_from_symlink_path(path),
1294            FileMode::Commit => match index.get_entry(path) {
1295                Some(IndexEntry {
1296                    oid: MaybeZeroOid::NonZero(oid),
1297                    ..
1298                }) => Ok(Some(oid)),
1299                Some(IndexEntry {
1300                    oid: MaybeZeroOid::Zero,
1301                    ..
1302                })
1303                | None => Ok(None),
1304            },
1305            FileMode::Tree | FileMode::Unreadable => Ok(None),
1306        }
1307    }
1308
1309    /// Create a blob corresponding to the provided byte slice.
1310    #[instrument]
1311    pub fn create_blob_from_contents(&self, contents: &[u8]) -> Result<NonZeroOid> {
1312        let oid = self.inner.blob(contents).map_err(Error::CreateBlob)?;
1313        Ok(make_non_zero_oid(oid))
1314    }
1315
1316    /// Create a new commit.
1317    #[instrument]
1318    pub fn create_commit(
1319        &self,
1320        update_ref: Option<&str>,
1321        author: &Signature,
1322        committer: &Signature,
1323        message: &str,
1324        tree: &Tree,
1325        parents: Vec<&Commit>,
1326    ) -> Result<NonZeroOid> {
1327        let parents = parents
1328            .iter()
1329            .map(|commit| &commit.inner)
1330            .collect::<Vec<_>>();
1331        let oid = self
1332            .inner
1333            .commit(
1334                update_ref,
1335                &author.inner,
1336                &committer.inner,
1337                message,
1338                &tree.inner,
1339                parents.as_slice(),
1340            )
1341            .map_err(Error::CreateCommit)?;
1342        Ok(make_non_zero_oid(oid))
1343    }
1344
1345    /// Cherry-pick a commit in memory and return the resulting index.
1346    #[instrument]
1347    pub fn cherry_pick_commit(
1348        &self,
1349        cherry_pick_commit: &Commit,
1350        our_commit: &Commit,
1351        mainline: u32,
1352    ) -> Result<Index> {
1353        let index = self
1354            .inner
1355            .cherrypick_commit(&cherry_pick_commit.inner, &our_commit.inner, mainline, None)
1356            .map_err(|err| Error::CherryPickCommit {
1357                source: err,
1358                commit: cherry_pick_commit.get_oid(),
1359                onto: our_commit.get_oid(),
1360            })?;
1361        Ok(Index { inner: index })
1362    }
1363
1364    /// Cherry-pick a commit in memory and return the resulting tree.
1365    ///
1366    /// The `libgit2` routines operate on entire `Index`es, which contain one
1367    /// entry per file in the repository. When operating on a large repository,
1368    /// this is prohibitively slow, as it takes several seconds just to write
1369    /// the index to disk. To improve performance, we reduce the size of the
1370    /// involved indexes by filtering out any unchanged entries from the input
1371    /// trees, then call into `libgit2`, then add back the unchanged entries to
1372    /// the output tree.
1373    #[instrument]
1374    pub fn cherry_pick_fast<'repo>(
1375        &'repo self,
1376        patch_commit: &'repo Commit,
1377        target_commit: &'repo Commit,
1378        options: &CherryPickFastOptions,
1379    ) -> std::result::Result<Tree<'repo>, CreateCommitFastError> {
1380        let CherryPickFastOptions {
1381            reuse_parent_tree_if_possible,
1382        } = options;
1383
1384        if *reuse_parent_tree_if_possible {
1385            if let Some(only_parent) = patch_commit.get_only_parent() {
1386                if only_parent.get_tree_oid() == target_commit.get_tree_oid() {
1387                    // If this patch is being applied to the same commit it was
1388                    // originally based on, then we can skip cherry-picking
1389                    // altogether, and use its tree directly. This is common e.g.
1390                    // when only rewording a commit message.
1391                    return Ok(patch_commit.get_tree()?);
1392                }
1393            };
1394        }
1395
1396        let changed_pathbufs = self
1397            .get_paths_touched_by_commit(patch_commit)?
1398            .into_iter()
1399            .collect_vec();
1400        let changed_paths = changed_pathbufs.iter().map(PathBuf::borrow).collect_vec();
1401
1402        let dehydrated_patch_commit =
1403            self.dehydrate_commit(patch_commit, changed_paths.as_slice(), true)?;
1404        let dehydrated_target_commit =
1405            self.dehydrate_commit(target_commit, changed_paths.as_slice(), false)?;
1406
1407        let rebased_index =
1408            self.cherry_pick_commit(&dehydrated_patch_commit, &dehydrated_target_commit, 0)?;
1409        let rebased_tree = {
1410            if rebased_index.has_conflicts() {
1411                let conflicting_paths = {
1412                    let mut result = HashSet::new();
1413                    for conflict in rebased_index.inner.conflicts().map_err(|err| {
1414                        CreateCommitFastError::GetConflicts {
1415                            source: err,
1416                            commit: patch_commit.get_oid(),
1417                            onto: target_commit.get_oid(),
1418                        }
1419                    })? {
1420                        let conflict =
1421                            conflict.map_err(|err| CreateCommitFastError::GetConflicts {
1422                                source: err,
1423                                commit: patch_commit.get_oid(),
1424                                onto: target_commit.get_oid(),
1425                            })?;
1426                        if let Some(ancestor) = conflict.ancestor {
1427                            result.insert(ancestor.path.into_path_buf().map_err(|err| {
1428                                CreateCommitFastError::DecodePath {
1429                                    source: err,
1430                                    item: "ancestor",
1431                                }
1432                            })?);
1433                        }
1434                        if let Some(our) = conflict.our {
1435                            result.insert(our.path.into_path_buf().map_err(|err| {
1436                                CreateCommitFastError::DecodePath {
1437                                    source: err,
1438                                    item: "our",
1439                                }
1440                            })?);
1441                        }
1442                        if let Some(their) = conflict.their {
1443                            result.insert(their.path.into_path_buf().map_err(|err| {
1444                                CreateCommitFastError::DecodePath {
1445                                    source: err,
1446                                    item: "their",
1447                                }
1448                            })?);
1449                        }
1450                    }
1451                    result
1452                };
1453
1454                if conflicting_paths.is_empty() {
1455                    warn!(
1456                        "BUG: A merge conflict was detected, but there were no entries in `conflicting_paths`. Maybe the wrong index entry was used?"
1457                    )
1458                }
1459
1460                return Err(CreateCommitFastError::MergeConflict { conflicting_paths });
1461            }
1462            let rebased_entries: HashMap<PathBuf, Option<(NonZeroOid, FileMode)>> =
1463                changed_pathbufs
1464                    .into_iter()
1465                    .map(|changed_path| {
1466                        let value = match rebased_index.get_entry(&changed_path) {
1467                            Some(IndexEntry {
1468                                oid: MaybeZeroOid::Zero,
1469                                file_mode: _,
1470                            }) => {
1471                                warn!(
1472                                    ?patch_commit,
1473                                    ?changed_path,
1474                                    "BUG: index entry was zero. \
1475                                This probably indicates that a removed path \
1476                                was not handled correctly."
1477                                );
1478                                None
1479                            }
1480                            Some(IndexEntry {
1481                                oid: MaybeZeroOid::NonZero(oid),
1482                                file_mode,
1483                            }) => Some((oid, file_mode)),
1484                            None => None,
1485                        };
1486                        (changed_path, value)
1487                    })
1488                    .collect();
1489            let rebased_tree_oid =
1490                hydrate_tree(self, Some(&target_commit.get_tree()?), rebased_entries)
1491                    .map_err(CreateCommitFastError::HydrateTree)?;
1492            self.find_tree_or_fail(rebased_tree_oid)?
1493        };
1494        Ok(rebased_tree)
1495    }
1496
1497    #[instrument]
1498    fn dehydrate_commit(
1499        &self,
1500        commit: &Commit,
1501        changed_paths: &[&Path],
1502        base_on_parent: bool,
1503    ) -> Result<Commit<'_>> {
1504        let tree = commit.get_tree()?;
1505        let dehydrated_tree_oid =
1506            dehydrate_tree(self, &tree, changed_paths).map_err(Error::DehydrateTree)?;
1507        let dehydrated_tree = self.find_tree_or_fail(dehydrated_tree_oid)?;
1508
1509        let signature = Signature::automated()?;
1510        let message = format!(
1511            "generated by git-branchless: temporary dehydrated commit \
1512                \
1513                This commit was originally: {:?}",
1514            commit.get_oid()
1515        );
1516
1517        let parents = if base_on_parent {
1518            match commit.get_only_parent() {
1519                Some(parent) => {
1520                    let dehydrated_parent = self.dehydrate_commit(&parent, changed_paths, false)?;
1521                    vec![dehydrated_parent]
1522                }
1523                None => vec![],
1524            }
1525        } else {
1526            vec![]
1527        };
1528        let dehydrated_commit_oid = self.create_commit(
1529            None,
1530            &signature,
1531            &signature,
1532            &message,
1533            &dehydrated_tree,
1534            parents.iter().collect_vec(),
1535        )?;
1536        let dehydrated_commit = self.find_commit_or_fail(dehydrated_commit_oid)?;
1537        Ok(dehydrated_commit)
1538    }
1539
1540    /// Look up the tree with the given OID. Returns `None` if not found.
1541    #[instrument]
1542    pub fn find_tree(&self, oid: NonZeroOid) -> Result<Option<Tree<'_>>> {
1543        match self.inner.find_tree(oid.inner) {
1544            Ok(tree) => Ok(Some(Tree { inner: tree })),
1545            Err(err) if err.code() == git2::ErrorCode::NotFound => Ok(None),
1546            Err(err) => Err(Error::FindTree {
1547                source: err,
1548                oid: oid.into(),
1549            }),
1550        }
1551    }
1552
1553    /// Like `find_tree`, but raises a generic error if the commit could not
1554    /// be found.
1555    #[instrument]
1556    pub fn find_tree_or_fail(&self, oid: NonZeroOid) -> Result<Tree<'_>> {
1557        match self.inner.find_tree(oid.inner) {
1558            Ok(tree) => Ok(Tree { inner: tree }),
1559            Err(err) => Err(Error::FindTree {
1560                source: err,
1561                oid: oid.into(),
1562            }),
1563        }
1564    }
1565
1566    /// Write the provided in-memory index as a tree into Git`s object database.
1567    /// There must be no merge conflicts in the index.
1568    #[instrument]
1569    pub fn write_index_to_tree(&self, index: &mut Index) -> Result<NonZeroOid> {
1570        let oid = index
1571            .inner
1572            .write_tree_to(&self.inner)
1573            .map_err(Error::WriteIndexToTree)?;
1574        Ok(make_non_zero_oid(oid))
1575    }
1576
1577    /// Amends the provided parent commit in memory and returns the resulting tree.
1578    ///
1579    /// Only amends the files provided in the options, and only supports amending from
1580    /// either the working tree or the index, but not both.
1581    ///
1582    /// See `Repo::cherry_pick_fast` for motivation for performing the operation
1583    /// in-memory.
1584    #[instrument]
1585    pub fn amend_fast(
1586        &self,
1587        parent_commit: &Commit,
1588        opts: &AmendFastOptions,
1589    ) -> std::result::Result<Tree<'_>, CreateCommitFastError> {
1590        let changed_paths: Vec<PathBuf> = {
1591            let mut result = self.get_paths_touched_by_commit(parent_commit)?;
1592            match opts {
1593                AmendFastOptions::FromIndex { paths } => result.extend(paths.iter().cloned()),
1594                AmendFastOptions::FromWorkingCopy { status_entries } => {
1595                    for entry in status_entries {
1596                        result.extend(entry.paths().iter().cloned());
1597                    }
1598                }
1599                AmendFastOptions::FromCommit { commit } => {
1600                    result.extend(self.get_paths_touched_by_commit(commit)?);
1601                }
1602            };
1603            result.into_iter().collect_vec()
1604        };
1605        let changed_paths = changed_paths
1606            .iter()
1607            .map(|path| path.as_path())
1608            .collect_vec();
1609
1610        let dehydrated_parent =
1611            self.dehydrate_commit(parent_commit, changed_paths.as_slice(), true)?;
1612        let dehydrated_parent_tree = dehydrated_parent.get_tree()?;
1613
1614        let index = self.get_index()?;
1615        let index = &index;
1616        let new_tree_entries: HashMap<PathBuf, Option<(NonZeroOid, FileMode)>> = match opts {
1617            AmendFastOptions::FromWorkingCopy { status_entries } => status_entries
1618                .iter()
1619                .flat_map(|entry| {
1620                    let file_mode = entry.working_copy_file_mode;
1621                    entry.paths().into_iter().map(
1622                        move |path| -> Result<(PathBuf, Option<(NonZeroOid, FileMode)>)> {
1623                            let entry = self
1624                                .create_blob_from_path_for_mode(&path, file_mode, index)?
1625                                .map(|oid| (oid, file_mode));
1626                            Ok((path, entry))
1627                        },
1628                    )
1629                })
1630                .collect::<Result<HashMap<_, _>>>()?,
1631            AmendFastOptions::FromIndex { paths } => paths
1632                .iter()
1633                .filter_map(|path| match index.get_entry(path) {
1634                    Some(IndexEntry {
1635                        oid: MaybeZeroOid::Zero,
1636                        ..
1637                    }) => {
1638                        warn!(?path, "index entry was zero");
1639                        None
1640                    }
1641                    Some(IndexEntry {
1642                        oid: MaybeZeroOid::NonZero(oid),
1643                        file_mode,
1644                        ..
1645                    }) => Some((path.clone(), Some((oid, file_mode)))),
1646                    None => Some((path.clone(), None)),
1647                })
1648                .collect::<HashMap<_, _>>(),
1649            AmendFastOptions::FromCommit { commit } => {
1650                let amended_tree = self.cherry_pick_fast(
1651                    commit,
1652                    parent_commit,
1653                    &CherryPickFastOptions {
1654                        reuse_parent_tree_if_possible: false,
1655                    },
1656                )?;
1657                self.get_paths_touched_by_commit(commit)?
1658                    .iter()
1659                    .filter_map(|path| match amended_tree.get_path(path) {
1660                        Ok(Some(entry)) => {
1661                            Some((path.clone(), Some((entry.get_oid(), entry.get_filemode()))))
1662                        }
1663                        Ok(None) | Err(_) => None,
1664                    })
1665                    .collect::<HashMap<_, _>>()
1666            }
1667        };
1668
1669        // Merge the new path entries into the existing set of parent tree.
1670        let amended_tree_entries: HashMap<PathBuf, Option<(NonZeroOid, FileMode)>> = changed_paths
1671            .into_iter()
1672            .map(|changed_path| {
1673                let value = match new_tree_entries.get(changed_path) {
1674                    Some(new_tree_entry) => new_tree_entry.as_ref().copied(),
1675                    None => match dehydrated_parent_tree.get_path(changed_path) {
1676                        Ok(Some(entry)) => Some((entry.get_oid(), entry.get_filemode())),
1677                        Ok(None) => None,
1678                        Err(err) => return Err(Error::ReadTree(err)),
1679                    },
1680                };
1681                Ok((changed_path.into(), value))
1682            })
1683            .collect::<Result<_>>()?;
1684
1685        let amended_tree_oid =
1686            hydrate_tree(self, Some(&parent_commit.get_tree()?), amended_tree_entries)
1687                .map_err(Error::HydrateTree)?;
1688        let amended_tree = self.find_tree_or_fail(amended_tree_oid)?;
1689
1690        Ok(amended_tree)
1691    }
1692}
1693
1694/// The signature of a commit, identifying who it was made by and when it was made.
1695pub struct Signature<'repo> {
1696    pub(super) inner: git2::Signature<'repo>,
1697}
1698
1699impl std::fmt::Debug for Signature<'_> {
1700    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1701        write!(f, "<Signature>")
1702    }
1703}
1704
1705impl<'repo> Signature<'repo> {
1706    #[instrument]
1707    pub fn automated() -> Result<Self> {
1708        Ok(Signature {
1709            inner: git2::Signature::new(
1710                "git-branchless",
1711                "git-branchless@example.com",
1712                &git2::Time::new(0, 0),
1713            )
1714            .map_err(Error::CreateSignature)?,
1715        })
1716    }
1717
1718    /// Update the timestamp of this signature to a new time.
1719    #[instrument]
1720    pub fn update_timestamp(self, now: SystemTime) -> Result<Signature<'repo>> {
1721        let seconds: i64 = now
1722            .duration_since(SystemTime::UNIX_EPOCH)
1723            .map_err(Error::SystemTime)?
1724            .as_secs()
1725            .try_into()
1726            .map_err(Error::IntegerConvert)?;
1727        let time = git2::Time::new(seconds, self.inner.when().offset_minutes());
1728        let name = match self.inner.name() {
1729            Some(name) => name,
1730            None => {
1731                return Err(Error::DecodeUtf8 {
1732                    item: "signature name",
1733                });
1734            }
1735        };
1736        let email = match self.inner.email() {
1737            Some(email) => email,
1738            None => {
1739                return Err(Error::DecodeUtf8 {
1740                    item: "signature email",
1741                });
1742            }
1743        };
1744        let signature = git2::Signature::new(name, email, &time).map_err(Error::CreateSignature)?;
1745        Ok(Signature { inner: signature })
1746    }
1747
1748    /// Get the time when this signature was applied.
1749    pub fn get_time(&self) -> Time {
1750        Time {
1751            inner: self.inner.when(),
1752        }
1753    }
1754
1755    pub fn get_name(&self) -> Option<&str> {
1756        self.inner.name()
1757    }
1758
1759    pub fn get_email(&self) -> Option<&str> {
1760        self.inner.email()
1761    }
1762
1763    /// Return the friendly formatted name and email of the signature.
1764    pub fn friendly_describe(&self) -> Option<String> {
1765        let name = self.inner.name();
1766        let email = self.inner.email().map(|email| format!("<{email}>"));
1767        match (name, email) {
1768            (Some(name), Some(email)) => Some(format!("{name} {email}")),
1769            (Some(name), _) => Some(name.into()),
1770            (_, Some(email)) => Some(email),
1771            _ => None,
1772        }
1773    }
1774}
1775
1776/// A checksum of the diff induced by a given commit, used for duplicate commit
1777/// detection.
1778#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1779pub struct PatchId {
1780    patch_id: git2::Oid,
1781}
1782
1783/// A timestamp as used in a [`git2::Signature`].
1784#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1785pub struct Time {
1786    pub(super) inner: git2::Time,
1787}
1788
1789impl Time {
1790    /// Calculate the associated [`SystemTime`].
1791    pub fn to_system_time(&self) -> Result<SystemTime> {
1792        Ok(SystemTime::UNIX_EPOCH.add(Duration::from_secs(
1793            self.inner
1794                .seconds()
1795                .try_into()
1796                .map_err(Error::IntegerConvert)?,
1797        )))
1798    }
1799
1800    /// Calculate the associated [`DateTime`].
1801    pub fn to_date_time(&self) -> Option<DateTime<Utc>> {
1802        DateTime::from_timestamp(self.inner.seconds(), 0)
1803    }
1804}