29 Commits

Author SHA1 Message Date
dbfb2064d2 chore(release): 0.1.0 (#6)
### Added
- add docker setup
- refactor frontend configuration
- with all the way through
- with create pull request and release
- with gitea
- with prepend as well
- add cliff
- remove tokio
- with doctor
- with git client
- with fixes
- with conventional parse
- with tags command
- add semver
- can get commit chain
- with start of environment engine
- with gitea client
- fmt
- add gitea client stub
- add tests for git setup
- split headings into local and global
- rename to cuddle_please
- add config parsing
- with basic get dir
- add mkdocs
- add base

### Other
- remove old changelog
- *(deps)* update all dependencies (#2)
- *(release)* 0.0.1 (#4)
- release command
- add cuddle.release to this repository
- add granular docker setup
- fix checks
- chck refactor commands
- move doctor command
- fmt
- rename release command
- move gitea command into its own file
- move config list
- move gitea out of the way
- move config building out of main execution loop
- move commands and misc out of main binary package
- fmt
- check hide commands
- move cuddle-please to cuddle-please release
- remove no-vcs option (moved to a later stage if github is someday adopted
- fix clippy warnings
- clippy fix
- fix
- cleanup

Reviewed-on: https://git.front.kjuulh.io/kjuulh/cuddle-please/pulls/6
Co-authored-by: kjuulh <contact@kjuulh.io>
Co-committed-by: kjuulh <contact@kjuulh.io>
2023-08-01 20:54:26 +00:00
ae4b8d7c2d docs: remove old changelog
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-01 17:58:42 +02:00
b16fa8ea87 chore(deps): update all dependencies (#2)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [anyhow](https://github.com/dtolnay/anyhow) | workspace.dependencies | patch | `1.0.71` -> `1.0.72` |
| [clap](https://github.com/clap-rs/clap) | workspace.dependencies | patch | `4.3.4` -> `4.3.19` |

---

### Release Notes

<details>
<summary>dtolnay/anyhow</summary>

### [`v1.0.72`](https://github.com/dtolnay/anyhow/releases/tag/1.0.72)

[Compare Source](https://github.com/dtolnay/anyhow/compare/1.0.71...1.0.72)

-   Documentation improvements

</details>

<details>
<summary>clap-rs/clap</summary>

### [`v4.3.19`](https://github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#&#8203;4319---2023-07-21)

[Compare Source](https://github.com/clap-rs/clap/compare/v4.3.18...v4.3.19)

##### Fixes

-   *(parse)* Respect `value_terminator` even in the presence of later multiple-value positional arguments

### [`v4.3.18`](https://github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#&#8203;4318---2023-07-21)

[Compare Source](https://github.com/clap-rs/clap/compare/v4.3.17...v4.3.18)

##### Fixes

-   *(parse)* Suggest `--` in fewer places where it won't work

### [`v4.3.17`](https://github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#&#8203;4317---2023-07-19)

[Compare Source](https://github.com/clap-rs/clap/compare/v4.3.16...v4.3.17)

##### Fixes

-   *(help)* Address a regression in wrapping `PossibleValue` descriptions in `--help`

### [`v4.3.16`](https://github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#&#8203;4316---2023-07-18)

[Compare Source](https://github.com/clap-rs/clap/compare/v4.3.15...v4.3.16)

##### Fixes

-   Don't assert when stateful value parsers fail on defaults (e.g. checking if a path exists)

### [`v4.3.15`](https://github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#&#8203;4315---2023-07-18)

[Compare Source](https://github.com/clap-rs/clap/compare/v4.3.14...v4.3.15)

##### Features

-   *(unstable-styles)* Re-export `anstyle`

##### Documentation

-   *(unstable-styles)* Provide more examples

### [`v4.3.14`](https://github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#&#8203;4314---2023-07-17)

[Compare Source](https://github.com/clap-rs/clap/compare/v4.3.13...v4.3.14)

##### Features

-   `ArgAction::HelpShort` and `ArgAction::HelpLong` for explicitly specifying which style of help to display

##### Fixes

-   Skip `[OPTIONS]` in usage if a help or version `ArgAction` is used

### [`v4.3.13`](https://github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#&#8203;4313---2023-07-17)

[Compare Source](https://github.com/clap-rs/clap/compare/v4.3.12...v4.3.13)

### [`v4.3.12`](https://github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#&#8203;4312---2023-07-14)

[Compare Source](https://github.com/clap-rs/clap/compare/v4.3.11...v4.3.12)

##### Fixes

-   *(derive)* Don't error on enum variant field attributes

### [`v4.3.11`](https://github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#&#8203;4311---2023-07-05)

[Compare Source](https://github.com/clap-rs/clap/compare/v4.3.10...v4.3.11)

##### Features

-   *(derive)* Support fields wrapped in `num::Wrapping`, `Box`, or `Arc`
-   *(derive)* Support `Box<str>`, `Box<OsStr>`, and `Box<Path>`

### [`v4.3.10`](https://github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#&#8203;4310---2023-06-30)

[Compare Source](https://github.com/clap-rs/clap/compare/v4.3.9...v4.3.10)

##### Performance

-   Drop a dependency, reducing binary size by 1.3 KiB

### [`v4.3.9`](https://github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#&#8203;439---2023-06-28)

[Compare Source](https://github.com/clap-rs/clap/compare/v4.3.8...v4.3.9)

##### Fixes

-   `Command::ignore_errors` no longer masks help/version

### [`v4.3.8`](https://github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#&#8203;438---2023-06-23)

[Compare Source](https://github.com/clap-rs/clap/compare/v4.3.7...v4.3.8)

##### Fixes

-   Error on ambiguity with `infer_long_arg`, rather than arbitrarily picking one, matching the documentation and subcommand's behavior

### [`v4.3.7`](https://github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#&#8203;437---2023-06-23)

[Compare Source](https://github.com/clap-rs/clap/compare/v4.3.6...v4.3.7)

##### Documentation

-   Further clarify magic behavior in derive tutorial
-   Further clarify derive API's relationship to builder within the tutorial

### [`v4.3.6`](https://github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#&#8203;436---2023-06-23)

[Compare Source](https://github.com/clap-rs/clap/compare/v4.3.5...v4.3.6)

##### Documentation

-   Suggest `clio`

### [`v4.3.5`](https://github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#&#8203;435---2023-06-20)

[Compare Source](https://github.com/clap-rs/clap/compare/v4.3.4...v4.3.5)

-   `ColorChoice::possible_values` is added to simplify things for builder users

##### Fixes

-   `ColorChoice::to_possible_value` no longer includes descriptions, encouraging shorter help where possible

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://github.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNC4xNjAuMCIsInVwZGF0ZWRJblZlciI6IjM0LjE2MC4wIn0=-->

Reviewed-on: https://git.front.kjuulh.io/kjuulh/cuddle-please/pulls/2
2023-08-01 15:56:54 +00:00
52d551425a chore(release): 0.0.1 (#4)
# Changelog
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.2.0] - 2023-08-01

### Added
- add docker setup
- refactor frontend configuration
- with all the way through
- with create pull request and release
- with gitea
- with prepend as well
- add cliff
- remove tokio
- with doctor
- with git client
- with fixes
- with conventional parse
- with tags command
- add semver
- can get commit chain
- with start of environment engine
- with gitea client
- fmt
- add gitea client stub
- add tests for git setup
- split headings into local and global
- rename to cuddle_please
- add config parsing
- with basic get dir
- add mkdocs
- add base

### Other
- release command
- add cuddle.release to this repository
- add granular docker setup
- fix checks
- chck refactor commands
- move doctor command
- fmt
- rename release command
- move gitea command into its own file
- move config list
- move gitea out of the way
- move config building out of main execution loop
- move commands and misc out of main binary package
- fmt
- check hide commands
- move cuddle-please to cuddle-please release
- remove no-vcs option (moved to a later stage if github is someday adopted
- fix clippy warnings
- clippy fix
- fix
- cleanup

Reviewed-on: https://git.front.kjuulh.io/kjuulh/cuddle-please/pulls/4
Co-authored-by: kjuulh <contact@kjuulh.io>
Co-committed-by: kjuulh <contact@kjuulh.io>
2023-08-01 15:53:50 +00:00
e6f84f744d refactor: release command
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-01 17:47:21 +02:00
5be71b1af6 docs: add cuddle.release to this repository
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-01 17:19:05 +02:00
edbc3fb164 docs: add granular docker setup
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-01 17:18:30 +02:00
4b4f967af8 feat: add docker setup
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-01 17:17:55 +02:00
3bfac7bb54 docs: fix checks
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-01 17:17:39 +02:00
241241aaf4 docs: chck refactor commands
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-01 17:16:43 +02:00
526b2b7461 refactor: move doctor command
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-01 17:15:58 +02:00
af5d0f4af5 chore: fmt
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-01 17:12:50 +02:00
aeaffb775e refactor: rename release command
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-01 17:12:24 +02:00
b13e3916f6 refactor: move gitea command into its own file
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-01 17:11:59 +02:00
ae9073bf0b refactor: move config list
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-01 17:01:00 +02:00
e51454088e refactor: move gitea out of the way
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-01 16:38:30 +02:00
39db4b8d1c refactor: move config building out of main execution loop
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-01 15:36:24 +02:00
c7793f7422 refactor: move commands and misc out of main binary package
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-01 15:34:24 +02:00
8b83b9c14d chore: fmt
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-01 15:02:05 +02:00
8cd68d569b feat: refactor frontend configuration
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-01 02:31:44 +02:00
e235483783 docs: check hide commands
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-31 23:14:18 +02:00
ebbae295fd refactor: move cuddle-please to cuddle-please release
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-31 23:04:49 +02:00
2650edb61e chore: remove no-vcs option (moved to a later stage if github is someday adopted
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-31 23:00:53 +02:00
2d5abedf1a chore: fix clippy warnings
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-31 13:38:20 +02:00
bc3e091f45 chore: clippy fix
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-31 13:34:23 +02:00
df96de1cd0 feat: with all the way through
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-31 13:27:55 +02:00
86eabad6fe feat: with create pull request and release
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-31 11:40:16 +02:00
0e876a25a6 feat: with gitea
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-31 11:28:56 +02:00
8c3a0c699c feat: with prepend as well
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-07-31 10:35:22 +02:00
51 changed files with 2848 additions and 1181 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
target/
.cuddle/
.env

63
CHANGELOG.md Normal file
View File

@@ -0,0 +1,63 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.1.0] - 2023-08-01
### Added
- add docker setup
- refactor frontend configuration
- with all the way through
- with create pull request and release
- with gitea
- with prepend as well
- add cliff
- remove tokio
- with doctor
- with git client
- with fixes
- with conventional parse
- with tags command
- add semver
- can get commit chain
- with start of environment engine
- with gitea client
- fmt
- add gitea client stub
- add tests for git setup
- split headings into local and global
- rename to cuddle_please
- add config parsing
- with basic get dir
- add mkdocs
- add base
### Other
- remove old changelog
- *(deps)* update all dependencies (#2)
- *(release)* 0.0.1 (#4)
- release command
- add cuddle.release to this repository
- add granular docker setup
- fix checks
- chck refactor commands
- move doctor command
- fmt
- rename release command
- move gitea command into its own file
- move config list
- move gitea out of the way
- move config building out of main execution loop
- move commands and misc out of main binary package
- fmt
- check hide commands
- move cuddle-please to cuddle-please release
- remove no-vcs option (moved to a later stage if github is someday adopted
- fix clippy warnings
- clippy fix
- fix
- cleanup

97
Cargo.lock generated
View File

@@ -344,8 +344,82 @@ dependencies = [
"chrono",
"clap",
"conventional_commit_parser",
"cuddle-please-commands",
"cuddle-please-frontend",
"cuddle-please-misc",
"dotenv",
"git-cliff-core",
"lazy_static",
"parse-changelog",
"pretty_assertions",
"regex",
"reqwest",
"semver",
"serde",
"serde_yaml",
"tempdir",
"tracing",
"tracing-subscriber",
"tracing-test",
"url",
]
[[package]]
name = "cuddle-please-commands"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"clap",
"conventional_commit_parser",
"cuddle-please-frontend",
"cuddle-please-misc",
"dotenv",
"git-cliff-core",
"lazy_static",
"parse-changelog",
"pretty_assertions",
"regex",
"reqwest",
"semver",
"serde",
"serde_yaml",
"tempdir",
"tracing",
"tracing-subscriber",
"tracing-test",
"url",
]
[[package]]
name = "cuddle-please-frontend"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"clap",
"dotenv",
"pretty_assertions",
"serde",
"serde_yaml",
"tempdir",
"tracing",
"tracing-subscriber",
"tracing-test",
]
[[package]]
name = "cuddle-please-misc"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"clap",
"conventional_commit_parser",
"dotenv",
"git-cliff-core",
"lazy_static",
"parse-changelog",
"pretty_assertions",
"regex",
"reqwest",
@@ -815,6 +889,7 @@ checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [
"equivalent",
"hashbrown 0.14.0",
"serde",
]
[[package]]
@@ -887,6 +962,12 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lexopt"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baff4b617f7df3d896f97fe922b64817f6cd9a756bb81d40f8883f2f66dcb401"
[[package]]
name = "libc"
version = "0.2.147"
@@ -1110,6 +1191,22 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "parse-changelog"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39a24196a65fc15a0a747df8c041abc5a009f2c09c550b0a14f7eeb0c10255ef"
dependencies = [
"anyhow",
"indexmap 2.0.0",
"lexopt",
"memchr",
"once_cell",
"regex",
"serde",
"serde_json",
]
[[package]]
name = "parse-zoneinfo"
version = "0.3.0"

View File

@@ -1,14 +1,17 @@
[workspace]
members = ["crates/cuddle-please"]
members = ["crates/cuddle-please", "crates/cuddle-please-frontend", "crates/cuddle-please-commands", "crates/cuddle-please-misc"]
resolver = "2"
[workspace.dependencies]
cuddle-please = { path = "crates/cuddle-please" }
cuddle-please-frontend = { path = "crates/cuddle-please-frontend" }
cuddle-please-commands = { path = "crates/cuddle-please-commands" }
cuddle-please-misc = { path = "crates/cuddle-please-misc" }
anyhow = { version = "1.0.71" }
anyhow = { version = "1.0.72" }
tracing = { version = "0.1", features = ["log"] }
tracing-subscriber = { version = "0.3.17" }
clap = { version = "4.3.4", features = ["derive", "env"] }
clap = { version = "4.3.19", features = ["derive", "env"] }
dotenv = { version = "0.15.0" }
url = { version = "2.4.0" }
serde_yaml = { version = "0.9.25" }
@@ -20,6 +23,8 @@ reqwest = { version = "0.11.18" }
git-cliff-core = "1.2.0"
regex = "*"
chrono = "*"
lazy_static = "*"
parse-changelog = "*"
tracing-test = "0.2"
pretty_assertions = "1.4"

View File

@@ -1,17 +1,23 @@
# Cuddle Please
Cuddle Please is an extension to `cuddle`, it is a separate binary that can be executed standalone as cuddle-please, or in cuddle as `cuddle please`.
Cuddle Please is an extension to `cuddle`, it is a separate binary that can be executed standalone as `cuddle-please`, or in cuddle as `cuddle please`.
The goal of the software is to be a `release-please` clone, targeting `gitea` instead of `github`.
The tool can be executed as a binary using:
`cuddle please pr create`
```bash
cuddle please release # if using cuddle
# or
cuddle-please release # if using standalone
```
And when a release has been built:
```bash
cuddle please release
# or
cuddle-please release
```
cuddle will default to information to it available in git, or use a specific entry in `cuddle.yaml` called
@@ -27,3 +33,35 @@ please:
or as `cuddle.please.yaml`
See docs for more information about installation and some such
## Checklist
### 0.1 Milestone
- [x] Hide unneccessary commands
- [x] Redo configuration frontend
- [x] Refactor command.rs into smaller bits so that bits are easier to test
- [x] Add cuddle.release.yaml to this repo
- [x] Setup temporary git name and email to use for git committing
### 0.2 Milestone
- [ ] Add docs
- [ ] Add asciinema
- [ ] Add examples
- [ ] Fx drone config
- [ ] Releaser
- [ ] On main/master
- [ ] Add reporter for PR and Repositories
- [ ] tbd...
### 0.3 Milestone
- [ ] Add inquire for missing values when needed (when not running in ci or have a proper tty)
- [ ] Break down cuddle-please-misc
### 0.x Milestone
- [ ] Add github support
- [ ] Add custom strategies
- [ ] Create docker image
- [ ] Add more granular tests

View File

@@ -0,0 +1,31 @@
[package]
name = "cuddle-please-commands"
version = "0.1.0"
edition = "2021"
[dependencies]
cuddle-please-frontend.workspace = true
cuddle-please-misc.workspace = true
anyhow.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
clap.workspace = true
dotenv.workspace = true
serde_yaml.workspace = true
serde.workspace = true
reqwest = { workspace = true, features = ["blocking", "json"] }
url.workspace = true
semver.workspace = true
conventional_commit_parser.workspace = true
tempdir.workspace = true
git-cliff-core.workspace = true
regex.workspace = true
chrono.workspace = true
lazy_static.workspace = true
parse-changelog.workspace = true
[dev-dependencies]
tracing-test = { workspace = true, features = ["no-env-filter"] }
pretty_assertions.workspace = true

View File

@@ -0,0 +1,193 @@
use std::{
io::Read,
ops::Deref,
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
use clap::{Parser, Subcommand};
use cuddle_please_frontend::{gatheres::ConfigArgs, PleaseConfig, PleaseConfigBuilder};
use cuddle_please_misc::{
ConsoleUi, DynRemoteGitClient, DynUi, GiteaClient, GlobalArgs, LocalGitClient, StdinFn,
VcsClient,
};
use crate::{
config_command::{ConfigCommand, ConfigCommandHandler},
doctor_command::DoctorCommandHandler,
gitea_command::{GiteaCommand, GiteaCommandHandler},
release_command::ReleaseCommandHandler,
};
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
pub struct Command {
#[command(flatten)]
global: GlobalArgs,
#[command(flatten)]
config: ConfigArgs,
#[command(subcommand)]
commands: Option<Commands>,
#[clap(skip)]
ui: DynUi,
#[clap(skip)]
stdin: StdinFn,
}
impl Default for Command {
fn default() -> Self {
Self::new()
}
}
impl Command {
pub fn new() -> Self {
let args = std::env::args();
Self::new_from_args_with_stdin(Some(ConsoleUi::default()), args, || {
let mut input = String::new();
std::io::stdin().read_to_string(&mut input)?;
Ok(input)
})
}
pub fn new_from_args<I, T, UIF>(ui: Option<UIF>, i: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
UIF: Into<DynUi>,
{
let mut s = Self::parse_from(i);
if let Some(ui) = ui {
s.ui = ui.into();
}
s
}
pub fn new_from_args_with_stdin<I, T, F, UIF>(ui: Option<UIF>, i: I, input: F) -> Self
where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
F: Fn() -> anyhow::Result<String> + Send + Sync + 'static,
UIF: Into<DynUi>,
{
let mut s = Self::parse_from(i);
if let Some(ui) = ui {
s.ui = ui.into();
}
s.stdin = Some(Arc::new(Mutex::new(input)));
s
}
pub fn execute(self, current_dir: Option<&Path>) -> anyhow::Result<()> {
let config = self.build_config(current_dir)?;
let git_client = self.get_git(&config)?;
let gitea_client = self.get_gitea_client(&config);
match &self.commands {
Some(Commands::Release {}) => {
ReleaseCommandHandler::new(self.ui, config, git_client, gitea_client)
.execute(self.global.dry_run)?;
}
Some(Commands::Config { command }) => {
ConfigCommandHandler::new(self.ui, config).execute(command)?;
}
Some(Commands::Gitea { command }) => {
GiteaCommandHandler::new(self.ui, config, gitea_client)
.execute(command, self.global.token.expect("token to be set").deref())?;
}
Some(Commands::Doctor {}) => {
DoctorCommandHandler::new(self.ui).execute()?;
}
None => {}
}
Ok(())
}
fn build_config(&self, current_dir: Option<&Path>) -> Result<PleaseConfig, anyhow::Error> {
let mut builder = &mut PleaseConfigBuilder::new();
if self.global.config_stdin {
if let Some(stdin_fn) = self.stdin.clone() {
let output = (stdin_fn.lock().unwrap().deref())();
builder = builder.with_stdin(output?);
}
}
let current_dir = get_current_path(current_dir, self.config.source.clone())?;
let config = builder
.with_config_file(&current_dir)
.with_source(&current_dir)
.with_execution_env(std::env::vars())
.with_cli(self.config.clone())
.build()?;
Ok(config)
}
fn get_git(&self, config: &PleaseConfig) -> anyhow::Result<VcsClient> {
if self.global.no_vcs {
Ok(VcsClient::new_noop())
} else {
VcsClient::new_git(
config.get_source(),
config.settings.git_username.clone(),
config.settings.git_email.clone(),
)
}
}
fn get_gitea_client(&self, config: &PleaseConfig) -> DynRemoteGitClient {
match self.global.engine {
cuddle_please_misc::RemoteEngine::Local => Box::new(LocalGitClient::new()),
cuddle_please_misc::RemoteEngine::Gitea => Box::new(GiteaClient::new(
config.get_api_url(),
self.global.token.as_deref(),
)),
}
}
}
#[derive(Debug, Clone, Subcommand)]
pub enum Commands {
/// Config is mostly used for debugging the final config output
Release {},
#[command(hide = true)]
Config {
#[command(subcommand)]
command: ConfigCommand,
},
#[command(hide = true)]
Gitea {
#[command(subcommand)]
command: GiteaCommand,
},
/// Helps you identify missing things from your execution environment for cuddle-please to function as intended
Doctor {},
}
fn get_current_path(
optional_current_dir: Option<&Path>,
optional_source_path: Option<PathBuf>,
) -> anyhow::Result<PathBuf> {
let path = optional_source_path
.or_else(|| optional_current_dir.map(|p| p.to_path_buf())) // fall back on current env from environment
.filter(|v| v.to_string_lossy() != "") // make sure we don't get empty values
//.and_then(|p| p.canonicalize().ok()) // Make sure we get the absolute path
//.context("could not find current dir, pass --source as a replacement")?;
.unwrap_or(PathBuf::from("."));
if !path.exists() {
anyhow::bail!("path doesn't exist {}", path.display());
}
Ok(path)
}

View File

@@ -0,0 +1,32 @@
use clap::Subcommand;
use cuddle_please_frontend::PleaseConfig;
use cuddle_please_misc::DynUi;
#[derive(Subcommand, Debug, Clone)]
pub enum ConfigCommand {
/// List will list the final configuration
List {},
}
pub struct ConfigCommandHandler {
ui: DynUi,
config: PleaseConfig,
}
impl ConfigCommandHandler {
pub fn new(ui: DynUi, config: PleaseConfig) -> Self {
Self { ui, config }
}
pub fn execute(&self, command: &ConfigCommand) -> anyhow::Result<()> {
match command {
ConfigCommand::List {} => {
tracing::debug!("running command: config list");
self.ui.write_str_ln("cuddle-config");
self.ui.write_str(&format!("{}", self.config));
}
}
Ok(())
}
}

View File

@@ -0,0 +1,26 @@
use cuddle_please_misc::DynUi;
pub struct DoctorCommandHandler {
ui: DynUi,
}
impl DoctorCommandHandler {
pub fn new(ui: DynUi) -> Self {
Self { ui }
}
pub fn execute(&self) -> anyhow::Result<()> {
match std::process::Command::new("git").arg("-v").output() {
Ok(o) => {
let stdout = std::str::from_utf8(&o.stdout).unwrap_or("");
self.ui.write_str_ln(&format!("OK: {}", stdout));
}
Err(e) => {
self.ui
.write_str_ln(&format!("WARNING: git is not installed: {}", e));
}
}
Ok(())
}
}

View File

@@ -0,0 +1,102 @@
use clap::Subcommand;
use cuddle_please_frontend::PleaseConfig;
use cuddle_please_misc::{get_most_significant_version, DynRemoteGitClient, DynUi};
#[derive(Subcommand, Debug, Clone)]
pub enum GiteaCommand {
Connect {},
Tags {
#[command(subcommand)]
command: Option<GiteaTagsCommand>,
},
SinceCommit {
#[arg(long)]
sha: String,
#[arg(long)]
branch: String,
},
CheckPr {},
}
#[derive(Subcommand, Debug, Clone)]
pub enum GiteaTagsCommand {
MostSignificant {},
}
pub struct GiteaCommandHandler {
ui: DynUi,
config: PleaseConfig,
gitea_client: DynRemoteGitClient,
}
impl GiteaCommandHandler {
pub fn new(ui: DynUi, config: PleaseConfig, gitea_client: DynRemoteGitClient) -> Self {
Self {
ui,
config,
gitea_client,
}
}
pub fn execute(&self, command: &GiteaCommand, _token: &str) -> anyhow::Result<()> {
let owner = self.config.get_owner();
let repository = self.config.get_repository();
match command {
GiteaCommand::Connect {} => {
self.gitea_client.connect(owner, repository)?;
self.ui.write_str_ln("connected succesfully go gitea");
}
GiteaCommand::Tags { command } => match command {
Some(GiteaTagsCommand::MostSignificant {}) => {
let tags = self.gitea_client.get_tags(owner, repository)?;
match get_most_significant_version(tags.iter().collect()) {
Some(tag) => {
self.ui.write_str_ln(&format!(
"found most significant tags: {}",
tag.name
));
}
None => {
self.ui.write_str_ln("found no tags with versioning schema");
}
}
}
None => {
let tags = self.gitea_client.get_tags(owner, repository)?;
self.ui.write_str_ln("got tags from gitea");
for tag in tags {
self.ui.write_str_ln(&format!("- {}", tag.name))
}
}
},
GiteaCommand::SinceCommit { sha, branch } => {
let commits =
self.gitea_client
.get_commits_since(owner, repository, Some(sha), branch)?;
self.ui.write_str_ln("got commits from gitea");
for commit in commits {
self.ui.write_str_ln(&format!("- {}", commit.get_title()))
}
}
GiteaCommand::CheckPr {} => {
let pr = self.gitea_client.get_pull_request(owner, repository)?;
match pr {
Some(index) => {
self.ui.write_str_ln(&format!(
"found cuddle-please (index={}) pr from gitea",
index
));
}
None => {
self.ui.write_str_ln("found no cuddle-please pr from gitea");
}
}
}
}
Ok(())
}
}

View File

@@ -0,0 +1,7 @@
mod command;
mod config_command;
mod doctor_command;
mod gitea_command;
mod release_command;
pub use command::Command as PleaseCommand;

View File

@@ -0,0 +1,234 @@
use cuddle_please_frontend::PleaseConfig;
use ::semver::Version;
use anyhow::Context;
use cuddle_please_misc::{
changelog_parser, get_most_significant_version, ChangeLogBuilder, Commit, DynRemoteGitClient,
DynUi, NextVersion, Tag, VcsClient,
};
pub struct ReleaseCommandHandler {
ui: DynUi,
config: PleaseConfig,
git_client: VcsClient,
gitea_client: DynRemoteGitClient,
}
impl ReleaseCommandHandler {
pub fn new(
ui: DynUi,
config: PleaseConfig,
git_client: VcsClient,
gitea_client: DynRemoteGitClient,
) -> Self {
Self {
ui,
config,
git_client,
gitea_client,
}
}
pub fn execute(&self, dry_run: bool) -> anyhow::Result<()> {
tracing::debug!("running command: release");
let owner = self.config.get_owner();
let repository = self.config.get_repository();
let branch = self.config.get_branch();
let source = self.config.get_source();
self.check_git_remote_connection(owner, repository)?;
let significant_tag = self.get_most_significant_tag(owner, repository)?;
let commits =
self.fetch_commits_since_last_tag(owner, repository, &significant_tag, branch)?;
let current_version = get_current_version(significant_tag);
let conventional_commit_results = parse_conventional_commits(current_version, commits)?;
if conventional_commit_results.is_none() {
tracing::debug!("found no new commits, aborting early");
self.ui
.write_str_ln("no new commits found, no release required");
return Ok(());
}
let (commit_strs, next_version) = conventional_commit_results.unwrap();
let (changelog_placement, changelog, changelog_last_changes) =
compose_changelog(&commit_strs, &next_version, source)?;
if let Some(first_commit) = commit_strs.first() {
if first_commit.contains("chore(release): ") {
self.create_release(
dry_run,
owner,
repository,
&next_version,
changelog_last_changes,
)?;
return Ok(());
}
}
self.create_pull_request(
changelog_placement,
changelog,
next_version,
dry_run,
owner,
repository,
changelog_last_changes,
branch,
)?;
Ok(())
}
fn create_release(
&self,
dry_run: bool,
owner: &str,
repository: &str,
next_version: &Version,
changelog_last_changes: Option<String>,
) -> Result<(), anyhow::Error> {
Ok(if !dry_run {
self.gitea_client.create_release(
owner,
repository,
&next_version.to_string(),
&changelog_last_changes.unwrap(),
!next_version.pre.is_empty(),
)?;
} else {
tracing::debug!("creating release (dry_run)");
})
}
fn create_pull_request(
&self,
changelog_placement: std::path::PathBuf,
changelog: String,
next_version: Version,
dry_run: bool,
owner: &str,
repository: &str,
changelog_last_changes: Option<String>,
branch: &str,
) -> Result<(), anyhow::Error> {
self.git_client.checkout_branch()?;
std::fs::write(changelog_placement, changelog.as_bytes())?;
self.git_client
.commit_and_push(next_version.to_string(), dry_run)?;
let _pr_number = match self.gitea_client.get_pull_request(owner, repository)? {
Some(existing_pr) => {
if !dry_run {
self.gitea_client.update_pull_request(
owner,
repository,
&next_version.to_string(),
&changelog_last_changes.unwrap(),
existing_pr,
)?
} else {
tracing::debug!("updating pull request (dry_run)");
1
}
}
None => {
if !dry_run {
self.gitea_client.create_pull_request(
owner,
repository,
&next_version.to_string(),
&changelog,
branch,
)?
} else {
tracing::debug!("creating pull request (dry_run)");
1
}
}
};
Ok(())
}
fn fetch_commits_since_last_tag(
&self,
owner: &str,
repository: &str,
significant_tag: &Option<Tag>,
branch: &str,
) -> Result<Vec<Commit>, anyhow::Error> {
let commits = self.gitea_client.get_commits_since(
owner,
repository,
significant_tag.as_ref().map(|st| st.commit.sha.as_str()),
branch,
)?;
Ok(commits)
}
fn get_most_significant_tag(
&self,
owner: &str,
repository: &str,
) -> Result<Option<Tag>, anyhow::Error> {
let tags = self.gitea_client.get_tags(owner, repository)?;
let significant_tag = get_most_significant_version(tags.iter().collect());
Ok(significant_tag.map(|t| t.clone()))
}
fn check_git_remote_connection(
&self,
owner: &str,
repository: &str,
) -> Result<(), anyhow::Error> {
self.gitea_client
.connect(owner, repository)
.context("failed to connect to gitea repository")?;
Ok(())
}
}
fn compose_changelog(
commit_strs: &Vec<String>,
next_version: &Version,
source: &std::path::PathBuf,
) -> Result<(std::path::PathBuf, String, Option<String>), anyhow::Error> {
let builder = ChangeLogBuilder::new(commit_strs, next_version.to_string()).build();
let changelog_placement = source.join("CHANGELOG.md");
let changelog = match std::fs::read_to_string(&changelog_placement).ok() {
Some(existing_changelog) => builder.prepend(existing_changelog)?,
None => builder.generate()?,
};
let changelog_last_changes = changelog_parser::last_changes(&changelog)?;
Ok((changelog_placement, changelog, changelog_last_changes))
}
fn parse_conventional_commits(
current_version: Version,
commits: Vec<Commit>,
) -> anyhow::Result<Option<(Vec<String>, Version)>> {
let commit_strs = commits
.iter()
.map(|c| c.commit.message.clone())
.collect::<Vec<_>>();
if commit_strs.is_empty() {
tracing::info!("no commits to base release on");
return Ok(None);
}
let next_version = current_version.next(&commit_strs);
Ok(Some((commit_strs, next_version)))
}
fn get_current_version(significant_tag: Option<Tag>) -> Version {
let current_version = significant_tag
.map(|st| Version::try_from(st).unwrap())
.unwrap_or(Version::new(0, 0, 0));
current_version
}

View File

@@ -0,0 +1,19 @@
[package]
name = "cuddle-please-frontend"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
clap.workspace = true
dotenv.workspace = true
serde_yaml.workspace = true
serde.workspace = true
chrono.workspace = true
tempdir.workspace = true
[dev-dependencies]
tracing-test = { workspace = true, features = ["no-env-filter"] }
pretty_assertions.workspace = true

View File

@@ -0,0 +1,91 @@
use std::path::PathBuf;
use clap::Args;
use crate::stage0_config::{
PleaseConfigBuilder, PleaseProjectConfigBuilder, PleaseSettingsConfigBuilder,
};
#[derive(Args, Debug, Clone)]
pub struct ConfigArgs {
/// Which repository to publish against. If not supplied remote url will be inferred from environment or fail if not present.
#[arg(
env = "CUDDLE_PLEASE_API_URL",
long,
global = true,
help_heading = "Config"
)]
pub api_url: Option<String>,
/// repo is the name of repository you want to release for
#[arg(
env = "CUDDLE_PLEASE_REPO",
long,
global = true,
help_heading = "Config"
)]
pub repo: Option<String>,
/// owner is the name of user from which the repository belongs <user>/<repo>
#[arg(
env = "CUDDLE_PLEASE_OWNER",
long,
global = true,
help_heading = "Config"
)]
pub owner: Option<String>,
/// which source directory to use, if not set `std::env::current_dir` is used instead.
#[arg(
env = "CUDDLE_PLEASE_SOURCE",
long,
global = true,
help_heading = "Config"
)]
pub source: Option<PathBuf>,
/// which branch is being run from
#[arg(
env = "CUDDLE_PLEASE_BRANCH",
long,
global = true,
help_heading = "Config"
)]
pub branch: Option<String>,
/// which git username to use for commits
#[arg(
env = "CUDDLE_PLEASE_GIT_USERNAME",
long,
global = true,
help_heading = "Config"
)]
pub git_username: Option<String>,
/// which git email to use for commits
#[arg(
env = "CUDDLE_PLEASE_GIT_EMAIL",
long,
global = true,
help_heading = "Config"
)]
pub git_email: Option<String>,
}
impl From<ConfigArgs> for PleaseConfigBuilder {
fn from(value: ConfigArgs) -> Self {
Self {
project: Some(PleaseProjectConfigBuilder {
owner: value.owner,
repository: value.repo,
source: value.source,
branch: value.branch,
}),
settings: Some(PleaseSettingsConfigBuilder {
api_url: value.api_url,
git_username: value.git_username,
git_email: value.git_email,
}),
}
}
}

View File

@@ -0,0 +1,80 @@
use std::path::{Path, PathBuf};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use crate::stage0_config::PleaseConfigBuilder;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct CuddleEmbeddedPleaseConfig {
please: PleaseConfigBuilder,
}
impl From<CuddleEmbeddedPleaseConfig> for PleaseConfigBuilder {
fn from(value: CuddleEmbeddedPleaseConfig) -> Self {
value.please
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct CuddlePleaseConfig {
#[serde(flatten)]
please: PleaseConfigBuilder,
}
impl From<CuddlePleaseConfig> for PleaseConfigBuilder {
fn from(value: CuddlePleaseConfig) -> Self {
value.please
}
}
const CUDDLE_FILE_NAME: &str = "cuddle";
const CUDDLE_CONFIG_FILE_NAME: &str = "cuddle.please";
const YAML_EXTENSION: &str = "yaml";
pub fn get_config_from_config_file(current_dir: &Path) -> PleaseConfigBuilder {
let current_cuddle_path = current_dir
.clone()
.join(format!("{CUDDLE_FILE_NAME}.{YAML_EXTENSION}"));
let current_cuddle_config_path = current_dir
.clone()
.join(format!("{CUDDLE_CONFIG_FILE_NAME}.{YAML_EXTENSION}"));
let mut please_config = PleaseConfigBuilder::default();
if let Some(config) = get_config_from_file::<CuddleEmbeddedPleaseConfig>(current_cuddle_path) {
please_config = please_config.merge(&config).clone();
}
if let Some(config) = get_config_from_file::<CuddlePleaseConfig>(current_cuddle_config_path) {
please_config = please_config.merge(&config).clone();
}
please_config
}
pub fn get_config_from_file<T>(current_cuddle_path: PathBuf) -> Option<PleaseConfigBuilder>
where
T: DeserializeOwned,
T: Into<PleaseConfigBuilder>,
{
match std::fs::File::open(&current_cuddle_path) {
Ok(file) => match serde_yaml::from_reader::<_, T>(file) {
Ok(config) => {
return Some(config.into());
}
Err(e) => {
tracing::debug!(
"{} doesn't contain a valid please config: {}",
&current_cuddle_path.display(),
e
);
}
},
Err(e) => {
tracing::debug!(
"did not find or was not allowed to read {}, error: {}",
&current_cuddle_path.display(),
e,
);
}
}
None
}

View File

@@ -0,0 +1,53 @@
use std::{collections::HashMap, path::PathBuf};
use crate::stage0_config::{PleaseConfigBuilder, PleaseProjectConfigBuilder};
pub fn get_from_environment(vars: std::env::Vars) -> PleaseConfigBuilder {
let vars: HashMap<String, String> = vars.collect();
let env = detect_environment(&vars);
match env {
ExecutionEnvironment::Local => PleaseConfigBuilder {
project: Some(PleaseProjectConfigBuilder {
source: Some(PathBuf::from(".")),
..Default::default()
}),
settings: None,
},
ExecutionEnvironment::Drone => PleaseConfigBuilder {
project: Some(PleaseProjectConfigBuilder {
owner: Some(
vars.get("DRONE_REPO_OWNER")
.expect("DRONE_REPO_OWNER to be present")
.clone(),
),
repository: Some(
vars.get("DRONE_REPO_NAME")
.expect("DRONE_REPO_NAME to be present")
.clone(),
),
source: Some(PathBuf::from(".")),
branch: Some(
vars.get("DRONE_REPO_BRANCH")
.expect("DRONE_REPO_BRANCH to be present")
.clone(),
),
}),
settings: None,
},
}
}
pub fn detect_environment(vars: &HashMap<String, String>) -> ExecutionEnvironment {
if vars.get("DRONE").is_some() {
return ExecutionEnvironment::Drone;
}
ExecutionEnvironment::Local
}
pub enum ExecutionEnvironment {
Local,
Drone,
}

View File

@@ -0,0 +1,11 @@
mod cli;
mod config_file;
mod execution_env;
mod source;
mod stdin;
pub use cli::ConfigArgs;
pub(crate) use config_file::get_config_from_config_file;
pub(crate) use execution_env::get_from_environment;
pub(crate) use source::get_source;
pub(crate) use stdin::get_config_from_stdin;

View File

@@ -0,0 +1,13 @@
use std::path::Path;
use crate::stage0_config;
pub fn get_source(source: &Path) -> stage0_config::PleaseConfigBuilder {
stage0_config::PleaseConfigBuilder {
project: Some(stage0_config::PleaseProjectConfigBuilder {
source: Some(source.to_path_buf()),
..Default::default()
}),
..Default::default()
}
}

View File

@@ -0,0 +1,19 @@
use serde::Deserialize;
use crate::stage0_config::PleaseConfigBuilder;
pub fn get_config_from_stdin<'d, T>(stdin: &'d str) -> PleaseConfigBuilder
where
T: Deserialize<'d>,
T: Into<PleaseConfigBuilder>,
{
match serde_yaml::from_str::<'d, T>(stdin) {
Ok(config) => {
return config.into();
}
Err(e) => {
tracing::debug!("stdin doesn't contain a valid please config: {}", e);
}
}
PleaseConfigBuilder::default()
}

View File

@@ -0,0 +1,137 @@
use std::{
fmt::Display,
path::{Path, PathBuf},
};
pub mod gatheres;
mod stage0_config;
pub use gatheres::ConfigArgs;
#[derive(Debug, Clone)]
pub struct PleaseProjectConfig {
pub owner: String,
pub repository: String,
pub source: PathBuf,
pub branch: String,
}
#[derive(Debug, Clone)]
pub struct PleaseSettingsConfig {
pub api_url: String,
pub git_username: Option<String>,
pub git_email: Option<String>,
}
#[derive(Debug, Clone)]
pub struct PleaseConfig {
pub project: PleaseProjectConfig,
pub settings: PleaseSettingsConfig,
}
impl PleaseConfig {
pub fn get_owner(&self) -> &str {
&self.project.owner
}
pub fn get_repository(&self) -> &str {
&self.project.repository
}
pub fn get_source(&self) -> &PathBuf {
&self.project.source
}
pub fn get_branch(&self) -> &str {
&self.project.branch
}
pub fn get_api_url(&self) -> &str {
&self.settings.api_url
}
pub fn get_git_username(&self) -> Option<String> {
self.settings.git_username.clone()
}
pub fn get_git_email(&self) -> Option<String> {
self.settings.git_email.clone()
}
}
impl Display for PleaseConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "PleaseConfig")?;
writeln!(f, " owner: {}", self.get_owner())?;
writeln!(f, " repository: {}", self.get_repository())?;
writeln!(f, " branch: {}", self.get_branch())?;
writeln!(f, " api_url: {}", self.get_api_url())?;
if let Some(git_username) = self.get_git_username() {
writeln!(f, " git_username: {}", git_username)?;
}
if let Some(git_email) = self.get_git_email() {
writeln!(f, " git_email: {}", git_email)?;
}
Ok(())
}
}
#[derive(Clone, Debug, Default)]
pub struct PleaseConfigBuilder {
stdin: Option<stage0_config::PleaseConfigBuilder>,
execution_env: Option<stage0_config::PleaseConfigBuilder>,
cli: Option<stage0_config::PleaseConfigBuilder>,
config: Option<stage0_config::PleaseConfigBuilder>,
source: Option<stage0_config::PleaseConfigBuilder>,
}
impl PleaseConfigBuilder {
pub fn new() -> Self {
Self {
..Default::default()
}
}
pub fn with_stdin(&mut self, stdin: String) -> &mut Self {
self.stdin = Some(gatheres::get_config_from_stdin::<
stage0_config::PleaseConfigBuilder,
>(stdin.as_str()));
self
}
pub fn with_config_file(&mut self, current_dir: &Path) -> &mut Self {
self.config = Some(gatheres::get_config_from_config_file(current_dir));
self
}
pub fn with_execution_env(&mut self, env_bag: std::env::Vars) -> &mut Self {
self.execution_env = Some(gatheres::get_from_environment(env_bag));
self
}
pub fn with_cli(&mut self, cli: gatheres::ConfigArgs) -> &mut Self {
self.cli = Some(cli.into());
self
}
pub fn with_source(&mut self, source: &Path) -> &mut Self {
self.source = Some(gatheres::get_source(source));
self
}
pub fn build(&mut self) -> anyhow::Result<PleaseConfig> {
let gathered = vec![
&self.execution_env,
&self.source,
&self.config,
&self.stdin,
&self.cli,
];
let final_config = gathered
.into_iter()
.flatten()
.fold(stage0_config::PleaseConfigBuilder::default(), |mut a, x| {
a.merge(x).clone()
});
final_config.try_into()
}
}

View File

@@ -0,0 +1,122 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::{PleaseConfig, PleaseProjectConfig, PleaseSettingsConfig};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PleaseProjectConfigBuilder {
pub owner: Option<String>,
pub repository: Option<String>,
pub source: Option<PathBuf>,
pub branch: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PleaseSettingsConfigBuilder {
pub api_url: Option<String>,
pub git_username: Option<String>,
pub git_email: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PleaseConfigBuilder {
pub project: Option<PleaseProjectConfigBuilder>,
pub settings: Option<PleaseSettingsConfigBuilder>,
}
impl PleaseConfigBuilder {
pub fn merge(&mut self, config: &PleaseConfigBuilder) -> &Self {
let config = config.clone();
let mut fproject = match self.project.clone() {
None => PleaseProjectConfigBuilder::default(),
Some(project) => project,
};
let mut fsettings = match self.settings.clone() {
None => PleaseSettingsConfigBuilder::default(),
Some(settings) => settings,
};
if let Some(project) = config.project {
if let Some(owner) = project.owner {
fproject.owner = Some(owner);
}
if let Some(repository) = project.repository {
fproject.repository = Some(repository);
}
if let Some(source) = project.source {
fproject.source = Some(source);
}
if let Some(branch) = project.branch {
fproject.branch = Some(branch);
}
self.project = Some(fproject);
}
if let Some(settings) = config.settings {
if let Some(api_url) = settings.api_url {
fsettings.api_url = Some(api_url);
}
if let Some(git_username) = settings.git_username {
fsettings.git_username = Some(git_username);
}
if let Some(git_email) = settings.git_email {
fsettings.git_email = Some(git_email);
}
self.settings = Some(fsettings);
}
self
}
}
impl TryFrom<PleaseConfigBuilder> for PleaseConfig {
type Error = anyhow::Error;
fn try_from(value: PleaseConfigBuilder) -> Result<Self, Self::Error> {
Ok(Self {
project: value
.project
.ok_or(value_is_missing("project"))?
.try_into()?,
settings: value
.settings
.ok_or(value_is_missing("settings"))?
.try_into()?,
})
}
}
impl TryFrom<PleaseProjectConfigBuilder> for PleaseProjectConfig {
type Error = anyhow::Error;
fn try_from(value: PleaseProjectConfigBuilder) -> Result<Self, Self::Error> {
Ok(Self {
owner: value.owner.ok_or(value_is_missing("owner"))?,
repository: value.repository.ok_or(value_is_missing("repository"))?,
source: value.source.ok_or(value_is_missing("source"))?,
branch: value.branch.ok_or(value_is_missing("branch"))?,
})
}
}
impl TryFrom<PleaseSettingsConfigBuilder> for PleaseSettingsConfig {
type Error = anyhow::Error;
fn try_from(value: PleaseSettingsConfigBuilder) -> Result<Self, Self::Error> {
Ok(Self {
api_url: value.api_url.ok_or(value_is_missing("api_url"))?,
git_username: value.git_username.clone(),
git_email: value.git_username.clone(),
})
}
}
fn value_is_missing(message: &str) -> anyhow::Error {
anyhow::anyhow!(
"{} is required, pass via. cli, env or config file, see --help",
message.to_string()
)
}

View File

@@ -0,0 +1,29 @@
[package]
name = "cuddle-please-misc"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
clap.workspace = true
dotenv.workspace = true
serde_yaml.workspace = true
serde.workspace = true
reqwest = { workspace = true, features = ["blocking", "json"] }
url.workspace = true
semver.workspace = true
conventional_commit_parser.workspace = true
tempdir.workspace = true
git-cliff-core.workspace = true
regex.workspace = true
chrono.workspace = true
lazy_static.workspace = true
parse-changelog.workspace = true
[dev-dependencies]
tracing-test = { workspace = true, features = ["no-env-filter"] }
pretty_assertions.workspace = true

View File

@@ -0,0 +1,61 @@
use std::sync::{Arc, Mutex};
use clap::{Args, ValueEnum};
pub type StdinFn = Option<Arc<Mutex<dyn Fn() -> anyhow::Result<String> + Send + Sync + 'static>>>;
#[derive(Args)]
pub struct GlobalArgs {
/// token is the personal access token from gitea.
#[arg(
env = "CUDDLE_PLEASE_TOKEN",
long,
long_help = "token is the personal access token from gitea. It requires at least repository write access, it isn't required by default, but for most usecases the flow will fail without it",
global = true,
help_heading = "Global"
)]
pub token: Option<String>,
/// whether to run in dry run mode (i.e. no pushes or releases)
#[arg(long, global = true, help_heading = "Global")]
pub dry_run: bool,
/// Inject configuration from stdin
#[arg(
env = "CUDDLE_PLEASE_CONFIG_STDIN",
long,
global = true,
help_heading = "Global",
long_help = "inject via stdin
cat <<EOF | cuddle-please --config-stdin
something
something
something
EOF
config-stdin will consume stdin until the channel is closed via. EOF"
)]
pub config_stdin: bool,
#[arg(
env = "CUDDLE_PLEASE_NO_VCS",
long,
global = true,
help_heading = "Global"
)]
pub no_vcs: bool,
#[arg(
env = "CUDDLE_PLEASE_ENGINE",
long,
global = true,
help_heading = "Global",
default_value = "gitea"
)]
pub engine: RemoteEngine,
}
#[derive(ValueEnum, Clone, Debug)]
pub enum RemoteEngine {
Local,
Gitea,
}

View File

@@ -0,0 +1,531 @@
use anyhow::Context;
use chrono::{DateTime, NaiveDate, Utc};
use git_cliff_core::{
changelog::Changelog,
commit::Commit,
config::{ChangelogConfig, CommitParser, Config, GitConfig},
release::Release,
};
use regex::Regex;
pub struct ChangeLogBuilder {
commits: Vec<String>,
version: String,
config: Option<Config>,
release_date: Option<NaiveDate>,
release_link: Option<String>,
}
impl ChangeLogBuilder {
pub fn new<C>(commits: C, version: impl Into<String>) -> Self
where
C: IntoIterator,
C::Item: AsRef<str>,
{
Self {
commits: commits
.into_iter()
.map(|s| s.as_ref().to_string())
.collect(),
version: version.into(),
config: None,
release_date: None,
release_link: None,
}
}
pub fn with_release_date(self, release_date: NaiveDate) -> Self {
Self {
release_date: Some(release_date),
..self
}
}
pub fn with_release_link(self, release_link: impl Into<String>) -> Self {
Self {
release_link: Some(release_link.into()),
..self
}
}
pub fn with_config(self, config: Config) -> Self {
Self {
config: Some(config),
..self
}
}
pub fn build<'a>(self) -> ChangeLog<'a> {
let git_config = self
.config
.clone()
.map(|c| c.git)
.unwrap_or_else(default_git_config);
let timestamp = self.release_timestamp();
let commits = self
.commits
.clone()
.into_iter()
.map(|c| Commit::new("id".into(), c))
.filter_map(|c| c.process(&git_config).ok())
.collect();
ChangeLog {
release: Release {
version: Some(self.version),
commits,
commit_id: None,
timestamp,
previous: None,
},
config: self.config,
release_link: self.release_link,
}
}
fn release_timestamp(&self) -> i64 {
self.release_date
.and_then(|date| date.and_hms_opt(0, 0, 0))
.map(|d| DateTime::<Utc>::from_utc(d, Utc))
.unwrap_or_else(Utc::now)
.timestamp()
}
}
pub struct ChangeLog<'a> {
release: Release<'a>,
config: Option<Config>,
release_link: Option<String>,
}
impl ChangeLog<'_> {
pub fn generate(&self) -> anyhow::Result<String> {
let config = self.config.clone().unwrap_or_else(|| self.default_config());
let changelog = Changelog::new(vec![self.release.clone()], &config)?;
let mut buffer = Vec::new();
changelog
.generate(&mut buffer)
.context("failed to generate changelog")?;
String::from_utf8(buffer)
.context("cannot convert bytes to string (contains non utf-8 char indices)")
}
pub fn prepend(self, old_changelog: impl Into<String>) -> anyhow::Result<String> {
let old_changelog = old_changelog.into();
if let Ok(Some(last_version)) = changelog_parser::last_version_from_str(&old_changelog) {
let next_version = self
.release
.version
.as_ref()
.context("current release contains no version")?;
if next_version == &last_version {
return Ok(old_changelog);
}
}
let old_header = changelog_parser::parse_header(&old_changelog);
let config = self
.config
.clone()
.unwrap_or_else(|| self.default_config_with_header(old_header));
let changelog = Changelog::new(vec![self.release], &config)?;
let mut out = Vec::new();
changelog.prepend(old_changelog, &mut out)?;
String::from_utf8(out)
.context("cannot convert bytes to string (contains non utf-8 char indices)")
}
fn default_config(&self) -> Config {
let config = Config {
changelog: default_changelog_config(None, self.release_link.as_deref()),
git: default_git_config(),
};
config
}
fn default_config_with_header(&self, header: Option<String>) -> Config {
let config = Config {
changelog: default_changelog_config(header, self.release_link.as_deref()),
git: default_git_config(),
};
config
}
}
fn default_git_config() -> GitConfig {
GitConfig {
conventional_commits: Some(true),
filter_unconventional: Some(false),
filter_commits: Some(true),
commit_parsers: Some(default_commit_parsers()),
..Default::default()
}
}
fn default_commit_parsers() -> Vec<CommitParser> {
fn create_commit_parser(message: &str, group: &str) -> CommitParser {
CommitParser {
message: Regex::new(&format!("^{message}")).ok(),
body: None,
group: Some(group.into()),
default_scope: None,
scope: None,
skip: None,
}
}
vec![
create_commit_parser("feat", "added"),
create_commit_parser("changed", "changed"),
create_commit_parser("deprecated", "deprecated"),
create_commit_parser("removed", "removed"),
create_commit_parser("fix", "fixed"),
create_commit_parser("security", "security"),
CommitParser {
message: Regex::new(".*").ok(),
group: Some(String::from("other")),
body: None,
default_scope: None,
skip: None,
scope: None,
},
]
}
const CHANGELOG_HEADER: &str = r#"# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
"#;
fn default_changelog_config(header: Option<String>, release_link: Option<&str>) -> ChangelogConfig {
ChangelogConfig {
header: Some(header.unwrap_or(String::from(CHANGELOG_HEADER))),
body: Some(default_changelog_body_config(release_link)),
footer: None,
trim: Some(true),
}
}
fn default_changelog_body_config(release_link: Option<&str>) -> String {
const PRE: &str = r#"
## [{{ version | trim_start_matches(pat="v") }}]"#;
const POST: &str = r#" - {{ timestamp | date(format="%Y-%m-%d") }}
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
{%- if commit.scope -%}
- *({{commit.scope}})* {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message }}{%- if commit.links %} ({% for link in commit.links %}[{{link.text}}]({{link.href}}) {% endfor -%}){% endif %}
{% else -%}
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message }}
{% endif -%}
{% endfor -%}
{% endfor %}"#;
match release_link {
Some(link) => format!("{}{}{}", PRE, link, POST),
None => format!("{}{}", PRE, POST),
}
}
pub mod changelog_parser {
use anyhow::Context;
use regex::Regex;
/// Parse the header from a changelog.
/// The changelog header is a string at the begin of the changelog that:
/// - Starts with `# Changelog`, `# CHANGELOG`, or `# changelog`
/// - ends with `## Unreleased`, `## [Unreleased]` or `## ..anything..`
/// (in the ..anything.. case, `## ..anything..` is not included in the header)
pub fn parse_header(changelog: &str) -> Option<String> {
lazy_static::lazy_static! {
static ref FIRST_RE: Regex = Regex::new(r"(?s)^(# Changelog|# CHANGELOG|# changelog)(.*)(## Unreleased|## \[Unreleased\])").unwrap();
static ref SECOND_RE: Regex = Regex::new(r"(?s)^(# Changelog|# CHANGELOG|# changelog)(.*)(\n## )").unwrap();
}
if let Some(captures) = FIRST_RE.captures(changelog) {
return Some(format!("{}\n", &captures[0]));
}
if let Some(captures) = SECOND_RE.captures(changelog) {
return Some(format!("{}{}", &captures[1], &captures[2]));
}
None
}
pub fn last_changes(changelog: &str) -> anyhow::Result<Option<String>> {
last_changes_from_str(changelog)
}
pub fn last_changes_from_str(changelog: &str) -> anyhow::Result<Option<String>> {
let parser = ChangelogParser::new(changelog)?;
let last_release = parser.last_release().map(|r| r.notes.to_string());
Ok(last_release)
}
pub fn last_version_from_str(changelog: &str) -> anyhow::Result<Option<String>> {
let parser = ChangelogParser::new(changelog)?;
let last_release = parser.last_release().map(|r| r.version.to_string());
Ok(last_release)
}
pub fn last_release_from_str(changelog: &str) -> anyhow::Result<Option<ChangelogRelease>> {
let parser = ChangelogParser::new(changelog)?;
let last_release = parser.last_release().map(ChangelogRelease::from_release);
Ok(last_release)
}
pub struct ChangelogRelease {
title: String,
notes: String,
}
impl ChangelogRelease {
fn from_release(release: &parse_changelog::Release) -> Self {
Self {
title: release.title.to_string(),
notes: release.notes.to_string(),
}
}
pub fn title(&self) -> &str {
&self.title
}
pub fn notes(&self) -> &str {
&self.notes
}
}
pub struct ChangelogParser<'a> {
changelog: parse_changelog::Changelog<'a>,
}
impl<'a> ChangelogParser<'a> {
pub fn new(changelog_text: &'a str) -> anyhow::Result<Self> {
let changelog =
parse_changelog::parse(changelog_text).context("can't parse changelog")?;
Ok(Self { changelog })
}
fn last_release(&self) -> Option<&parse_changelog::Release> {
let last_release = release_at(&self.changelog, 0)?;
let last_release = if last_release.version.to_lowercase().contains("unreleased") {
release_at(&self.changelog, 1)?
} else {
last_release
};
Some(last_release)
}
}
fn release_at<'a>(
changelog: &'a parse_changelog::Changelog,
index: usize,
) -> Option<&'a parse_changelog::Release<'a>> {
let release = changelog.get_index(index)?.1;
Some(release)
}
#[cfg(test)]
mod tests {
use super::*;
fn last_changes_from_str_test(changelog: &str) -> String {
last_changes_from_str(changelog).unwrap().unwrap()
}
#[test]
fn changelog_header_is_parsed() {
let changelog = "\
# Changelog
My custom changelog header
## [Unreleased]
";
let header = parse_header(changelog).unwrap();
let expected_header = "\
# Changelog
My custom changelog header
## [Unreleased]
";
assert_eq!(header, expected_header);
}
#[test]
fn changelog_header_without_unreleased_is_parsed() {
let changelog = "\
# Changelog
My custom changelog header
## [0.2.5] - 2022-12-16
";
let header = parse_header(changelog).unwrap();
let expected_header = "\
# Changelog
My custom changelog header
";
assert_eq!(header, expected_header);
}
#[test]
fn changelog_header_with_versions_is_parsed() {
let changelog = "\
# Changelog
My custom changelog header
## [Unreleased]
## [0.2.5] - 2022-12-16
";
let header = parse_header(changelog).unwrap();
let expected_header = "\
# Changelog
My custom changelog header
## [Unreleased]
";
assert_eq!(header, expected_header);
}
#[test]
fn changelog_header_isnt_recognized() {
// A two-level header similar to `## [Unreleased]` is missing
let changelog = "\
# Changelog
My custom changelog header
";
let header = parse_header(changelog);
assert_eq!(header, None);
}
#[test]
fn changelog_with_unreleased_section_is_parsed() {
let changelog = "\
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.2.5] - 2022-12-16
### Added
- Add function to retrieve default branch (#372)
## [0.2.4] - 2022-12-12
### Changed
- improved error message
";
let changes = last_changes_from_str_test(changelog);
let expected_changes = "\
### Added
- Add function to retrieve default branch (#372)";
assert_eq!(changes, expected_changes);
}
#[test]
fn changelog_without_unreleased_section_is_parsed() {
let changelog = "\
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.2.5](https://github.com/MarcoIeni/release-plz/compare/git_cmd-v0.2.4...git_cmd-v0.2.5) - 2022-12-16
### Added
- Add function to retrieve default branch (#372)
## [0.2.4] - 2022-12-12
### Changed
- improved error message
";
let changes = last_changes_from_str_test(changelog);
let expected_changes = "\
### Added
- Add function to retrieve default branch (#372)";
assert_eq!(changes, expected_changes);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bare_release() {
let commits: Vec<&str> = Vec::new();
let changelog = ChangeLogBuilder::new(commits, "0.0.0")
.with_release_date(NaiveDate::from_ymd_opt(1995, 5, 15).unwrap())
.build();
let expected = r######"# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
"######;
pretty_assertions::assert_eq!(expected, &changelog.generate().unwrap())
}
#[test]
fn generates_changelog() {
let commits: Vec<&str> = vec![
"feat: some feature",
"some random commit",
"fix: some fix",
"chore(scope): some chore",
];
let changelog = ChangeLogBuilder::new(commits, "1.0.0")
.with_release_date(NaiveDate::from_ymd_opt(1995, 5, 15).unwrap())
.build();
let expected = r######"# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [1.0.0] - 1995-05-15
### Added
- some feature
### Fixed
- some fix
### Other
- some random commit
- *(scope)* some chore
"######;
pretty_assertions::assert_eq!(expected, &changelog.generate().unwrap())
}
}

View File

@@ -3,7 +3,11 @@ use std::path::{Path, PathBuf};
#[derive(Clone, Debug, PartialEq)]
pub enum VcsClient {
Noop {},
Git { source: PathBuf },
Git {
source: PathBuf,
username: String,
email: String,
},
}
impl VcsClient {
@@ -11,13 +15,23 @@ impl VcsClient {
Self::Noop {}
}
pub fn new_git(path: &Path) -> anyhow::Result<VcsClient> {
pub fn new_git(
path: &Path,
git_username: Option<impl Into<String>>,
git_email: Option<impl Into<String>>,
) -> anyhow::Result<VcsClient> {
if !path.to_path_buf().join(".git").exists() {
anyhow::bail!("git directory not found in: {}", path.display().to_string())
}
Ok(Self::Git {
source: path.to_path_buf(),
username: git_username
.map(|u| u.into())
.unwrap_or("cuddle-please".to_string()),
email: git_email
.map(|e| e.into())
.unwrap_or("bot@cuddle.sh".to_string()),
})
}
@@ -38,9 +52,19 @@ impl VcsClient {
fn exec_git(&self, args: &[&str]) -> anyhow::Result<()> {
match self {
VcsClient::Noop {} => {}
VcsClient::Git { source } => {
VcsClient::Git {
source,
username,
email,
} => {
let checkout_branch = std::process::Command::new("git")
.current_dir(source.as_path())
.args(&[
"-c",
&format!("user.name={}", username),
"-c",
&format!("user.email={}", email),
])
.args(args)
.output()?;

View File

@@ -0,0 +1,635 @@
use anyhow::Context;
use reqwest::header::{HeaderMap, HeaderValue};
use semver::Version;
use serde::{Deserialize, Serialize};
pub trait RemoteGitEngine {
fn connect(&self, owner: &str, repo: &str) -> anyhow::Result<()>;
fn get_tags(&self, owner: &str, repo: &str) -> anyhow::Result<Vec<Tag>>;
fn get_commits_since(
&self,
owner: &str,
repo: &str,
since_sha: Option<&str>,
branch: &str,
) -> anyhow::Result<Vec<Commit>>;
fn get_pull_request(&self, owner: &str, repo: &str) -> anyhow::Result<Option<usize>>;
fn create_pull_request(
&self,
owner: &str,
repo: &str,
version: &str,
body: &str,
base: &str,
) -> anyhow::Result<usize>;
fn update_pull_request(
&self,
owner: &str,
repo: &str,
version: &str,
body: &str,
index: usize,
) -> anyhow::Result<usize>;
fn create_release(
&self,
owner: &str,
repo: &str,
version: &str,
body: &str,
prerelease: bool,
) -> anyhow::Result<Release>;
}
pub type DynRemoteGitClient = Box<dyn RemoteGitEngine>;
#[allow(dead_code)]
pub struct GiteaClient {
url: String,
token: Option<String>,
pub allow_insecure: bool,
}
const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
impl GiteaClient {
pub fn new(url: &str, token: Option<&str>) -> Self {
Self {
url: url.into(),
token: token.map(|t| t.into()),
allow_insecure: false,
}
}
fn create_client(&self) -> anyhow::Result<reqwest::blocking::Client> {
let cb = reqwest::blocking::ClientBuilder::new();
let mut header_map = HeaderMap::new();
if let Some(token) = &self.token {
header_map.insert(
"Authorization",
HeaderValue::from_str(format!("token {}", token).as_str())?,
);
}
let client = cb
.user_agent(APP_USER_AGENT)
.default_headers(header_map)
.danger_accept_invalid_certs(self.allow_insecure)
.build()?;
Ok(client)
}
fn get_commits_since_inner<F>(
&self,
owner: &str,
repo: &str,
since_sha: Option<&str>,
branch: &str,
get_commits: F,
) -> anyhow::Result<Vec<Commit>>
where
F: Fn(&str, &str, &str, usize) -> anyhow::Result<(Vec<Commit>, bool)>,
{
let mut commits = Vec::new();
let mut page = 1;
let owner: String = owner.into();
let repo: String = repo.into();
let since_sha: Option<String> = since_sha.map(|ss| ss.into());
let branch: String = branch.into();
let mut found_commit = false;
loop {
let (new_commits, has_more) = get_commits(&owner, &repo, &branch, page)?;
for commit in new_commits {
if let Some(since_sha) = &since_sha {
if commit.sha.contains(since_sha) {
found_commit = true;
} else if !found_commit {
commits.push(commit);
}
} else {
commits.push(commit);
}
}
if !has_more {
break;
}
page += 1;
}
if !found_commit && since_sha.is_some() {
return Err(anyhow::anyhow!(
"sha was not found in commit chain: {} on branch: {}",
since_sha.unwrap_or("".into()),
branch
));
}
Ok(commits)
}
fn get_pull_request_inner<F>(
&self,
owner: &str,
repo: &str,
request_pull_request: F,
) -> anyhow::Result<Option<usize>>
where
F: Fn(&str, &str, usize) -> anyhow::Result<(Vec<PullRequest>, bool)>,
{
let mut page = 1;
let owner: String = owner.into();
let repo: String = repo.into();
loop {
let (pull_requests, has_more) = request_pull_request(&owner, &repo, page)?;
for pull_request in pull_requests {
if pull_request.head.r#ref.contains("cuddle-please/release") {
return Ok(Some(pull_request.number));
}
}
if !has_more {
break;
}
page += 1;
}
Ok(None)
}
}
impl RemoteGitEngine for GiteaClient {
fn connect(&self, owner: &str, repo: &str) -> anyhow::Result<()> {
let client = self.create_client()?;
tracing::trace!(owner = &owner, repo = &repo, "gitea connect");
let request = client
.get(format!(
"{}/api/v1/repos/{}/{}",
&self.url.trim_end_matches('/'),
owner,
repo
))
.build()?;
let resp = client.execute(request)?;
if !resp.status().is_success() {
resp.error_for_status()?;
return Ok(());
}
Ok(())
}
fn get_tags(&self, owner: &str, repo: &str) -> anyhow::Result<Vec<Tag>> {
let client = self.create_client()?;
let request = client
.get(format!(
"{}/api/v1/repos/{}/{}/tags",
&self.url.trim_end_matches('/'),
owner,
repo
))
.build()?;
let resp = client.execute(request)?;
if !resp.status().is_success() {
return Err(anyhow::anyhow!(resp.error_for_status().unwrap_err()));
}
let tags: Vec<Tag> = resp.json()?;
Ok(tags)
}
fn get_commits_since(
&self,
owner: &str,
repo: &str,
since_sha: Option<&str>,
branch: &str,
) -> anyhow::Result<Vec<Commit>> {
let get_commits_since_page = |owner: &str,
repo: &str,
branch: &str,
page: usize|
-> anyhow::Result<(Vec<Commit>, bool)> {
let client = self.create_client()?;
tracing::trace!(
owner = owner,
repo = repo,
branch = branch,
page = page,
"fetching tags"
);
let request = client
.get(format!(
"{}/api/v1/repos/{}/{}/commits?page={}&limit={}&sha={}&stat=false&verification=false&files=false",
&self.url.trim_end_matches('/'),
owner,
repo,
page,
50,
branch,
))
.build()?;
let resp = client.execute(request)?;
let mut has_more = false;
if let Some(gitea_has_more) = resp.headers().get("X-HasMore") {
let gitea_has_more = gitea_has_more.to_str()?;
if gitea_has_more == "true" || gitea_has_more == "True" {
has_more = true;
}
}
if !resp.status().is_success() {
return Err(anyhow::anyhow!(resp.error_for_status().unwrap_err()));
}
let commits: Vec<Commit> = resp.json()?;
Ok((commits, has_more))
};
let commits =
self.get_commits_since_inner(owner, repo, since_sha, branch, get_commits_since_page)?;
Ok(commits)
}
fn get_pull_request(&self, owner: &str, repo: &str) -> anyhow::Result<Option<usize>> {
let request_pull_request =
|owner: &str, repo: &str, page: usize| -> anyhow::Result<(Vec<PullRequest>, bool)> {
let client = self.create_client()?;
tracing::trace!(owner = owner, repo = repo, "fetching pull-requests");
let request = client
.get(format!(
"{}/api/v1/repos/{}/{}/pulls?state=open&sort=recentupdate&page={}&limit={}",
&self.url.trim_end_matches('/'),
owner,
repo,
page,
50,
))
.build()?;
let resp = client.execute(request)?;
let mut has_more = false;
if let Some(gitea_has_more) = resp.headers().get("X-HasMore") {
let gitea_has_more = gitea_has_more.to_str()?;
if gitea_has_more == "true" || gitea_has_more == "True" {
has_more = true;
}
}
if !resp.status().is_success() {
return Err(anyhow::anyhow!(resp.error_for_status().unwrap_err()));
}
let commits: Vec<PullRequest> = resp.json()?;
Ok((commits, has_more))
};
self.get_pull_request_inner(owner, repo, request_pull_request)
}
fn create_pull_request(
&self,
owner: &str,
repo: &str,
version: &str,
body: &str,
base: &str,
) -> anyhow::Result<usize> {
#[derive(Clone, Debug, Serialize, Deserialize)]
struct CreatePullRequestOption {
base: String,
body: String,
head: String,
title: String,
}
let client = self.create_client()?;
let request = CreatePullRequestOption {
base: base.into(),
body: body.into(),
head: "cuddle-please/release".into(),
title: format!("chore(release): {}", version),
};
tracing::trace!(
owner = owner,
repo = repo,
version = version,
base = base,
"create pull_request"
);
let request = client
.post(format!(
"{}/api/v1/repos/{}/{}/pulls",
&self.url.trim_end_matches('/'),
owner,
repo,
))
.json(&request)
.build()?;
let resp = client.execute(request)?;
if !resp.status().is_success() {
return Err(anyhow::anyhow!(resp.error_for_status().unwrap_err()));
}
let commits: PullRequest = resp.json()?;
Ok(commits.number)
}
fn update_pull_request(
&self,
owner: &str,
repo: &str,
version: &str,
body: &str,
index: usize,
) -> anyhow::Result<usize> {
#[derive(Clone, Debug, Serialize, Deserialize)]
struct CreatePullRequestOption {
body: String,
title: String,
}
let client = self.create_client()?;
let request = CreatePullRequestOption {
body: body.into(),
title: format!("chore(release): {}", version),
};
tracing::trace!(
owner = owner,
repo = repo,
version = version,
"update pull_request"
);
let request = client
.patch(format!(
"{}/api/v1/repos/{}/{}/pulls/{}",
&self.url.trim_end_matches('/'),
owner,
repo,
index
))
.json(&request)
.build()?;
let resp = client.execute(request)?;
if !resp.status().is_success() {
return Err(anyhow::anyhow!(resp.error_for_status().unwrap_err()));
}
let commits: PullRequest = resp.json()?;
Ok(commits.number)
}
fn create_release(
&self,
owner: &str,
repo: &str,
version: &str,
body: &str,
prerelease: bool,
) -> anyhow::Result<Release> {
#[derive(Clone, Debug, Serialize, Deserialize)]
struct CreateReleaseOption {
body: String,
draft: bool,
name: String,
prerelease: bool,
#[serde(alias = "tag_name")]
tag_name: String,
}
let client = self.create_client()?;
let request = CreateReleaseOption {
body: body.into(),
draft: false,
name: version.into(),
prerelease,
tag_name: version.into(),
};
tracing::trace!(
owner = owner,
repo = repo,
version = version,
"create release"
);
let request = client
.post(format!(
"{}/api/v1/repos/{}/{}/releases",
&self.url.trim_end_matches('/'),
owner,
repo,
))
.json(&request)
.build()?;
let resp = client.execute(request)?;
if !resp.status().is_success() {
return Err(anyhow::anyhow!(resp.error_for_status().unwrap_err()));
}
let release: Release = resp.json()?;
Ok(release)
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct Release {
id: usize,
url: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct PullRequest {
number: usize,
head: PRBranchInfo,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct PRBranchInfo {
#[serde(alias = "ref")]
r#ref: String,
label: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct Commit {
sha: String,
pub created: String,
pub commit: CommitDetails,
}
impl Commit {
pub fn get_title(&self) -> String {
self.commit
.message
.split('\n')
.take(1)
.collect::<Vec<&str>>()
.join("\n")
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CommitDetails {
pub message: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct Tag {
pub id: String,
pub message: String,
pub name: String,
pub commit: TagCommit,
}
impl TryFrom<Tag> for Version {
type Error = anyhow::Error;
fn try_from(value: Tag) -> Result<Self, Self::Error> {
tracing::trace!(name = &value.name, "parsing tag into version");
value
.name
.parse::<Version>()
.context("could not get version from tag")
}
}
impl TryFrom<&Tag> for Version {
type Error = anyhow::Error;
fn try_from(value: &Tag) -> Result<Self, Self::Error> {
tracing::trace!(name = &value.name, "parsing tag into version");
value
.name
.parse::<Version>()
.context("could not get version from tag")
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct TagCommit {
pub created: String,
pub sha: String,
pub url: String,
}
#[cfg(test)]
mod test {
use tracing_test::traced_test;
use crate::gitea_client::{Commit, CommitDetails};
use super::GiteaClient;
fn get_api_res() -> Vec<Vec<Commit>> {
let api_results = vec![
vec![Commit {
sha: "first-sha".into(),
created: "".into(),
commit: CommitDetails {
message: "first-message".into(),
},
}],
vec![Commit {
sha: "second-sha".into(),
created: "".into(),
commit: CommitDetails {
message: "second-message".into(),
},
}],
vec![Commit {
sha: "third-sha".into(),
created: "".into(),
commit: CommitDetails {
message: "third-message".into(),
},
}],
];
api_results
}
fn get_commits(sha: String) -> anyhow::Result<(Vec<Vec<Commit>>, Vec<Commit>)> {
let api_res = get_api_res();
let client = GiteaClient::new("", Some(""));
let commits = client.get_commits_since_inner(
"owner",
"repo",
Some(&sha),
"some-branch",
|_, _, _, page| -> anyhow::Result<(Vec<Commit>, bool)> {
let commit_page = api_res.get(page - 1).unwrap();
Ok((commit_page.clone(), page != 3))
},
)?;
Ok((api_res, commits))
}
#[test]
#[traced_test]
fn finds_tag_in_list() {
let (expected, actual) = get_commits("second-sha".into()).unwrap();
assert_eq!(
expected.get(0).unwrap().clone().as_slice(),
actual.as_slice()
);
}
#[test]
#[traced_test]
fn finds_tag_in_list_already_newest_commit() {
let (_, actual) = get_commits("first-sha".into()).unwrap();
assert_eq!(0, actual.len());
}
#[test]
#[traced_test]
fn finds_tag_in_list_is_base() {
let (expected, actual) = get_commits("third-sha".into()).unwrap();
assert_eq!(expected[0..=1].concat().as_slice(), actual.as_slice());
}
#[test]
#[traced_test]
fn finds_didnt_find_tag_in_list() {
let error = get_commits("not-found-sha".into()).unwrap_err();
assert_eq!(
"sha was not found in commit chain: not-found-sha on branch: some-branch",
error.to_string()
);
}
}

View File

@@ -0,0 +1,15 @@
mod args;
mod cliff;
mod git_client;
mod gitea_client;
mod local_git_client;
mod ui;
mod versioning;
pub use args::{GlobalArgs, RemoteEngine, StdinFn};
pub use cliff::{changelog_parser, ChangeLogBuilder};
pub use git_client::VcsClient;
pub use gitea_client::{Commit, DynRemoteGitClient, GiteaClient, RemoteGitEngine, Tag};
pub use local_git_client::LocalGitClient;
pub use ui::{ConsoleUi, DynUi, Ui};
pub use versioning::{next_version::NextVersion, semver::get_most_significant_version};

View File

@@ -0,0 +1,66 @@
use crate::RemoteGitEngine;
pub struct LocalGitClient {}
impl LocalGitClient {
pub fn new() -> Self {
Self {}
}
}
impl RemoteGitEngine for LocalGitClient {
fn connect(&self, _owner: &str, _repo: &str) -> anyhow::Result<()> {
todo!()
}
fn get_tags(&self, _owner: &str, _repo: &str) -> anyhow::Result<Vec<crate::gitea_client::Tag>> {
todo!()
}
fn get_commits_since(
&self,
_owner: &str,
_repo: &str,
_since_sha: Option<&str>,
_branch: &str,
) -> anyhow::Result<Vec<crate::gitea_client::Commit>> {
todo!()
}
fn get_pull_request(&self, _owner: &str, _repo: &str) -> anyhow::Result<Option<usize>> {
todo!()
}
fn create_pull_request(
&self,
_owner: &str,
_repo: &str,
_version: &str,
_body: &str,
_base: &str,
) -> anyhow::Result<usize> {
todo!()
}
fn update_pull_request(
&self,
_owner: &str,
_repo: &str,
_version: &str,
_body: &str,
_index: usize,
) -> anyhow::Result<usize> {
todo!()
}
fn create_release(
&self,
_owner: &str,
_repo: &str,
_version: &str,
_body: &str,
_prerelease: bool,
) -> anyhow::Result<crate::gitea_client::Release> {
todo!()
}
}

View File

@@ -10,11 +10,12 @@ pub type DynUi = Box<dyn Ui + Send + Sync>;
impl Default for DynUi {
fn default() -> Self {
Box::new(ConsoleUi::default())
Box::<ConsoleUi>::default()
}
}
pub(crate) struct ConsoleUi {}
#[derive(Default)]
pub struct ConsoleUi {}
#[allow(dead_code)]
impl ConsoleUi {
@@ -23,12 +24,6 @@ impl ConsoleUi {
}
}
impl Default for ConsoleUi {
fn default() -> Self {
Self {}
}
}
impl From<ConsoleUi> for DynUi {
fn from(value: ConsoleUi) -> Self {
Box::new(value)

View File

@@ -16,16 +16,14 @@ impl VersionIncrement {
C::Item: AsRef<str>,
{
let mut commits = commits.into_iter().peekable();
if commits.peek().is_none() {
return None;
}
commits.peek()?;
if let Some(prerelease) = Self::is_prerelease(cur_version) {
return Some(prerelease);
}
let commits: Vec<ConventionalCommit> = Self::parse_commits::<C>(commits);
return Some(Self::from_conventional_commits(commits));
Some(Self::from_conventional_commits(commits))
}
#[inline]
@@ -71,8 +69,6 @@ mod tests {
use semver::Version;
use tracing_test::traced_test;
#[test]
#[traced_test]
fn is_prerelease() {
@@ -186,6 +182,6 @@ mod tests {
let commits: Vec<&str> = Vec::new();
let actual = VersionIncrement::from(&version, commits).is_none();
assert_eq!(true, actual);
assert!(actual);
}
}

View File

@@ -40,7 +40,7 @@ impl NextVersion for Version {
VersionIncrement::Prerelease => Self {
pre: {
let release = &self.pre;
let release_version = match release.rsplit_once(".") {
let release_version = match release.rsplit_once('.') {
Some((tag, version)) => match version.parse::<usize>() {
Ok(version) => format!("{tag}.{}", version + 1),
Err(_) => format!("{tag}.1"),

View File

@@ -1,13 +1,13 @@
use std::cmp::Reverse;
use crate::gitea_client::{Tag};
use crate::gitea_client::Tag;
use semver::Version;
pub fn get_most_significant_version<'a>(tags: Vec<&'a Tag>) -> Option<&'a Tag> {
let mut versions: Vec<(&'a Tag, Version)> = tags
.into_iter()
.filter_map(|c| {
if let Some(version) = c.name.trim_start_matches("v").parse::<Version>().ok() {
if let Ok(version) = c.name.trim_start_matches('v').parse::<Version>() {
Some((c, version))
} else {
None
@@ -16,7 +16,13 @@ pub fn get_most_significant_version<'a>(tags: Vec<&'a Tag>) -> Option<&'a Tag> {
.collect();
versions.sort_unstable_by_key(|(_, version)| Reverse(version.clone()));
versions.first().map(|(tag, _)| *tag)
let tag = versions.first().map(|(tag, _)| *tag);
if let Some(tag) = tag {
tracing::trace!(name = &tag.name, "found most significant tag with version");
}
tag
}
#[cfg(test)]

View File

@@ -4,6 +4,10 @@ version = "0.1.0"
edition = "2021"
[dependencies]
cuddle-please-frontend.workspace = true
cuddle-please-commands.workspace = true
cuddle-please-misc.workspace = true
anyhow.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
@@ -19,6 +23,8 @@ tempdir.workspace = true
git-cliff-core.workspace = true
regex.workspace = true
chrono.workspace = true
lazy_static.workspace = true
parse-changelog.workspace = true
[dev-dependencies]
tracing-test = { workspace = true, features = ["no-env-filter"] }

View File

@@ -1,260 +0,0 @@
use anyhow::Context;
use chrono::{DateTime, NaiveDate, Utc};
use git_cliff_core::{
changelog::Changelog,
commit::Commit,
config::{ChangelogConfig, CommitParser, Config, GitConfig},
release::Release,
};
use regex::Regex;
pub struct ChangeLogBuilder {
commits: Vec<String>,
version: String,
config: Option<Config>,
release_date: Option<NaiveDate>,
release_link: Option<String>,
}
impl ChangeLogBuilder {
pub fn new<C>(commits: C, version: impl Into<String>) -> Self
where
C: IntoIterator,
C::Item: Into<String>,
{
Self {
commits: commits.into_iter().map(|s| s.into()).collect(),
version: version.into(),
config: None,
release_date: None,
release_link: None,
}
}
pub fn with_release_date(self, release_date: NaiveDate) -> Self {
Self {
release_date: Some(release_date),
..self
}
}
pub fn with_release_link(self, release_link: impl Into<String>) -> Self {
Self {
release_link: Some(release_link.into()),
..self
}
}
pub fn with_config(self, config: Config) -> Self {
Self {
config: Some(config),
..self
}
}
pub fn build<'a>(self) -> ChangeLog<'a> {
let git_config = self
.config
.clone()
.map(|c| c.git)
.unwrap_or_else(default_git_config);
let timestamp = self.release_timestamp();
let commits = self
.commits
.clone()
.into_iter()
.map(|c| Commit::new("id".into(), c))
.filter_map(|c| c.process(&git_config).ok())
.collect();
ChangeLog {
release: Release {
version: Some(self.version),
commits,
commit_id: None,
timestamp,
previous: None,
},
config: self.config,
release_link: self.release_link,
}
}
fn release_timestamp(&self) -> i64 {
self.release_date
.and_then(|date| date.and_hms_opt(0, 0, 0))
.map(|d| DateTime::<Utc>::from_utc(d, Utc))
.unwrap_or_else(Utc::now)
.timestamp()
}
}
pub struct ChangeLog<'a> {
release: Release<'a>,
config: Option<Config>,
release_link: Option<String>,
}
impl ChangeLog<'_> {
pub fn generate(&self) -> anyhow::Result<String> {
let config = self.config.clone().unwrap_or_else(|| self.default_config());
let changelog = Changelog::new(vec![self.release.clone()], &config)?;
let mut buffer = Vec::new();
changelog
.generate(&mut buffer)
.context("failed to generate changelog")?;
String::from_utf8(buffer)
.context("cannot convert bytes to string (contains non utf-8 char indices)")
}
fn default_config<'a>(&self) -> Config {
let config = Config {
changelog: default_changelog_config(
None,
self.release_link.as_ref().map(|rl| rl.as_str()),
),
git: default_git_config(),
};
config
}
}
fn default_git_config() -> GitConfig {
GitConfig {
conventional_commits: Some(true),
filter_unconventional: Some(false),
filter_commits: Some(true),
commit_parsers: Some(default_commit_parsers()),
..Default::default()
}
}
fn default_commit_parsers() -> Vec<CommitParser> {
fn create_commit_parser(message: &str, group: &str) -> CommitParser {
CommitParser {
message: Regex::new(&format!("^{message}")).ok(),
body: None,
group: Some(group.into()),
default_scope: None,
scope: None,
skip: None,
}
}
vec![
create_commit_parser("feat", "added"),
create_commit_parser("changed", "changed"),
create_commit_parser("deprecated", "deprecated"),
create_commit_parser("removed", "removed"),
create_commit_parser("fix", "fixed"),
create_commit_parser("security", "security"),
CommitParser {
message: Regex::new(".*").ok(),
group: Some(String::from("other")),
body: None,
default_scope: None,
skip: None,
scope: None,
},
]
}
const CHANGELOG_HEADER: &'static str = r#"# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
"#;
fn default_changelog_config(header: Option<String>, release_link: Option<&str>) -> ChangelogConfig {
ChangelogConfig {
header: Some(header.unwrap_or(String::from(CHANGELOG_HEADER))),
body: Some(default_changelog_body_config(release_link)),
footer: None,
trim: Some(true),
}
}
fn default_changelog_body_config(release_link: Option<&str>) -> String {
const pre: &'static str = r#"
## [{{ version | trim_start_matches(pat="v") }}]"#;
const post: &'static str = r#" - {{ timestamp | date(format="%Y-%m-%d") }}
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
{%- if commit.scope -%}
- *({{commit.scope}})* {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message }}{%- if commit.links %} ({% for link in commit.links %}[{{link.text}}]({{link.href}}) {% endfor -%}){% endif %}
{% else -%}
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message }}
{% endif -%}
{% endfor -%}
{% endfor %}"#;
match release_link {
Some(link) => format!("{}{}{}", pre, link, post),
None => format!("{}{}", pre, post),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bare_release() {
let commits: Vec<&str> = Vec::new();
let changelog = ChangeLogBuilder::new(commits, "0.0.0")
.with_release_date(NaiveDate::from_ymd_opt(1995, 5, 15).unwrap())
.build();
let expected = r######"# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
"######;
pretty_assertions::assert_eq!(expected, &changelog.generate().unwrap())
}
#[test]
fn generates_changelog() {
let commits: Vec<&str> = vec![
"feat: some feature",
"some random commit",
"fix: some fix",
"chore(scope): some chore",
];
let changelog = ChangeLogBuilder::new(commits, "1.0.0")
.with_release_date(NaiveDate::from_ymd_opt(1995, 5, 15).unwrap())
.build();
let expected = r######"# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [1.0.0] - 1995-05-15
### Added
- some feature
### Fixed
- some fix
### Other
- some random commit
- *(scope)* some chore
"######;
pretty_assertions::assert_eq!(expected, &changelog.generate().unwrap())
}
}

View File

@@ -1,477 +0,0 @@
use std::{
io::Read,
ops::Deref,
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
use anyhow::Context;
use clap::{Args, Parser, Subcommand};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use crate::{
environment::get_from_environment,
git_client::VcsClient,
gitea_client::GiteaClient,
ui::{ConsoleUi, DynUi},
versioning::semver::get_most_significant_version,
};
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
pub struct Command {
#[command(flatten)]
global: GlobalArgs,
#[command(subcommand)]
commands: Option<Commands>,
#[clap(skip)]
ui: DynUi,
#[clap(skip)]
stdin: Option<Arc<Mutex<dyn Fn() -> anyhow::Result<String> + Send + Sync + 'static>>>,
}
#[derive(Args)]
struct GlobalArgs {
/// token is the personal access token from gitea.
#[arg(
env = "CUDDLE_PLEASE_TOKEN",
long,
long_help = "token is the personal access token from gitea. It requires at least repository write access, it isn't required by default, but for most usecases the flow will fail without it",
global = true,
help_heading = "Global"
)]
token: Option<String>,
/// Which repository to publish against. If not supplied remote url will be inferred from environment or fail if not present.
#[arg(long, global = true, help_heading = "Global")]
api_url: Option<String>,
/// repo is the name of repository you want to release for
#[arg(long, global = true, help_heading = "Global")]
repo: Option<String>,
/// owner is the name of user from which the repository belongs <user>/<repo>
#[arg(long, global = true, help_heading = "Global")]
owner: Option<String>,
/// which source directory to use, if not set `std::env::current_dir` is used instead.
#[arg(long, global = true, help_heading = "Global")]
source: Option<PathBuf>,
/// no version control system, forces please to allow no .git/ or friends
#[arg(
long,
global = true,
help_heading = "Global",
long_help = "no version control system. This forces cuddle-please to accept that it won't be running in git. All fields will have to be fed through values in the given commands."
)]
no_vcs: bool,
/// Inject configuration from stdin
#[arg(
long,
global = true,
help_heading = "Global",
long_help = "inject via stdin
cat <<EOF | cuddle-please --config-stdin
something
something
something
EOF
config-stdin will consume stdin until the channel is closed via. EOF"
)]
config_stdin: bool,
}
impl Command {
pub fn new() -> Self {
let args = std::env::args();
Self::new_from_args_with_stdin(Some(ConsoleUi::default()), args, || {
let mut input = String::new();
std::io::stdin().read_to_string(&mut input)?;
Ok(input)
})
}
pub fn new_from_args<I, T, UIF>(ui: Option<UIF>, i: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
UIF: Into<DynUi>,
{
let mut s = Self::parse_from(i);
if let Some(ui) = ui {
s.ui = ui.into();
}
s
}
pub fn new_from_args_with_stdin<I, T, F, UIF>(ui: Option<UIF>, i: I, input: F) -> Self
where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
F: Fn() -> anyhow::Result<String> + Send + Sync + 'static,
UIF: Into<DynUi>,
{
let mut s = Self::parse_from(i);
if let Some(ui) = ui {
s.ui = ui.into();
}
s.stdin = Some(Arc::new(Mutex::new(input)));
s
}
fn get_config(
&self,
current_dir: &Path,
stdin: Option<String>,
) -> anyhow::Result<PleaseConfig> {
let mut config = get_config(current_dir, stdin)?;
self.get_from_environment(&mut config)?;
Ok(config)
}
pub fn execute(self, current_dir: Option<&Path>) -> anyhow::Result<()> {
// 1. Parse the current directory
let current_dir = get_current_path(current_dir, self.global.source.clone())?;
let stdin = if self.global.config_stdin {
if let Some(stdin_fn) = self.stdin.clone() {
let output = (stdin_fn.lock().unwrap().deref())();
Some(output.unwrap())
} else {
None
}
} else {
None
};
match &self.commands {
Some(Commands::Config { command }) => match command {
ConfigCommand::List { .. } => {
tracing::debug!("running command: config list");
let _config = self.get_config(current_dir.as_path(), stdin)?;
self.ui.write_str_ln(&format!("cuddle-config"));
}
},
Some(Commands::Gitea { command }) => {
let git_url = url::Url::parse(&self.global.api_url.unwrap())?;
let mut url = String::new();
url.push_str(git_url.scheme());
url.push_str("://");
url.push_str(&git_url.host().unwrap().to_string());
if let Some(port) = git_url.port() {
url.push_str(format!(":{port}").as_str());
}
let client = GiteaClient::new(url, self.global.token);
match command {
GiteaCommand::Connect {} => {
client.connect(self.global.owner.unwrap(), self.global.repo.unwrap())?;
self.ui.write_str_ln("connected succesfully go gitea");
}
GiteaCommand::Tags { command } => match command {
Some(GiteaTagsCommand::MostSignificant {}) => {
let tags = client
.get_tags(self.global.owner.unwrap(), self.global.repo.unwrap())?;
match get_most_significant_version(tags.iter().collect()) {
Some(tag) => {
self.ui.write_str_ln(&format!(
"found most significant tags: {}",
tag.name
));
}
None => {
self.ui.write_str_ln("found no tags with versioning schema");
}
}
}
None => {
let tags = client
.get_tags(self.global.owner.unwrap(), self.global.repo.unwrap())?;
self.ui.write_str_ln("got tags from gitea");
for tag in tags {
self.ui.write_str_ln(&format!("- {}", tag.name))
}
}
},
GiteaCommand::SinceCommit { sha, branch } => {
let commits = client.get_commits_since(
self.global.owner.unwrap(),
self.global.repo.unwrap(),
sha,
branch,
)?;
self.ui.write_str_ln("got commits from gitea");
for commit in commits {
self.ui.write_str_ln(&format!("- {}", commit.get_title()))
}
}
}
}
Some(Commands::Doctor {}) => {
match std::process::Command::new("git").arg("-v").output() {
Ok(o) => {
let stdout = std::str::from_utf8(&o.stdout).unwrap_or("".into());
self.ui.write_str_ln(&format!("OK: {}", stdout));
}
Err(e) => {
self.ui.write_str_ln(&format!(
"WARNING: git is not installed: {}",
e.to_string()
));
}
}
}
None => {
tracing::debug!("running bare command");
// 2. Parse the cuddle.please.yaml let cuddle.please.yaml take precedence
// 2a. if not existing use default.
// 2b. if not in a git repo abort. (unless --no-vcs is turned added)
let _config = self.get_config(&current_dir, stdin)?;
let _git_client = self.get_git(&current_dir)?;
// 3. Create gitea client and do a health check
// 4. Fetch git tags for the current repository
// 5. Fetch git commits since last git tag
// 6. Slice commits since last git tag
// 7. Create a versioning client
// 8. Parse conventional commits and determine next version
// 9a. Check for open pr.
// 10a. If exists parse history, rebase from master and rewrite pr
// 9b. check for release commit and release, if release exists continue
// 10b. create release
}
}
Ok(())
}
fn get_git(&self, current_dir: &PathBuf) -> anyhow::Result<VcsClient> {
if self.global.no_vcs {
Ok(VcsClient::new_noop())
} else {
VcsClient::new_git(current_dir)
}
}
fn get_from_environment(&self, config: &mut PleaseConfig) -> anyhow::Result<()> {
let input_config = get_from_environment();
config.merge_mut(input_config);
Ok(())
}
}
#[derive(Debug, Clone, Subcommand)]
enum Commands {
/// Config is mostly used for debugging the final config output
Config {
#[command(subcommand)]
command: ConfigCommand,
},
Gitea {
#[command(subcommand)]
command: GiteaCommand,
},
/// Helps you identify missing things from your execution environment for cuddle-please to function as intended
Doctor {},
}
#[derive(Subcommand, Debug, Clone)]
enum ConfigCommand {
/// List will list the final configuration
List {},
}
#[derive(Subcommand, Debug, Clone)]
enum GiteaCommand {
Connect {},
Tags {
#[command(subcommand)]
command: Option<GiteaTagsCommand>,
},
SinceCommit {
#[arg(long)]
sha: String,
#[arg(long)]
branch: String,
},
}
#[derive(Subcommand, Debug, Clone)]
enum GiteaTagsCommand {
MostSignificant {},
}
fn get_current_path(
optional_current_dir: Option<&Path>,
optional_source_path: Option<PathBuf>,
) -> anyhow::Result<PathBuf> {
let path = optional_source_path
.or_else(|| optional_current_dir.map(|p| p.to_path_buf())) // fall back on current env from environment
.filter(|v| v.to_string_lossy() != "") // make sure we don't get empty values
//.and_then(|p| p.canonicalize().ok()) // Make sure we get the absolute path
.context("could not find current dir, pass --source as a replacement")?;
if !path.exists() {
anyhow::bail!("path doesn't exist {}", path.display());
}
Ok(path)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PleaseProjectConfig {
pub owner: Option<String>,
pub repository: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PleaseSettingsConfig {
pub api_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PleaseConfig {
pub project: Option<PleaseProjectConfig>,
pub settings: Option<PleaseSettingsConfig>,
}
impl PleaseConfig {
fn merge(self, _config: PleaseConfig) -> Self {
self
}
fn merge_mut(&mut self, _config: PleaseConfig) -> &mut Self {
self
}
}
impl Default for PleaseConfig {
fn default() -> Self {
Self {
project: None,
settings: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct CuddleEmbeddedPleaseConfig {
please: PleaseConfig,
}
impl From<CuddleEmbeddedPleaseConfig> for PleaseConfig {
fn from(value: CuddleEmbeddedPleaseConfig) -> Self {
value.please
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct CuddlePleaseConfig {
#[serde(flatten)]
please: PleaseConfig,
}
impl From<CuddlePleaseConfig> for PleaseConfig {
fn from(value: CuddlePleaseConfig) -> Self {
value.please
}
}
const CUDDLE_FILE_NAME: &'static str = "cuddle";
const CUDDLE_CONFIG_FILE_NAME: &'static str = "cuddle.please";
const YAML_EXTENSION: &'static str = "yaml";
fn get_config(current_dir: &Path, stdin: Option<String>) -> anyhow::Result<PleaseConfig> {
let current_cuddle_path = current_dir
.clone()
.join(format!("{CUDDLE_FILE_NAME}.{YAML_EXTENSION}"));
let current_cuddle_config_path = current_dir
.clone()
.join(format!("{CUDDLE_CONFIG_FILE_NAME}.{YAML_EXTENSION}"));
let mut please_config = PleaseConfig::default();
if let Some(config) = get_config_from_file::<CuddleEmbeddedPleaseConfig>(current_cuddle_path) {
please_config = please_config.merge(config);
}
if let Some(config) = get_config_from_file::<CuddlePleaseConfig>(current_cuddle_config_path) {
please_config = please_config.merge(config);
}
if let Some(input_config) = get_config_from_stdin::<CuddlePleaseConfig>(stdin.as_ref()) {
please_config = please_config.merge(input_config);
}
Ok(please_config)
}
fn get_config_from_file<'d, T>(current_cuddle_path: PathBuf) -> Option<PleaseConfig>
where
T: DeserializeOwned,
T: Into<PleaseConfig>,
{
match std::fs::File::open(&current_cuddle_path) {
Ok(file) => match serde_yaml::from_reader::<_, T>(file) {
Ok(config) => {
return Some(config.into());
}
Err(e) => {
tracing::debug!(
"{} doesn't contain a valid please config: {}",
&current_cuddle_path.display(),
e
);
}
},
Err(e) => {
tracing::debug!(
"did not find or was not allowed to read {}, error: {}",
&current_cuddle_path.display(),
e,
);
}
}
None
}
fn get_config_from_stdin<'d, T>(stdin: Option<&'d String>) -> Option<PleaseConfig>
where
T: Deserialize<'d>,
T: Into<PleaseConfig>,
{
match stdin {
Some(content) => match serde_yaml::from_str::<'d, T>(&content) {
Ok(config) => {
return Some(config.into());
}
Err(e) => {
tracing::debug!("stdin doesn't contain a valid please config: {}", e);
}
},
None => {
tracing::trace!("Stdin was not set continueing",);
}
}
None
}

View File

@@ -1,38 +0,0 @@
use crate::command::{PleaseConfig, PleaseProjectConfig};
pub mod drone;
pub fn get_from_environment() -> PleaseConfig {
let env = detect_environment();
match env {
ExecutionEnvironment::Local => PleaseConfig {
project: None,
settings: None,
},
ExecutionEnvironment::Drone => PleaseConfig {
project: Some(PleaseProjectConfig {
owner: Some(
std::env::var("DRONE_REPO_OWNER").expect("DRONE_REPO_OWNER to be present"),
),
repository: Some(
std::env::var("DRONE_REPO_NAME").expect("DRONE_REPO_NAME to be present"),
),
}),
settings: None,
},
}
}
pub fn detect_environment() -> ExecutionEnvironment {
if let Some(_) = std::env::var("DRONE").ok() {
return ExecutionEnvironment::Drone;
}
return ExecutionEnvironment::Local;
}
pub enum ExecutionEnvironment {
Local,
Drone,
}

View File

@@ -1,326 +0,0 @@
use reqwest::header::{HeaderMap, HeaderValue};
use serde::{Deserialize, Serialize};
#[allow(dead_code)]
pub struct GiteaClient {
url: String,
token: Option<String>,
pub allow_insecure: bool,
}
const APP_USER_AGENT: &'static str =
concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
impl GiteaClient {
pub fn new(url: impl Into<String>, token: Option<impl Into<String>>) -> Self {
Self {
url: url.into(),
token: token.map(|t| t.into()),
allow_insecure: false,
}
}
fn create_client(&self) -> anyhow::Result<reqwest::blocking::Client> {
let cb = reqwest::blocking::ClientBuilder::new();
let mut header_map = HeaderMap::new();
if let Some(token) = &self.token {
header_map.insert(
"Authorization",
HeaderValue::from_str(format!("token {}", token).as_str())?,
);
}
let client = cb
.user_agent(APP_USER_AGENT)
.default_headers(header_map)
.danger_accept_invalid_certs(self.allow_insecure)
.build()?;
Ok(client)
}
pub fn connect(&self, owner: impl Into<String>, repo: impl Into<String>) -> anyhow::Result<()> {
let client = self.create_client()?;
let request = client
.get(format!(
"{}/api/v1/repos/{}/{}",
&self.url.trim_end_matches("/"),
owner.into(),
repo.into()
))
.build()?;
let resp = client.execute(request)?;
if !resp.status().is_success() {
resp.error_for_status()?;
return Ok(());
}
Ok(())
}
pub fn get_tags(
&self,
owner: impl Into<String>,
repo: impl Into<String>,
) -> anyhow::Result<Vec<Tag>> {
let client = self.create_client()?;
let request = client
.get(format!(
"{}/api/v1/repos/{}/{}/tags",
&self.url.trim_end_matches("/"),
owner.into(),
repo.into()
))
.build()?;
let resp = client.execute(request)?;
if !resp.status().is_success() {
return Err(anyhow::anyhow!(resp.error_for_status().unwrap_err()));
}
let tags: Vec<Tag> = resp.json()?;
Ok(tags)
}
pub fn get_commits_since(
&self,
owner: impl Into<String>,
repo: impl Into<String>,
since_sha: impl Into<String>,
branch: impl Into<String>,
) -> anyhow::Result<Vec<Commit>> {
let get_commits_since_page = |owner: &str,
repo: &str,
branch: &str,
page: usize|
-> anyhow::Result<(Vec<Commit>, bool)> {
let client = self.create_client()?;
tracing::trace!(
owner = owner,
repo = repo,
branch = branch,
page = page,
"fetching tags"
);
let request = client
.get(format!(
"{}/api/v1/repos/{}/{}/commits?page={}&limit={}&sha={}&stat=false&verification=false&files=false",
&self.url.trim_end_matches("/"),
owner,
repo,
page,
50,
branch,
))
.build()?;
let resp = client.execute(request)?;
let mut has_more = false;
if let Some(gitea_has_more) = resp.headers().get("X-HasMore") {
let gitea_has_more = gitea_has_more.to_str()?;
if gitea_has_more == "true" || gitea_has_more == "True" {
has_more = true;
}
}
if !resp.status().is_success() {
return Err(anyhow::anyhow!(resp.error_for_status().unwrap_err()));
}
let commits: Vec<Commit> = resp.json()?;
Ok((commits, has_more))
};
let commits =
self.get_commits_since_inner(owner, repo, since_sha, branch, get_commits_since_page)?;
Ok(commits)
}
fn get_commits_since_inner<F>(
&self,
owner: impl Into<String>,
repo: impl Into<String>,
since_sha: impl Into<String>,
branch: impl Into<String>,
get_commits: F,
) -> anyhow::Result<Vec<Commit>>
where
F: Fn(&str, &str, &str, usize) -> anyhow::Result<(Vec<Commit>, bool)>,
{
let mut commits = Vec::new();
let mut page = 1;
let owner: String = owner.into();
let repo: String = repo.into();
let since_sha: String = since_sha.into();
let branch: String = branch.into();
let mut found_commit = false;
loop {
let (new_commits, has_more) = get_commits(&owner, &repo, &branch, page)?;
for commit in new_commits {
if commit.sha.contains(&since_sha) {
found_commit = true;
} else {
if !found_commit {
commits.push(commit);
}
}
}
if !has_more {
break;
}
page += 1;
}
if found_commit == false {
return Err(anyhow::anyhow!(
"sha was not found in commit chain: {} on branch: {}",
since_sha,
branch
));
}
Ok(commits)
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct Commit {
sha: String,
pub created: String,
pub commit: CommitDetails,
}
impl Commit {
pub fn get_title(&self) -> String {
self.commit
.message
.split("\n")
.take(1)
.collect::<Vec<&str>>()
.join("\n")
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct CommitDetails {
pub message: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct Tag {
pub id: String,
pub message: String,
pub name: String,
pub commit: TagCommit,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct TagCommit {
pub created: String,
pub sha: String,
pub url: String,
}
#[cfg(test)]
mod test {
use tracing_test::traced_test;
use crate::gitea_client::{Commit, CommitDetails};
use super::GiteaClient;
fn get_api_res() -> Vec<Vec<Commit>> {
let api_results = vec![
vec![Commit {
sha: "first-sha".into(),
created: "".into(),
commit: CommitDetails {
message: "first-message".into(),
},
}],
vec![Commit {
sha: "second-sha".into(),
created: "".into(),
commit: CommitDetails {
message: "second-message".into(),
},
}],
vec![Commit {
sha: "third-sha".into(),
created: "".into(),
commit: CommitDetails {
message: "third-message".into(),
},
}],
];
api_results
}
fn get_commits(sha: String) -> anyhow::Result<(Vec<Vec<Commit>>, Vec<Commit>)> {
let api_res = get_api_res();
let client = GiteaClient::new("", Some(""));
let commits = client.get_commits_since_inner(
"owner",
"repo",
sha,
"some-branch",
|_, _, _, page| -> anyhow::Result<(Vec<Commit>, bool)> {
let commit_page = api_res.get(page - 1).unwrap();
Ok((commit_page.clone(), page != 3))
},
)?;
Ok((api_res, commits))
}
#[test]
#[traced_test]
fn finds_tag_in_list() {
let (expected, actual) = get_commits("second-sha".into()).unwrap();
assert_eq!(
expected.get(0).unwrap().clone().as_slice(),
actual.as_slice()
);
}
#[test]
#[traced_test]
fn finds_tag_in_list_already_newest_commit() {
let (_, actual) = get_commits("first-sha".into()).unwrap();
assert_eq!(0, actual.len());
}
#[test]
#[traced_test]
fn finds_tag_in_list_is_base() {
let (expected, actual) = get_commits("third-sha".into()).unwrap();
assert_eq!(expected[0..=1].concat().as_slice(), actual.as_slice());
}
#[test]
#[traced_test]
fn finds_didnt_find_tag_in_list() {
let error = get_commits("not-found-sha".into()).unwrap_err();
assert_eq!(
"sha was not found in commit chain: not-found-sha on branch: some-branch",
error.to_string()
);
}
}

View File

@@ -1,7 +0,0 @@
pub mod cliff;
pub mod command;
pub mod environment;
pub mod git_client;
pub mod gitea_client;
pub mod ui;
pub mod versioning;

View File

@@ -1,21 +1,13 @@
pub mod cliff;
pub mod command;
pub mod environment;
pub mod git_client;
pub mod gitea_client;
pub mod ui;
pub mod versioning;
use command::Command;
use cuddle_please_commands::PleaseCommand;
fn main() -> anyhow::Result<()> {
dotenv::dotenv().ok();
tracing_subscriber::fmt::init();
let current_dir = std::env::current_dir().ok();
let current_dir = current_dir.as_ref().map(|p| p.as_path());
let current_dir = current_dir.as_deref();
Command::new().execute(current_dir)?;
PleaseCommand::new().execute(current_dir)?;
Ok(())
}

View File

@@ -0,0 +1,6 @@
project:
owner: kjuulh
repository: cuddle-please
branch: main
settings:
api_url: https://some-example.gitea-instance

View File

@@ -0,0 +1,7 @@
please:
project:
owner: kjuulh
repository: cuddle-please
branch: main
settings:
api_url: https://some-example.gitea-instance

View File

@@ -0,0 +1,6 @@
project:
owner: kjuulh
repository: cuddle-please
branch: main
settings:
api_url: https://some-example.gitea-instance

View File

@@ -1,4 +1,4 @@
use cuddle_please::ui::{DynUi, Ui};
use cuddle_please_misc::{DynUi, Ui};
use std::{
io::Write,
@@ -6,6 +6,7 @@ use std::{
sync::{Arc, Mutex},
};
#[derive(Default)]
struct BufferInner {
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
@@ -84,15 +85,6 @@ impl Ui for BufferUi {
}
}
impl Default for BufferInner {
fn default() -> Self {
Self {
stdout: Vec::new(),
stderr: Vec::new(),
}
}
}
impl Default for BufferUi {
fn default() -> Self {
Self {
@@ -116,8 +108,8 @@ impl From<&BufferUi> for DynUi {
pub fn assert_output(ui: &BufferUi, expected_stdout: &str, expected_stderr: &str) {
let (stdout, stderr) = ui.get_output();
assert_eq!(expected_stdout, &stdout);
assert_eq!(expected_stderr, &stderr);
pretty_assertions::assert_eq!(expected_stdout, &stdout);
pretty_assertions::assert_eq!(expected_stderr, &stderr);
}
pub fn get_test_data_path(item: &str) -> PathBuf {

View File

@@ -1,15 +1,29 @@
pub mod common;
use common::BufferUi;
use cuddle_please::command::Command;
use cuddle_please_commands::PleaseCommand;
use tracing_test::traced_test;
use crate::common::{assert_output, get_test_data_path};
fn get_base_args<'a>() -> Vec<&'a str> {
vec!["cuddle-please", "config", "list"]
vec![
"cuddle-please",
"config",
"list",
"--no-vcs",
"--engine=local",
]
}
const EXPECTED_OUTPUT: &str = r#"cuddle-config
PleaseConfig
owner: kjuulh
repository: cuddle-please
branch: main
api_url: https://some-example.gitea-instance
"#;
#[test]
#[traced_test]
fn test_config_from_current_dir() {
@@ -17,11 +31,11 @@ fn test_config_from_current_dir() {
let ui = &BufferUi::default();
let current_dir = get_test_data_path("cuddle-embed");
Command::new_from_args(Some(ui), args.into_iter())
PleaseCommand::new_from_args(Some(ui), args)
.execute(Some(&current_dir))
.unwrap();
assert_output(ui, "cuddle-config\n", "");
assert_output(ui, EXPECTED_OUTPUT, "");
}
#[test]
@@ -33,11 +47,11 @@ fn test_config_from_source_dir() {
args.push("--source");
args.push(current_dir.to_str().unwrap());
Command::new_from_args(Some(ui), args.into_iter())
PleaseCommand::new_from_args(Some(ui), args)
.execute(None)
.unwrap();
assert_output(ui, "cuddle-config\n", "");
assert_output(ui, EXPECTED_OUTPUT, "");
}
#[test]
@@ -45,16 +59,19 @@ fn test_config_from_source_dir() {
fn test_config_from_stdin() {
let mut args = get_base_args();
let ui = &BufferUi::default();
let current_dir = get_test_data_path("cuddle-embed");
args.push("--source");
args.push(current_dir.to_str().unwrap());
args.push("--config-stdin");
let config = r#"
project:
owner: kjuulh
repository: cuddle-please
branch: main
settings:
api_url: https://some-example.gitea-instance"#;
Command::new_from_args_with_stdin(Some(ui), args.into_iter(), || Ok("please".into()))
args.push("--config-stdin");
PleaseCommand::new_from_args_with_stdin(Some(ui), args, || Ok(config.into()))
.execute(None)
.unwrap();
assert_output(ui, "cuddle-config\n", "");
assert_output(ui, EXPECTED_OUTPUT, "");
}
#[test]
@@ -63,7 +80,7 @@ fn test_config_fails_when_not_path_is_set() {
let args = get_base_args();
let ui = &BufferUi::default();
let res = Command::new_from_args(Some(ui), args.into_iter()).execute(None);
let res = PleaseCommand::new_from_args(Some(ui), args).execute(None);
assert!(res.is_err())
}

View File

@@ -1,6 +1,6 @@
use std::path::Path;
use cuddle_please::git_client::VcsClient;
use cuddle_please_misc::VcsClient;
use pretty_assertions::assert_eq;
use tracing_test::traced_test;
@@ -17,7 +17,7 @@ fn exec_git_into_branch() {
add_commit(tempdir.path(), "second").unwrap();
add_tag(tempdir.path(), "1.0.1").unwrap();
let vcs = VcsClient::new_git(tempdir.path()).unwrap();
let vcs = VcsClient::new_git(tempdir.path(), None::<String>, None::<String>).unwrap();
vcs.checkout_branch().unwrap();
let output = std::process::Command::new("git")
@@ -48,7 +48,7 @@ fn add_files_to_commit() {
add_commit(tempdir.path(), "first").unwrap();
add_tag(tempdir.path(), "1.0.0").unwrap();
let vcs = VcsClient::new_git(tempdir.path()).unwrap();
let vcs = VcsClient::new_git(tempdir.path(), None::<String>, None::<String>).unwrap();
vcs.checkout_branch().unwrap();
std::fs::File::create(tempdir.path().join("changelog")).unwrap();
@@ -76,7 +76,7 @@ fn reset_branch() {
add_commit(tempdir.path(), "first").unwrap();
add_tag(tempdir.path(), "1.0.0").unwrap();
let vcs = VcsClient::new_git(tempdir.path()).unwrap();
let vcs = VcsClient::new_git(tempdir.path(), None::<String>, None::<String>).unwrap();
vcs.checkout_branch().unwrap();
std::fs::File::create(tempdir.path().join("changelog-first")).unwrap();

View File

@@ -1,6 +1,6 @@
pub mod common;
use cuddle_please::git_client::VcsClient;
use cuddle_please_misc::VcsClient;
use tracing_test::traced_test;
use crate::common::get_test_data_path;
@@ -16,6 +16,13 @@ fn test_vcs_get_noop() {
#[traced_test]
fn test_vcs_get_git_found() {
let testdata = get_test_data_path("git-found");
let git = VcsClient::new_git(&testdata).unwrap();
assert_eq!(git, VcsClient::Git { source: testdata })
let git = VcsClient::new_git(&testdata, None::<String>, None::<String>).unwrap();
assert_eq!(
git,
VcsClient::Git {
source: testdata,
username: "cuddle-please".into(),
email: "bot@cuddle.sh".into()
}
)
}

View File

@@ -1,12 +1,20 @@
# yaml-language-server: $schema=https://git.front.kjuulh.io/kjuulh/cuddle/raw/branch/main/schemas/base.json
base: "git@git.front.kjuulh.io:kjuulh/cuddle-base.git"
base: "git@git.front.kjuulh.io:kjuulh/cuddle-rust-cli-plan.git"
vars:
service: "cuddle-please"
registry: kasperhermansen
mkdocs_image: "squidfunk/mkdocs-material:9.1"
please:
project:
owner: kjuulh
repository: cuddle-please
branch: main
settings:
api_url: https://git.front.kjuulh.io
scripts:
"mkdocs:new":
type: shell

3
renovate.json Normal file
View File

@@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}