Skip to main content

radicle/
git.rs

1pub mod canonical;
2pub mod raw;
3
4use std::io;
5use std::path::Path;
6use std::process::Command;
7use std::str::FromStr;
8
9pub use radicle_oid::{str::ParseOidError, Oid};
10
11pub extern crate radicle_git_ref_format as fmt;
12
13use crate::crypto::PublicKey;
14use crate::node::Alias;
15use crate::rad;
16use crate::storage::RemoteId;
17
18pub use crate::storage::git::transport::local::Url;
19
20use raw::ErrorExt as _;
21
22pub type BranchName = crate::git::fmt::RefString;
23
24/// Default port of the `git` transport protocol.
25pub const PROTOCOL_PORT: u16 = 9418;
26/// Minimum required git version.
27pub const VERSION_REQUIRED: Version = Version {
28    major: 2,
29    minor: 31,
30    patch: 0,
31};
32
33/// A parsed git version.
34#[derive(PartialEq, Eq, Debug, PartialOrd, Ord)]
35pub struct Version {
36    pub major: u8,
37    pub minor: u8,
38    pub patch: u8,
39}
40
41impl std::fmt::Display for Version {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
44    }
45}
46
47/// Verbosity level for Git commands.
48#[derive(Default, Clone, Copy)]
49pub struct Verbosity(i8);
50
51impl Verbosity {
52    /// Transform into a command line flag, helpful for passing to invocations
53    /// of `git`.
54    ///
55    /// See <https://github.com/git/git/blob/c44beea485f0f2feaf460e2ac87fdd5608d63cf0/builtin/pull.c#L264-L276>
56    pub fn into_flag(&self) -> Option<String> {
57        const FLAG_PREFIX: &str = "-";
58        const FLAG_QUIET: &str = "q";
59        const FLAG_VERBOSE: &str = "v";
60
61        let repetitions = self.0.unsigned_abs() as usize;
62
63        if repetitions == 0 {
64            return None;
65        }
66
67        let flag = if self.0 > 0 { FLAG_VERBOSE } else { FLAG_QUIET };
68
69        Some(FLAG_PREFIX.to_string() + &flag.repeat(repetitions))
70    }
71
72    /// Clamps verbosity to a range, as some commands only accept a specific
73    /// number of repetitions.
74    fn clamp(self, min: i8, max: i8) -> Self {
75        Self(self.0.clamp(min, max))
76    }
77
78    /// Clamps verbosity to at most `-v` or `-q`, as some commands do not accept
79    /// repetitions.
80    pub fn clamp_one(self) -> Self {
81        self.clamp(-1, 1)
82    }
83}
84
85impl From<i8> for Verbosity {
86    fn from(v: i8) -> Self {
87        Self(v)
88    }
89}
90
91#[derive(thiserror::Error, Debug)]
92pub enum VersionError {
93    #[error("malformed git version string")]
94    Malformed,
95    #[error("malformed git version string: {0}")]
96    ParseInt(#[from] std::num::ParseIntError),
97    #[error("malformed git version string: {0}")]
98    Utf8(#[from] std::string::FromUtf8Error),
99    #[error("error retrieving git version: {0}")]
100    Io(#[from] io::Error),
101    #[error("error retrieving git version: {0}")]
102    Other(String),
103}
104
105impl std::str::FromStr for Version {
106    type Err = VersionError;
107
108    fn from_str(input: &str) -> Result<Self, Self::Err> {
109        let rest = input
110            .strip_prefix("git version ")
111            .ok_or(VersionError::Malformed)?;
112        let rest = rest.split(' ').next().ok_or(VersionError::Malformed)?;
113        let rest = rest.trim_end();
114
115        let mut parts = rest.split('.');
116        let major = parts.next().ok_or(VersionError::Malformed)?.parse()?;
117        let minor = parts.next().ok_or(VersionError::Malformed)?.parse()?;
118
119        let patch = match parts.next() {
120            None => 0,
121            Some(patch) => patch.parse()?,
122        };
123
124        Ok(Self {
125            major,
126            minor,
127            patch,
128        })
129    }
130}
131
132/// Get the system's git version.
133pub fn version() -> Result<Version, VersionError> {
134    let output = Command::new("git").arg("version").output()?;
135
136    if output.status.success() {
137        let output = String::from_utf8(output.stdout)?;
138        let version = output.parse()?;
139
140        return Ok(version);
141    }
142    Err(VersionError::Other(
143        String::from_utf8_lossy(&output.stderr).to_string(),
144    ))
145}
146
147#[derive(thiserror::Error, Debug)]
148pub enum RefError {
149    #[error("ref name is not valid UTF-8")]
150    InvalidName,
151    #[error("unexpected unqualified ref: {0}")]
152    Unqualified(fmt::RefString),
153    #[error("invalid ref format: {0}")]
154    Format(#[from] fmt::Error),
155    #[error("reference has no target")]
156    NoTarget,
157    #[error("expected ref to begin with 'refs/namespaces' but found '{0}'")]
158    MissingNamespace(fmt::RefString),
159    #[error("ref name contains invalid namespace identifier '{name}'")]
160    InvalidNamespace {
161        name: fmt::RefString,
162        #[source]
163        err: Box<dyn std::error::Error + Send + Sync + 'static>,
164    },
165    #[error(transparent)]
166    Other(#[from] raw::Error),
167}
168
169#[derive(thiserror::Error, Debug)]
170pub enum ListRefsError {
171    #[error("git error: {0}")]
172    Git(#[from] raw::Error),
173    #[error("invalid ref: {0}")]
174    InvalidRef(#[from] RefError),
175}
176
177pub mod refs {
178    use std::sync::LazyLock;
179
180    use radicle_cob as cob;
181
182    use super::fmt::*;
183    use super::*;
184
185    /// Try to get a qualified reference from a generic reference.
186    pub fn qualified_from<'a>(r: &'a raw::Reference) -> Result<(Qualified<'a>, Oid), RefError> {
187        let name = r.name().ok_or(RefError::InvalidName)?;
188        let refstr = RefStr::try_from_str(name)?;
189        let target = r.resolve()?.target().ok_or(RefError::NoTarget)?;
190        let qualified = Qualified::from_refstr(refstr)
191            .ok_or_else(|| RefError::Unqualified(refstr.to_owned()))?;
192
193        Ok((qualified, target.into()))
194    }
195
196    /// Create a qualified branch reference.
197    ///
198    /// `refs/heads/<branch>`
199    ///
200    pub fn branch<'a>(branch: &RefStr) -> Qualified<'a> {
201        Qualified::from(lit::refs_heads(branch))
202    }
203
204    /// A patch reference.
205    ///
206    /// `refs/heads/patches/<object_id>`
207    ///
208    pub fn patch<'a>(object_id: &cob::ObjectId) -> Qualified<'a> {
209        Qualified::from_components(
210            component!("heads"),
211            component!("patches"),
212            Some(object_id.into()),
213        )
214    }
215
216    pub mod storage {
217        use super::*;
218
219        /// Where the repo's identity document is stored.
220        ///
221        /// `refs/rad/id`
222        ///
223        pub static IDENTITY_BRANCH: LazyLock<Qualified> =
224            LazyLock::new(|| Qualified::from_components(component!("rad"), component!("id"), None));
225
226        /// Where the repo's identity root document is stored.
227        ///
228        /// `refs/rad/root`
229        ///
230        pub static IDENTITY_ROOT: LazyLock<Qualified> = LazyLock::new(|| {
231            Qualified::from_components(component!("rad"), component!("root"), None)
232        });
233
234        /// Where the project's signed references are stored.
235        ///
236        /// `refs/rad/sigrefs`
237        ///
238        pub static SIGREFS_BRANCH: LazyLock<Qualified> = LazyLock::new(|| {
239            Qualified::from_components(component!("rad"), component!("sigrefs"), None)
240        });
241
242        /// A reference to the parent commit.
243        ///
244        /// `refs/rad/sigrefs-parent`
245        ///
246        pub static SIGREFS_PARENT: LazyLock<Qualified> = LazyLock::new(|| {
247            Qualified::from_components(component!("rad"), component!("sigrefs-parent"), None)
248        });
249
250        /// The set of special references used in the Heartwood protocol.
251        #[derive(Clone, Copy, Debug)]
252        pub enum Special {
253            /// `rad/id`
254            Id,
255            /// `rad/sigrefs`
256            SignedRefs,
257        }
258
259        impl From<Special> for Qualified<'_> {
260            fn from(s: Special) -> Self {
261                match s {
262                    Special::Id => (*IDENTITY_BRANCH).clone(),
263                    Special::SignedRefs => (*SIGREFS_BRANCH).clone(),
264                }
265            }
266        }
267
268        impl Special {
269            pub fn namespaced<'a>(&self, remote: &PublicKey) -> Namespaced<'a> {
270                Qualified::from(*self).with_namespace(Component::from(remote))
271            }
272
273            pub fn from_qualified(refname: &Qualified) -> Option<Self> {
274                if refname == &*IDENTITY_BRANCH {
275                    Some(Special::Id)
276                } else if refname == &*SIGREFS_BRANCH {
277                    Some(Special::SignedRefs)
278                } else {
279                    None
280                }
281            }
282        }
283
284        /// Create the [`Namespaced`] `branch` under the `remote` namespace, i.e.
285        ///
286        /// `refs/namespaces/<remote>/refs/heads/<branch>`
287        ///
288        pub fn branch_of<'a>(remote: &RemoteId, branch: &RefStr) -> Namespaced<'a> {
289            Qualified::from(lit::refs_heads(branch)).with_namespace(remote.into())
290        }
291
292        /// Get the branch where the project's identity document is stored.
293        ///
294        /// `refs/namespaces/<remote>/refs/rad/id`
295        ///
296        pub fn id(remote: &RemoteId) -> Namespaced<'_> {
297            IDENTITY_BRANCH.with_namespace(remote.into())
298        }
299
300        /// Get the root of the branch where the project's identity document is stored.
301        ///
302        /// `refs/namespaces/<remote>/refs/rad/root`
303        ///
304        pub fn id_root(remote: &RemoteId) -> Namespaced<'_> {
305            IDENTITY_ROOT.with_namespace(remote.into())
306        }
307
308        /// Get the branch where the `remote`'s signed references are
309        /// stored.
310        ///
311        /// `refs/namespaces/<remote>/refs/rad/sigrefs`
312        ///
313        pub fn sigrefs(remote: &RemoteId) -> Namespaced<'_> {
314            SIGREFS_BRANCH.with_namespace(remote.into())
315        }
316
317        /// The collaborative object reference, identified by `typename` and `object_id`, under the given `remote`.
318        ///
319        /// `refs/namespaces/<remote>/refs/cobs/<typename>/<object_id>`
320        ///
321        pub fn cob<'a>(
322            remote: &RemoteId,
323            typename: &cob::TypeName,
324            object_id: &cob::ObjectId,
325        ) -> Namespaced<'a> {
326            Qualified::from_components(
327                component!("cobs"),
328                Component::from(typename),
329                Some(object_id.into()),
330            )
331            .with_namespace(remote.into())
332        }
333
334        /// All collaborative objects, identified by `typename` and `object_id`, for all remotes.
335        ///
336        /// `refs/namespaces/*/refs/cobs/<typename>/<object_id>`
337        ///
338        pub fn cobs(typename: &cob::TypeName, object_id: &cob::ObjectId) -> refspec::PatternString {
339            pattern!("refs/namespaces/*")
340                .join(refname!("refs/cobs"))
341                .join(Component::from(typename))
342                .join(Component::from(object_id))
343        }
344
345        /// Draft references.
346        ///
347        /// These references are not replicated or signed.
348        pub mod draft {
349            use super::*;
350
351            /// Review draft reference. Points to the non-COB part of a patch review.
352            ///
353            /// `refs/namespaces/<remote>/refs/drafts/reviews/<patch-id>`
354            ///
355            /// When building a patch review, we store the intermediate state in this ref.
356            pub fn review<'a>(remote: &RemoteId, patch: &cob::ObjectId) -> Namespaced<'a> {
357                Qualified::from_components(
358                    component!("drafts"),
359                    component!("reviews"),
360                    Some(Component::from(patch)),
361                )
362                .with_namespace(remote.into())
363            }
364
365            /// A draft collaborative object. This can also be a draft operation on an existing
366            /// object.
367            ///
368            /// `refs/namespaces/<remote>/refs/drafts/cobs/<typename>/<object_id>`
369            ///
370            pub fn cob<'a>(
371                remote: &RemoteId,
372                typename: &cob::TypeName,
373                object_id: &cob::ObjectId,
374            ) -> Namespaced<'a> {
375                Qualified::from_components(
376                    component!("drafts"),
377                    component!("cobs"),
378                    [Component::from(typename), object_id.into()],
379                )
380                .with_namespace(remote.into())
381            }
382
383            /// All draft collaborative object, identified by `typename` and `object_id`, for all remotes.
384            ///
385            /// `refs/namespaces/*/refs/drafts/cobs/<typename>/<object_id>`
386            ///
387            pub fn cobs(
388                typename: &cob::TypeName,
389                object_id: &cob::ObjectId,
390            ) -> refspec::PatternString {
391                pattern!("refs/namespaces/*")
392                    .join(refname!("refs/drafts/cobs"))
393                    .join(Component::from(typename))
394                    .join(Component::from(object_id))
395            }
396        }
397
398        /// Staging/temporary references.
399        pub mod staging {
400            use super::*;
401
402            /// Where patch heads are pushed initially, before patch creation.
403            /// This is a short-lived reference, which is deleted after the patch has been opened.
404            /// The `<oid>` is the commit proposed in the patch.
405            ///
406            /// `refs/namespaces/<remote>/refs/tmp/heads/<oid>`
407            ///
408            pub fn patch<'a>(remote: &RemoteId, oid: impl Into<Oid>) -> Namespaced<'a> {
409                // SAFETY: OIDs are valid reference names and valid path component.
410                #[allow(clippy::unwrap_used)]
411                let oid = RefString::try_from(oid.into().to_string()).unwrap();
412                #[allow(clippy::unwrap_used)]
413                let oid = Component::from_refstr(oid).unwrap();
414
415                Qualified::from_components(component!("tmp"), component!("heads"), Some(oid))
416                    .with_namespace(remote.into())
417            }
418        }
419    }
420
421    pub mod workdir {
422        use super::*;
423
424        /// Create a [`RefString`] that corresponds to `refs/heads/<branch>`.
425        pub fn branch(branch: &RefStr) -> RefString {
426            refname!("refs/heads").join(branch)
427        }
428
429        /// Create a [`RefString`] that corresponds to `refs/notes/<name>`.
430        pub fn note(name: &RefStr) -> RefString {
431            refname!("refs/notes").join(name)
432        }
433
434        /// Create a [`RefString`] that corresponds to `refs/remotes/<remote>/<branch>`.
435        pub fn remote_branch(remote: &RefStr, branch: &RefStr) -> RefString {
436            refname!("refs/remotes").and(remote).and(branch)
437        }
438
439        /// Create a [`RefString`] that corresponds to `refs/tags/<branch>`.
440        pub fn tag(name: &RefStr) -> RefString {
441            refname!("refs/tags").join(name)
442        }
443
444        /// A patch head.
445        ///
446        /// `refs/remotes/rad/patches/<patch-id>`
447        ///
448        pub fn patch_upstream<'a>(patch_id: &cob::ObjectId) -> Qualified<'a> {
449            Qualified::from_components(
450                component!("remotes"),
451                crate::rad::REMOTE_COMPONENT.clone(),
452                [component!("patches"), patch_id.into()],
453            )
454        }
455    }
456}
457
458/// Parse a [`fmt::Qualified`] reference string while expecting the reference
459/// to start with `refs/namespaces`. If the namespace is not present, then an
460/// error will be returned.
461///
462/// The namespace returned is the path component that is after `refs/namespaces`,
463/// e.g. in the reference below, the segment is
464/// `z6MkvUJtYD9dHDJfpevWRT98mzDDpdAtmUjwyDSkyqksUr7C`:
465///
466/// ```text, no_run
467/// refs/namespaces/z6MkvUJtYD9dHDJfpevWRT98mzDDpdAtmUjwyDSkyqksUr7C/refs/heads/main
468/// ```
469///
470/// The `T` can be specified when calling the function. For example, if you
471/// wanted to parse the namespace as a `PublicKey`, then you would the function
472/// like so, `parse_ref_namespaced::<PublicKey>(s)`.
473pub fn parse_ref_namespaced<T>(s: &str) -> Result<(T, fmt::Qualified<'_>), RefError>
474where
475    T: FromStr,
476    T::Err: std::error::Error + Send + Sync + 'static,
477{
478    match parse_ref::<T>(s) {
479        Ok((None, refname)) => Err(RefError::MissingNamespace(refname.to_ref_string())),
480        Ok((Some(t), r)) => Ok((t, r)),
481        Err(err) => Err(err),
482    }
483}
484
485/// Parse a [`fmt::Qualified`] reference string. It will optionally return
486/// the namespace, if present.
487///
488/// The qualified form could be of the form: `refs/heads/main`,
489/// `refs/tags/v1.0`, etc.
490///
491/// The namespace returned is the path component that is after `refs/namespaces`,
492/// e.g. in the reference below, the segment is
493/// `z6MkvUJtYD9dHDJfpevWRT98mzDDpdAtmUjwyDSkyqksUr7C`:
494///
495/// ```text, no_run
496/// refs/namespaces/z6MkvUJtYD9dHDJfpevWRT98mzDDpdAtmUjwyDSkyqksUr7C/refs/heads/main
497/// ```
498///
499/// The `T` can be specified when calling the function. For example, if you
500/// wanted to parse the namespace as a `PublicKey`, then you would the function
501/// like so, `parse_ref::<PublicKey>(s)`.
502pub fn parse_ref<T>(s: &str) -> Result<(Option<T>, fmt::Qualified<'_>), RefError>
503where
504    T: FromStr,
505    T::Err: std::error::Error + Send + Sync + 'static,
506{
507    let input = fmt::RefStr::try_from_str(s)?;
508    match input.to_namespaced() {
509        None => {
510            let refname = fmt::Qualified::from_refstr(input)
511                .ok_or_else(|| RefError::Unqualified(input.to_owned()))?;
512
513            Ok((None, refname))
514        }
515        Some(ns) => {
516            let id = ns
517                .namespace()
518                .as_str()
519                .parse()
520                .map_err(|err| RefError::InvalidNamespace {
521                    name: input.to_owned(),
522                    err: Box::new(err),
523                })?;
524            let rest = ns.strip_namespace();
525
526            Ok((Some(id), rest))
527        }
528    }
529}
530
531/// Create an initial empty commit.
532pub fn initial_commit<'a>(
533    repo: &'a raw::Repository,
534    sig: &raw::Signature,
535) -> Result<raw::Commit<'a>, raw::Error> {
536    let tree_id = repo.index()?.write_tree()?;
537    let tree = repo.find_tree(tree_id)?;
538    let oid = repo.commit(None, sig, sig, "Initial commit", &tree, &[])?;
539    let commit = repo.find_commit(oid)?;
540
541    Ok(commit)
542}
543
544/// Create a commit and update the given ref to it.
545pub fn commit<'a>(
546    repo: &'a raw::Repository,
547    parent: &'a raw::Commit,
548    target: &fmt::RefStr,
549    message: &str,
550    sig: &raw::Signature,
551    tree: &raw::Tree,
552) -> Result<raw::Commit<'a>, raw::Error> {
553    let oid = repo.commit(Some(target.as_str()), sig, sig, message, tree, &[parent])?;
554    let commit = repo.find_commit(oid)?;
555
556    Ok(commit)
557}
558
559/// Create an empty commit on top of the parent.
560pub fn empty_commit<'a>(
561    repo: &'a raw::Repository,
562    parent: &'a raw::Commit,
563    target: &fmt::RefStr,
564    message: &str,
565    sig: &raw::Signature,
566) -> Result<raw::Commit<'a>, raw::Error> {
567    let tree = parent.tree()?;
568    let oid = repo.commit(Some(target.as_str()), sig, sig, message, &tree, &[parent])?;
569    let commit = repo.find_commit(oid)?;
570
571    Ok(commit)
572}
573
574/// Get the repository head.
575pub fn head(repo: &raw::Repository) -> Result<raw::Commit<'_>, raw::Error> {
576    let head = repo.head()?.peel_to_commit()?;
577
578    Ok(head)
579}
580
581/// Write a tree with the given blob at the given path.
582pub fn write_tree<'r>(
583    path: &Path,
584    bytes: &[u8],
585    repo: &'r raw::Repository,
586) -> Result<raw::Tree<'r>, raw::Error> {
587    let blob_id = repo.blob(bytes)?;
588    let mut builder = repo.treebuilder(None)?;
589    builder.insert(path, blob_id, 0o100_644)?;
590
591    let tree_id = builder.write()?;
592    let tree = repo.find_tree(tree_id)?;
593
594    Ok(tree)
595}
596
597/// Configure a radicle repository.
598///
599/// * Sets `push.default = upstream`.
600pub fn configure_repository(repo: &raw::Repository) -> Result<(), raw::Error> {
601    let mut cfg = repo.config()?;
602    cfg.set_str("push.default", "upstream")?;
603
604    Ok(())
605}
606
607/// Configure a repository's radicle remote.
608///
609/// The entry for this remote will be:
610/// ```text
611/// [remote.<name>]
612///   url = <fetch>
613///   pushurl = <push>
614///   fetch = +refs/heads/*:refs/remotes/<name>/*
615///   fetch = +refs/tags/*:refs/remotes/<name>/tags/*
616///   tagOpt = --no-tags
617///   pruneTags = false
618/// ```
619///
620/// Because of the `+refs/tags/*:…` refspec, set:
621///  1. `pruneTags = false` to ensure that `git` does not delete tags because
622///     the remote does not have them. Tags for a Radicle repository are
623///     synthesised by canonical refs and thus, the `rad` remote will handle
624///     fetching them.
625///  2. `tagOpt = --no-tags` to ensure that tags are not fetched and stored
626///     under `refs/tags`, again, because these are fetched by the `rad` remote.
627pub fn configure_remote<'r>(
628    repo: &'r raw::Repository,
629    name: &str,
630    fetch: &Url,
631    push: &Url,
632) -> Result<raw::Remote<'r>, raw::Error> {
633    let fetchspec = format!("+refs/heads/*:refs/remotes/{name}/*");
634    let remote = repo.remote_with_fetch(name, fetch.to_string().as_str(), &fetchspec)?;
635
636    // We want to be able fetch tags from a peer's namespace and this is
637    // necessary to do so, since Git assumes that tags should always be fetched
638    // from the top-level `refs/tags` namespace
639    let tags = format!("+refs/tags/*:refs/remotes/{name}/tags/*");
640    repo.remote_add_fetch(name, &tags)?;
641
642    if name != (*rad::REMOTE_NAME).as_str() {
643        let mut config = repo.config()?;
644        config.set_bool(&format!("remote.{name}.pruneTags"), false)?;
645        config.set_str(&format!("remote.{name}.tagOpt"), "--no-tags")?;
646    }
647
648    if push != fetch {
649        repo.remote_set_pushurl(name, Some(push.to_string().as_str()))?;
650    }
651    Ok(remote)
652}
653
654/// Fetch from the given `remote`.
655pub fn fetch(repo: &raw::Repository, remote: &str) -> Result<(), raw::Error> {
656    repo.find_remote(remote)?.fetch::<&str>(
657        &[],
658        Some(
659            raw::FetchOptions::new()
660                .update_fetchhead(false)
661                .prune(raw::FetchPrune::On)
662                .download_tags(raw::AutotagOption::None),
663        ),
664        None,
665    )
666}
667
668/// Push `refspecs` to the given `remote` using the provided `namespace`.
669pub fn push<'a>(
670    repo: &raw::Repository,
671    remote: &str,
672    refspecs: impl IntoIterator<Item = (&'a fmt::Qualified<'a>, &'a fmt::Qualified<'a>)>,
673) -> Result<(), raw::Error> {
674    let refspecs = refspecs
675        .into_iter()
676        .map(|(src, dst)| format!("{src}:{dst}"));
677
678    repo.find_remote(remote)?
679        .push(refspecs.collect::<Vec<_>>().as_slice(), None)?;
680
681    Ok(())
682}
683
684/// Set the upstream of the given branch to the given remote.
685///
686/// This writes to the `config` directly. The entry will look like the
687/// following:
688///
689/// ```text
690/// [branch "main"]
691///     remote = rad
692///     merge = refs/heads/main
693/// ```
694pub fn set_upstream(
695    repo: &raw::Repository,
696    remote: impl AsRef<str>,
697    branch: impl AsRef<str>,
698    merge: impl AsRef<str>,
699) -> Result<(), raw::Error> {
700    let remote = remote.as_ref();
701    let branch = branch.as_ref();
702    let merge = merge.as_ref();
703
704    let mut config = repo.config()?;
705    let branch_remote = format!("branch.{branch}.remote");
706    let branch_merge = format!("branch.{branch}.merge");
707
708    config.remove_multivar(&branch_remote, ".*").or_else(|e| {
709        if e.is_not_found() {
710            Ok(())
711        } else {
712            Err(e)
713        }
714    })?;
715    config.remove_multivar(&branch_merge, ".*").or_else(|e| {
716        if e.is_not_found() {
717            Ok(())
718        } else {
719            Err(e)
720        }
721    })?;
722    config.set_multivar(&branch_remote, ".*", remote)?;
723    config.set_multivar(&branch_merge, ".*", merge)?;
724
725    Ok(())
726}
727
728pub fn init_default_branch(repo: &raw::Repository) -> Result<Option<String>, raw::Error> {
729    let config = repo.config().and_then(|mut c| c.snapshot())?;
730    let default_branch = config.get_str("init.defaultbranch")?;
731    let branch = repo.find_branch(default_branch, raw::BranchType::Local)?;
732    Ok(branch.into_reference().shorthand().map(ToOwned::to_owned))
733}
734
735pub fn head_refname(repo: &raw::Repository) -> Result<Option<String>, raw::Error> {
736    let head = repo.head()?;
737    match head.shorthand() {
738        Some("HEAD") => Ok(None),
739        Some(refname) => Ok(Some(refname.to_owned())),
740        None => Ok(None),
741    }
742}
743
744/// Execute a `git` command by spawning a child process and collect its output.
745/// If `working` is [`Some`], the command is run as if `git` was started in
746/// `working` instead of the current working directory, by prepending
747/// `-C <working>` to the command line.
748pub fn run<S>(
749    working: Option<&std::path::Path>,
750    args: impl IntoIterator<Item = S>,
751) -> io::Result<std::process::Output>
752where
753    S: AsRef<std::ffi::OsStr>,
754{
755    let mut cmd = Command::new("git");
756
757    if let Some(working) = working {
758        cmd.arg("-C").arg(dunce::canonicalize(working)?);
759    }
760
761    cmd.args(args).output()
762}
763
764/// Functions that call to the `git` CLI instead of `git2`.
765pub mod process {
766    use std::io;
767    use std::path::Path;
768
769    use crate::storage::ReadRepository;
770
771    use super::{run, Oid, Verbosity};
772
773    /// Perform a local fetch, from storage using `git fetch-pack`.
774    ///
775    /// `oids` are the set of [`Oid`]s that are being fetched from the
776    /// `storage`.
777    pub fn fetch_pack<R>(
778        working: Option<&Path>,
779        storage: &R,
780        oids: impl IntoIterator<Item = Oid>,
781        verbosity: Verbosity,
782    ) -> io::Result<std::process::Output>
783    where
784        R: ReadRepository,
785    {
786        let mut args = vec!["fetch-pack".to_string()];
787        args.extend(verbosity.clamp_one().into_flag());
788        args.push(dunce::canonicalize(storage.path())?.display().to_string());
789        args.extend(oids.into_iter().map(|oid| oid.to_string()));
790        run(working, args)
791    }
792}
793
794/// Git URLs.
795pub mod url {
796    use std::path::PathBuf;
797
798    use crate::prelude::RepoId;
799
800    /// A Git URL using the `file://` scheme.
801    pub struct File {
802        pub path: PathBuf,
803    }
804
805    impl File {
806        /// Create a new file URL pointing to the given path.
807        pub fn new(path: impl Into<PathBuf>) -> Self {
808            Self { path: path.into() }
809        }
810
811        /// Return a URL with the given RID set.
812        pub fn rid(mut self, rid: RepoId) -> Self {
813            self.path.push(rid.canonical());
814            self
815        }
816    }
817
818    impl std::fmt::Display for File {
819        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
820            write!(f, "file://{}", self.path.display())
821        }
822    }
823}
824
825/// Git environment variables.
826pub mod env {
827    /// Set of environment vars to reset git's configuration to default.
828    pub const GIT_DEFAULT_CONFIG: [(&str, &str); 2] = [
829        ("GIT_CONFIG_GLOBAL", "/dev/null"),
830        ("GIT_CONFIG_NOSYSTEM", "1"),
831    ];
832}
833
834/// The user information used for signing commits and configuring the
835/// `name` and `email` fields in the Git config.
836#[derive(Debug, Clone)]
837pub struct UserInfo {
838    /// Alias of the local peer.
839    pub alias: Alias,
840    /// [`PublicKey`] of the local peer.
841    pub key: PublicKey,
842}
843
844impl UserInfo {
845    /// The name of the user, i.e. the `alias`.
846    pub fn name(&self) -> Alias {
847        self.alias.clone()
848    }
849
850    /// The "email" of the user, which is in the form
851    /// `<alias>@<public key>`.
852    pub fn email(&self) -> String {
853        format!("{}@{}", self.alias, self.key)
854    }
855}
856
857#[cfg(test)]
858mod test {
859    use super::*;
860    use std::str::FromStr;
861
862    #[test]
863    fn test_version_ord() {
864        assert!(
865            Version {
866                major: 2,
867                minor: 34,
868                patch: 1
869            } > Version {
870                major: 2,
871                minor: 34,
872                patch: 0
873            }
874        );
875        assert!(
876            Version {
877                major: 2,
878                minor: 24,
879                patch: 12
880            } < Version {
881                major: 2,
882                minor: 34,
883                patch: 0
884            }
885        );
886    }
887
888    #[test]
889    fn test_version_from_str() {
890        assert_eq!(
891            Version::from_str("git version 2.34.1\n").ok(),
892            Some(Version {
893                major: 2,
894                minor: 34,
895                patch: 1
896            })
897        );
898
899        assert_eq!(
900            Version::from_str("git version 2.34.1 (macOS)").ok(),
901            Some(Version {
902                major: 2,
903                minor: 34,
904                patch: 1
905            })
906        );
907
908        assert_eq!(
909            Version::from_str("git version 2.34").ok(),
910            Some(Version {
911                major: 2,
912                minor: 34,
913                patch: 0
914            })
915        );
916
917        assert!(Version::from_str("2.34").is_err());
918    }
919}