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