up_rs/tasks/
git.rs

1//! The git library task.
2use self::GitTaskError as E;
3use crate::opts::GitOptions;
4use crate::tasks::task::TaskStatus;
5use crate::tasks::ResolveEnv;
6use crate::tasks::TaskError;
7use camino::Utf8PathBuf;
8use clap::Parser;
9use color_eyre::eyre::Result;
10use displaydoc::Display;
11use git2::Remote;
12use rayon::iter::Either;
13use rayon::prelude::*;
14use serde_derive::Deserialize;
15use serde_derive::Serialize;
16use std::convert::From;
17use thiserror::Error;
18use tracing::error;
19
20pub mod branch;
21pub mod checkout;
22pub mod cherry;
23pub mod errors;
24pub mod fetch;
25pub mod merge;
26pub mod prune;
27pub mod status;
28pub mod update;
29
30/// Default git remote name.
31pub const DEFAULT_REMOTE_NAME: &str = "origin";
32
33/// `up git` configuration options.
34#[derive(Debug, Default, Serialize, Deserialize)]
35pub struct GitConfig {
36    /// Path to download git repo to.
37    pub path: Utf8PathBuf,
38    /// Remote to set/update.
39    pub remotes: Vec<GitRemote>,
40    /// Branch to checkout when cloning/updating. Defaults to the current branch
41    /// when updating, or the default branch of the first remote for
42    /// cloning.
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub branch: Option<String>,
45    /// Prune local branches whose changes have already been merged upstream.
46    #[serde(default = "prune_default")]
47    pub prune: bool,
48}
49
50/// Serde needs a function to set a default, so this sets a default of false.
51const fn prune_default() -> bool {
52    false
53}
54
55/// Run the `up git` task.
56pub(crate) fn run(configs: &[GitConfig]) -> Result<TaskStatus> {
57    let (statuses, errors): (Vec<_>, Vec<_>) = configs
58        .par_iter()
59        .map(update::update)
60        .partition_map(|x| match x {
61            Ok(status) => Either::Left(status),
62            Err(e) => Either::Right(e),
63        });
64
65    if errors.is_empty() {
66        if statuses.iter().all(|s| matches!(s, TaskStatus::Skipped)) {
67            Ok(TaskStatus::Skipped)
68        } else {
69            Ok(TaskStatus::Passed)
70        }
71    } else {
72        for error in &errors {
73            error!("{error:?}");
74        }
75        let first_error = errors.into_iter().next().ok_or(E::UnexpectedNone)?;
76        Err(first_error)
77    }
78}
79
80impl From<GitOptions> for GitConfig {
81    fn from(item: GitOptions) -> Self {
82        Self {
83            path: item.git_path,
84            remotes: vec![GitRemote {
85                name: item.remote,
86                push_url: None,
87                fetch_url: item.git_url,
88            }],
89            branch: item.branch,
90            prune: item.prune,
91        }
92    }
93}
94
95impl ResolveEnv for Vec<GitConfig> {
96    fn resolve_env<F>(&mut self, env_fn: F) -> Result<(), TaskError>
97    where
98        F: Fn(&str) -> Result<String, TaskError>,
99    {
100        for config in self.iter_mut() {
101            if let Some(branch) = config.branch.as_ref() {
102                config.branch = Some(env_fn(branch)?);
103            }
104            config.path = Utf8PathBuf::from(env_fn(config.path.as_str())?);
105            for remote in &mut config.remotes {
106                remote.name = env_fn(&remote.name)?;
107                remote.push_url = if let Some(push_url) = &remote.push_url {
108                    Some(env_fn(push_url)?)
109                } else {
110                    None
111                };
112                remote.fetch_url = env_fn(&remote.fetch_url)?;
113            }
114        }
115        Ok(())
116    }
117}
118
119/// Represents a git remote.
120#[derive(Debug, Default, Parser, Serialize, Deserialize)]
121pub struct GitRemote {
122    /// Name of the remote to set in git.
123    pub name: String,
124    /// URL to fetch from. Also used for pushing if `push_url` unset.
125    pub fetch_url: String,
126    /// URL to push to, defaults to fetch URL.
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub push_url: Option<String>,
129}
130
131impl GitRemote {
132    /// Create a git remote from a git2-rs remote.
133    pub(crate) fn from(remote: &Remote) -> Result<Self> {
134        let fetch_url = remote.url().ok_or(E::InvalidRemote)?.to_owned();
135
136        let push_url = match remote.pushurl() {
137            Some(url) if url != fetch_url => Some(url.to_owned()),
138            _ => None,
139        };
140
141        Ok(Self {
142            name: remote.name().ok_or(E::InvalidRemote)?.to_owned(),
143            fetch_url,
144            push_url,
145        })
146    }
147}
148
149#[derive(Error, Debug, Display)]
150/// Errors thrown by this file.
151pub enum GitTaskError {
152    /// Remote un-named, or invalid UTF-8 name.
153    InvalidRemote,
154    /// Unexpected None in option.
155    UnexpectedNone,
156}