1use 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
284pub type Result<T> = std::result::Result<T, Error>;
286
287pub use git2::ErrorCode as GitErrorCode;
288
289pub(super) fn wrap_git_error(error: git2::Error) -> eyre::Error {
291 eyre::eyre!("Git error {:?}: {}", error.code(), error.message())
292}
293
294#[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#[derive(Debug, PartialEq, Eq)]
331pub struct ResolvedReferenceInfo {
332 pub oid: Option<NonZeroOid>,
335
336 pub reference_name: Option<ReferenceName>,
339}
340
341impl ResolvedReferenceInfo {
342 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#[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 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#[derive(Clone, Debug)]
393pub struct CherryPickFastOptions {
394 pub reuse_parent_tree_if_possible: bool,
397}
398
399#[allow(missing_docs)]
402#[derive(Debug, Error)]
403pub enum CreateCommitFastError {
404 #[error("merge conflict in {} paths", conflicting_paths.len())]
406 MergeConflict {
407 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#[derive(Debug)]
436pub enum AmendFastOptions<'repo> {
437 FromWorkingCopy {
439 status_entries: Vec<StatusEntry>,
441 },
442 FromIndex {
444 paths: Vec<PathBuf>,
446 },
447 FromCommit {
449 commit: Commit<'repo>,
451 },
452}
453
454impl AmendFastOptions<'_> {
455 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
465pub 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 #[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 #[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 #[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 pub fn get_path(&self) -> &Path {
512 self.inner.path()
513 }
514
515 pub fn get_packed_refs_path(&self) -> PathBuf {
517 self.inner.path().join("packed-refs")
518 }
519
520 pub fn get_rebase_state_dir_path(&self) -> PathBuf {
523 self.inner.path().join("rebase-merge")
524 }
525
526 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 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()?; std::fs::canonicalize(workdir).ok().or_else(|| {
561 warn!(?workdir, "Failed to canonicalize workdir");
562 None
563 })
564 }
565
566 pub fn get_index(&self) -> Result<Index> {
568 let mut index = self.inner.index().map_err(Error::ReadIndex)?;
569 index.read(false).map_err(Error::ReadIndex)?;
571 Ok(Index { inner: index })
572 }
573
574 #[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 #[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 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 #[instrument]
615 pub fn get_config_path(&self) -> Result<PathBuf> {
616 Ok(self.get_branchless_dir()?.join("config"))
617 }
618
619 #[instrument]
621 pub fn get_dag_dir(&self) -> Result<PathBuf> {
622 Ok(self.get_branchless_dir()?.join("dag2"))
625 }
626
627 #[instrument]
631 pub fn get_man_dir(&self) -> Result<PathBuf> {
632 Ok(self.get_branchless_dir()?.join("man"))
633 }
634
635 #[instrument]
642 pub fn get_tempfile_dir(&self) -> Result<PathBuf> {
643 Ok(self.get_branchless_dir()?.join("tmp"))
644 }
645
646 #[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 #[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 #[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 #[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 #[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 #[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 #[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 Clean | Merge | Revert | RevertSequence | CherryPick | CherryPickSequence | Bisect
756 | ApplyMailbox | ApplyMailboxOrRebase => Ok(false),
757 }
758 }
759
760 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 #[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 #[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(), ¤t_tree, 3)?;
819 Ok(Some(diff))
820 }
821
822 #[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 #[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 #[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(¤t_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(¤t_tree))
904 .map_err(Error::GetChangedPaths)?;
905 result.extend(changed_paths);
906 }
907 result
908 };
909 Ok(changed_paths)
910 }
911
912 #[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 pub fn revparse_single_commit(&self, spec: &str) -> Result<Option<Commit<'_>>> {
928 if spec.ends_with('@') && spec.len() > 1 {
929 return Err(Error::UnsupportedRevParseSpec(spec.to_owned()));
932 }
933
934 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 #[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 #[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 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 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 while let Some(line_prefix) = status_bytes.peek() {
1008 let line = match line_prefix {
1009 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 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); line.extend(status_bytes.by_ref().take_while(not_null_terminator));
1025 line
1026 }
1027 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[instrument]
1220 pub fn create_blob_from_path(&self, path: &Path) -> Result<Option<NonZeroOid>> {
1221 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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 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
1694pub 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 #[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 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1779pub struct PatchId {
1780 patch_id: git2::Oid,
1781}
1782
1783#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1785pub struct Time {
1786 pub(super) inner: git2::Time,
1787}
1788
1789impl Time {
1790 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 pub fn to_date_time(&self) -> Option<DateTime<Utc>> {
1802 DateTime::from_timestamp(self.inner.seconds(), 0)
1803 }
1804}