1use 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
282pub type Result<T> = std::result::Result<T, Error>;
284
285pub use git2::ErrorCode as GitErrorCode;
286
287pub(super) fn wrap_git_error(error: git2::Error) -> eyre::Error {
289 eyre::eyre!("Git error {:?}: {}", error.code(), error.message())
290}
291
292#[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#[derive(Debug, PartialEq, Eq)]
329pub struct ResolvedReferenceInfo {
330 pub oid: Option<NonZeroOid>,
333
334 pub reference_name: Option<ReferenceName>,
337}
338
339impl ResolvedReferenceInfo {
340 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#[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 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#[derive(Clone, Debug)]
391pub struct CherryPickFastOptions {
392 pub reuse_parent_tree_if_possible: bool,
395}
396
397#[allow(missing_docs)]
400#[derive(Debug, Error)]
401pub enum CreateCommitFastError {
402 #[error("merge conflict in {} paths", conflicting_paths.len())]
404 MergeConflict {
405 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#[derive(Debug)]
434pub enum AmendFastOptions<'repo> {
435 FromWorkingCopy {
437 status_entries: Vec<StatusEntry>,
439 },
440 FromIndex {
442 paths: Vec<PathBuf>,
444 },
445 FromCommit {
447 commit: Commit<'repo>,
449 },
450}
451
452impl<'repo> AmendFastOptions<'repo> {
453 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
463pub 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 #[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 #[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 #[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 pub fn get_path(&self) -> &Path {
510 self.inner.path()
511 }
512
513 pub fn get_packed_refs_path(&self) -> PathBuf {
515 self.inner.path().join("packed-refs")
516 }
517
518 pub fn get_rebase_state_dir_path(&self) -> PathBuf {
521 self.inner.path().join("rebase-merge")
522 }
523
524 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 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()?; std::fs::canonicalize(workdir).ok().or_else(|| {
559 warn!(?workdir, "Failed to canonicalize workdir");
560 None
561 })
562 }
563
564 pub fn get_index(&self) -> Result<Index> {
566 let mut index = self.inner.index().map_err(Error::ReadIndex)?;
567 index.read(false).map_err(Error::ReadIndex)?;
569 Ok(Index { inner: index })
570 }
571
572 #[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 let worktree_info_dir = self.get_path();
583 let parent_repo_path = match worktree_info_dir
584 .parent() .and_then(|path| path.parent()) .and_then(|path| path.parent()) {
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 #[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 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 #[instrument]
626 pub fn get_config_path(&self) -> Result<PathBuf> {
627 Ok(self.get_branchless_dir()?.join("config"))
628 }
629
630 #[instrument]
632 pub fn get_dag_dir(&self) -> Result<PathBuf> {
633 Ok(self.get_branchless_dir()?.join("dag2"))
636 }
637
638 #[instrument]
642 pub fn get_man_dir(&self) -> Result<PathBuf> {
643 Ok(self.get_branchless_dir()?.join("man"))
644 }
645
646 #[instrument]
653 pub fn get_tempfile_dir(&self) -> Result<PathBuf> {
654 Ok(self.get_branchless_dir()?.join("tmp"))
655 }
656
657 #[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 #[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 #[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 #[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 #[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 #[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 #[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 Clean | Merge | Revert | RevertSequence | CherryPick | CherryPickSequence | Bisect
767 | ApplyMailbox | ApplyMailboxOrRebase => Ok(false),
768 }
769 }
770
771 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 #[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 #[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(), ¤t_tree, 3)?;
826 Ok(Some(diff))
827 }
828
829 #[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 #[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 #[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(¤t_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(¤t_tree))
911 .map_err(Error::GetChangedPaths)?;
912 result.extend(changed_paths);
913 }
914 result
915 };
916 Ok(changed_paths)
917 }
918
919 #[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 pub fn revparse_single_commit(&self, spec: &str) -> Result<Option<Commit>> {
935 if spec.ends_with('@') && spec.len() > 1 {
936 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 #[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 #[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 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 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 while let Some(line_prefix) = status_bytes.peek() {
1012 let line = match line_prefix {
1013 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 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); 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[instrument]
1217 pub fn create_blob_from_path(&self, path: &Path) -> Result<Option<NonZeroOid>> {
1218 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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 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 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
1634pub 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 #[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 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1719pub struct PatchId {
1720 patch_id: git2::Oid,
1721}
1722
1723#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1725pub struct Time {
1726 pub(super) inner: git2::Time,
1727}
1728
1729impl Time {
1730 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 pub fn to_naive_date_time(&self) -> Option<NaiveDateTime> {
1742 NaiveDateTime::from_timestamp_opt(self.inner.seconds(), 0)
1743 }
1744}