Skip to content

Commit d08205a

Browse files
committed
refactor: use RepoName(String) and RepoOwner(String) newtypes
1 parent 57531ee commit d08205a

File tree

5 files changed

+120
-49
lines changed

5 files changed

+120
-49
lines changed

src/cli.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
//! Parse the command-line arguments
22
3-
use std::path::PathBuf;
4-
53
use clap::{
64
CommandFactory as _, Parser, Subcommand,
75
builder::styling::{AnsiColor, Effects},
86
};
97

108
use crate::{
119
commands,
12-
config::{BranchName, Commit, PrNumber, Remote},
10+
config::{BranchName, Commit, PatchName, PrNumber, Remote},
1311
};
1412

1513
/// A tool which makes it easy to declaratively manage personal forks by automatically merging pull requests
@@ -40,7 +38,7 @@ pub enum Command {
4038
commit: Commit,
4139
/// Choose a custom file name for the `.patch` file
4240
#[arg(short, long)]
43-
filename: Option<PathBuf>,
41+
filename: Option<PatchName>,
4442
},
4543
/// Fetch pull request for a GitHub repository as a local branch
4644
PrFetch {
@@ -138,15 +136,17 @@ pub struct Branch {
138136

139137
#[cfg(test)]
140138
mod test {
139+
use crate::config::{RepoName, RepoOwner};
140+
141141
use super::*;
142142

143143
#[test]
144144
fn parse_remote() {
145145
assert_eq!(
146146
"helix-editor/helix".parse::<Remote>().unwrap(),
147147
Remote {
148-
owner: "helix-editor".to_string(),
149-
repo: "helix".to_string(),
148+
owner: RepoOwner::try_new("helix-editor").unwrap(),
149+
repo: RepoName::try_new("helix").unwrap(),
150150
branch: BranchName::try_new("main").unwrap(),
151151
commit: None
152152
}
@@ -156,8 +156,8 @@ mod test {
156156
.parse::<Remote>()
157157
.unwrap(),
158158
Remote {
159-
owner: "helix-editor".to_string(),
160-
repo: "helix".to_string(),
159+
owner: RepoOwner::try_new("helix-editor").unwrap(),
160+
repo: RepoName::try_new("helix").unwrap(),
161161
branch: BranchName::try_new("master").unwrap(),
162162
commit: Some(Commit::try_new("1a2b3c").unwrap())
163163
}

src/commands/gen_patch.rs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ use std::path::PathBuf;
66
use anyhow::bail;
77

88
use crate::CONFIG_PATH;
9-
use crate::config::Commit;
9+
use crate::config::{Commit, PatchName};
1010
use crate::git::git;
1111
use crate::utils::normalize_commit_msg;
1212

1313
/// Generate patch `filename` at the given `Commit`
14-
pub fn gen_patch(commit: Commit, filename: Option<PathBuf>) -> anyhow::Result<()> {
14+
pub fn gen_patch(commit: Commit, filename: Option<PatchName>) -> anyhow::Result<()> {
1515
if !CONFIG_PATH.exists() {
1616
log::info!(
1717
"Config directory {} does not exist, creating it...",
@@ -23,15 +23,17 @@ pub fn gen_patch(commit: Commit, filename: Option<PathBuf>) -> anyhow::Result<()
2323
// 1. if the user provides a custom filename for the patch file, use that
2424
// 2. otherwise use the commit message
2525
// 3. if all fails use the commit hash
26-
let patch_filename = filename.map_or_else(
27-
|| {
28-
git(["log", "--format=%B", "--max-count=1", commit.as_ref()]).map_or_else(
29-
|_| commit.clone().into_inner(),
30-
|commit_msg| normalize_commit_msg(&commit_msg),
31-
)
32-
},
33-
|filename| filename.to_str().unwrap_or("").to_string(),
34-
);
26+
let patch_filename = filename.unwrap_or_else(|| {
27+
git(["log", "--format=%B", "--max-count=1", commit.as_ref()]).map_or_else(
28+
|_| {
29+
PatchName::try_new(commit.clone().into_inner().into()).expect("commit is not empty")
30+
},
31+
|commit_msg| {
32+
PatchName::try_new(PathBuf::from(normalize_commit_msg(&commit_msg)))
33+
.expect("normalized commit message is not empty")
34+
},
35+
)
36+
});
3537

3638
let patch_file_path = CONFIG_PATH.join(format!("{patch_filename}.patch"));
3739

src/commands/pr_fetch.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use anyhow::{Context as _, anyhow};
44
use colored::Colorize as _;
55

6-
use crate::config::{BranchName, Commit, PrNumber, Remote};
6+
use crate::config::{BranchName, Commit, PrNumber, Remote, RepoName, RepoOwner};
77
use crate::git::{fetch_pull_request, git};
88

99
/// Fetch the given `pr` of `remote` at `commit` and store it in local `branch`
@@ -33,8 +33,8 @@ pub async fn pr_fetch(
3333
.and_then(|x| x.split_once('/'))
3434
.with_context(err)?;
3535
Ok(Remote {
36-
owner: owner.to_string(),
37-
repo: repo.to_string(),
36+
owner: RepoOwner::try_new(owner)?,
37+
repo: RepoName::try_new(repo)?,
3838
branch: BranchName::try_new("main").expect("`main` is a valid branch name"),
3939
commit: None,
4040
})

src/commands/run.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,8 @@ pub async fn run(yes: bool) -> anyhow::Result<()> {
187187

188188
log::info!(
189189
"Merged branch {}/{}/{} {}",
190-
owner.bright_blue(),
191-
repo.bright_blue(),
190+
owner.as_ref().bright_blue(),
191+
repo.as_ref().bright_blue(),
192192
branch.as_ref().bright_blue(),
193193
remote
194194
.commit

src/config.rs

Lines changed: 94 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use anyhow::{anyhow, bail};
44
use itertools::Itertools;
55
use nutype::nutype;
6-
use std::{convert::Infallible, num::NonZeroU32, str::FromStr};
6+
use std::{convert::Infallible, fmt::Display, num::NonZeroU32, path::PathBuf, str::FromStr};
77
use tap::Pipe as _;
88

99
use indexmap::IndexSet;
@@ -17,7 +17,7 @@ pub struct Config {
1717
pub local_branch: BranchName,
1818
/// List of patches to apply
1919
#[serde(default)]
20-
pub patches: IndexSet<String>,
20+
pub patches: IndexSet<PatchName>,
2121
/// List of pull request to apply
2222
#[serde(default)]
2323
pub pull_requests: Vec<PullRequest>,
@@ -34,9 +34,9 @@ pub struct Config {
3434
#[derive(Debug, Eq, PartialEq, Clone)]
3535
pub struct Remote {
3636
/// e.g. `helix-editor`
37-
pub owner: String,
37+
pub owner: RepoOwner,
3838
/// e.g. `helix`
39-
pub repo: String,
39+
pub repo: RepoName,
4040
/// e.g. `master`
4141
pub branch: BranchName,
4242
/// e.g. `1a2b3c`
@@ -66,6 +66,9 @@ impl FromStr for Remote {
6666
bail!("Invalid branch format: {item}. Expected format: owner/repo/branch");
6767
};
6868

69+
let owner = RepoOwner::try_new(owner)?;
70+
let repo = RepoName::try_new(repo)?;
71+
6972
let branch = parts
7073
// insert back the removed '/', this could be part of the branch itself
7174
// e.g. in `helix-editor/helix/master/main` the branch is considered to be `master/main`
@@ -87,8 +90,8 @@ impl FromStr for Remote {
8790
.map_err(|err| anyhow!("invalid branch name: {err}"))?;
8891

8992
Ok(Self {
90-
owner: owner.to_string(),
91-
repo: repo.to_string(),
93+
owner,
94+
repo,
9295
branch,
9396
commit,
9497
})
@@ -184,11 +187,67 @@ impl FromStr for Ref {
184187
}
185188
}
186189

190+
#[cfg(test)]
191+
impl From<&str> for RepoOwner {
192+
fn from(value: &str) -> Self {
193+
Self::try_new(value).unwrap()
194+
}
195+
}
196+
#[cfg(test)]
197+
impl From<&str> for RepoName {
198+
fn from(value: &str) -> Self {
199+
Self::try_new(value).unwrap()
200+
}
201+
}
202+
#[cfg(test)]
203+
impl From<&str> for BranchName {
204+
fn from(value: &str) -> Self {
205+
Self::try_new(value).unwrap()
206+
}
207+
}
208+
#[cfg(test)]
209+
impl From<&str> for Commit {
210+
fn from(value: &str) -> Self {
211+
Self::try_new(value).unwrap()
212+
}
213+
}
214+
187215
/// Number of a pull request
188216
#[nutype(const_fn, derive(Eq, PartialEq, Display, Debug, FromStr, Copy, Clone))]
189217
pub struct PrNumber(NonZeroU32);
190218

219+
#[cfg(test)]
220+
impl From<u32> for PrNumber {
221+
fn from(value: u32) -> Self {
222+
Self::new(NonZeroU32::new(value).unwrap())
223+
}
224+
}
225+
226+
/// Represents owner of a repository
227+
///
228+
/// E.g. in `helix-editor/helix/master`, this is `helix-editor`
229+
#[nutype(
230+
validate(not_empty),
231+
derive(
232+
Debug, Eq, PartialEq, Ord, PartialOrd, Clone, AsRef, Display, Serialize
233+
)
234+
)]
235+
pub struct RepoOwner(String);
236+
237+
/// Represents name of a repository
238+
///
239+
/// E.g. in `helix-editor/helix/master`, this is `helix`
240+
#[nutype(
241+
validate(not_empty),
242+
derive(
243+
Debug, Eq, PartialEq, Ord, PartialOrd, Clone, AsRef, Display, Serialize
244+
)
245+
)]
246+
pub struct RepoName(String);
247+
191248
/// Name of a branch in git
249+
///
250+
/// E.g. in `helix-editor/helix/master`, this is `master`
192251
#[nutype(
193252
validate(not_empty),
194253
derive(
@@ -207,6 +266,16 @@ impl FromStr for BranchName {
207266
}
208267
}
209268

269+
/// File name of a patch
270+
#[nutype(validate(predicate = |p| !p.as_os_str().is_empty()), derive(Hash, Eq, PartialEq, Debug, AsRef, Deserialize, Clone, FromStr))]
271+
pub struct PatchName(PathBuf);
272+
273+
impl Display for PatchName {
274+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275+
write!(f, "{}", self.as_ref().display())
276+
}
277+
}
278+
210279
/// Represents a git commit hash
211280
#[nutype(
212281
validate(not_empty, predicate = is_valid_commit_hash),
@@ -264,45 +333,45 @@ mod tests {
264333
(
265334
"helix-editor/helix/master @ 1a2b3c",
266335
Remote {
267-
owner: "helix-editor".to_string(),
268-
repo: "helix".to_string(),
269-
branch: BranchName::try_new("master").unwrap(),
270-
commit: Some(Commit::try_new("1a2b3c".to_string()).unwrap()),
336+
owner: "helix-editor".into(),
337+
repo: "helix".into(),
338+
branch: "master".into(),
339+
commit: Some("1a2b3c".into()),
271340
},
272341
),
273342
(
274343
"helix-editor/helix @ deadbeef",
275344
Remote {
276-
owner: "helix-editor".to_string(),
277-
repo: "helix".to_string(),
278-
branch: BranchName::try_new(Remote::DEFAULT_BRANCH).unwrap(),
279-
commit: Some(Commit::try_new("deadbeef".to_string()).unwrap()),
345+
owner: "helix-editor".into(),
346+
repo: "helix".into(),
347+
branch: Remote::DEFAULT_BRANCH.into(),
348+
commit: Some("deadbeef".into()),
280349
},
281350
),
282351
(
283352
"helix-editor/helix/feat/feature-x @ abc123",
284353
Remote {
285-
owner: "helix-editor".to_string(),
286-
repo: "helix".to_string(),
287-
branch: BranchName::try_new("feat/feature-x").unwrap(),
288-
commit: Some(Commit::try_new("abc123".to_string()).unwrap()),
354+
owner: "helix-editor".into(),
355+
repo: "helix".into(),
356+
branch: "feat/feature-x".into(),
357+
commit: Some("abc123".into()),
289358
},
290359
),
291360
(
292361
"owner/repo/branch",
293362
Remote {
294-
owner: "owner".to_string(),
295-
repo: "repo".to_string(),
296-
branch: BranchName::try_new("branch").unwrap(),
363+
owner: "owner".into(),
364+
repo: "repo".into(),
365+
branch: "branch".into(),
297366
commit: None,
298367
},
299368
),
300369
(
301370
"owner/repo",
302371
Remote {
303-
owner: "owner".to_string(),
304-
repo: "repo".to_string(),
305-
branch: BranchName::try_new(Remote::DEFAULT_BRANCH).unwrap(),
372+
owner: "owner".into(),
373+
repo: "repo".into(),
374+
branch: Remote::DEFAULT_BRANCH.into(),
306375
commit: None,
307376
},
308377
),
@@ -338,7 +407,7 @@ patches = ['remove-tab']"#;
338407
conf,
339408
Config {
340409
local_branch: BranchName::try_new("patchy".to_string()).unwrap(),
341-
patches: indexset!["remove-tab".to_string()],
410+
patches: indexset![PatchName::try_new("remove-tab".into()).unwrap()],
342411
pull_requests: vec![
343412
PullRequest {
344413
number: pr_number!(10000),

0 commit comments

Comments
 (0)