1use 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
30pub const DEFAULT_REMOTE_NAME: &str = "origin";
32
33#[derive(Debug, Default, Serialize, Deserialize)]
35pub struct GitConfig {
36 pub path: Utf8PathBuf,
38 pub remotes: Vec<GitRemote>,
40 #[serde(skip_serializing_if = "Option::is_none")]
44 pub branch: Option<String>,
45 #[serde(default = "prune_default")]
47 pub prune: bool,
48}
49
50const fn prune_default() -> bool {
52 false
53}
54
55pub(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#[derive(Debug, Default, Parser, Serialize, Deserialize)]
121pub struct GitRemote {
122 pub name: String,
124 pub fetch_url: String,
126 #[serde(skip_serializing_if = "Option::is_none")]
128 pub push_url: Option<String>,
129}
130
131impl GitRemote {
132 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)]
150pub enum GitTaskError {
152 InvalidRemote,
154 UnexpectedNone,
156}