Compare commits
2 commits
656ec4a534
...
1bf9f6a516
Author | SHA1 | Date | |
---|---|---|---|
1bf9f6a516 | |||
3670979b3c |
8 changed files with 127 additions and 44 deletions
43
CHANGELOG.md
43
CHANGELOG.md
|
@ -2,8 +2,46 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.11.0] - 2024-07-26
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Remove unused dependecy from file-watcher-actor ([b8f4ade](https://git.kemitix.net/kemitix/git-next/commit/b8f4adeb50a98e64efe2a1a9009c4d6a6b458e3b))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Document Notifications to user ([1690e1b](https://git.kemitix.net/kemitix/git-next/commit/1690e1bff6a3b54ff59b0763ecc2e50c25f9b896))
|
||||||
|
- Update message graph for repo-actor ([758ca5c](https://git.kemitix.net/kemitix/git-next/commit/758ca5c2dc9273be15cdfb383bdc35095bc7834e))
|
||||||
|
- Update package graph ([768ec6a](https://git.kemitix.net/kemitix/git-next/commit/768ec6ae02fe7d850ff976d51aa3278c01ce1013))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Enable configuration of a webhook for receiving notifications ([c86d890](https://git.kemitix.net/kemitix/git-next/commit/c86d890c2cbbbe87fde58664c68c91b698862044))
|
||||||
|
- Support sending messages to the user ([e9877ca](https://git.kemitix.net/kemitix/git-next/commit/e9877ca9fa0addf3f018527712355ca0c3d9eb77))
|
||||||
|
- Dispatch NotifyUser messages to server for user (1/2) ([bcf57bc](https://git.kemitix.net/kemitix/git-next/commit/bcf57bc728fd53f0abb9c4e94d9768fcce5e9dbe))
|
||||||
|
- Dispatch NotifyUser messages to server for user (2/2) ([288c20c](https://git.kemitix.net/kemitix/git-next/commit/288c20c24b59b2fa5054c81c22d42af2af06afc7))
|
||||||
|
- Post webhook notifications to user ([9e12f5e](https://git.kemitix.net/kemitix/git-next/commit/9e12f5eb5db5f3b150886b444af4c0ce3dbf2ed9))
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Reduce cognitive complexity of `WebhookNotification` handler. 1/2 ([06292c2](https://git.kemitix.net/kemitix/git-next/commit/06292c2711f3aca6bc369b78f67e1936fdba7eb8))
|
||||||
|
- Reduce cognitive complexity of `WebhookNotification` handler. 2/2 ([c104dfe](https://git.kemitix.net/kemitix/git-next/commit/c104dfedc1f41020b3468d73a52ae49e0050ebb2))
|
||||||
|
- Reduce cognitive complexity of 'validate_position' ([92ebd45](https://git.kemitix.net/kemitix/git-next/commit/92ebd453076015993d25102d262a4821fe416e06))
|
||||||
|
- Flag internally that dev not based on main will require used intervention ([ba67b1e](https://git.kemitix.net/kemitix/git-next/commit/ba67b1ebcba46308a44d3f6dccc16ed8b0acefe3))
|
||||||
|
- Extract messages and handlers modules from webhook-actor ([8f95ae0](https://git.kemitix.net/kemitix/git-next/commit/8f95ae0058a9f426c5d3f8f96990f6b0eb358b9e))
|
||||||
|
- Use Option<&T> over &Option<T> ([4978400](https://git.kemitix.net/kemitix/git-next/commit/4978400ece7c37ed51328da0667b2abb1b528fc7))
|
||||||
|
- Merge actor-macros into core ([48c968d](https://git.kemitix.net/kemitix/git-next/commit/48c968db2d166942ba1be0f09f729d5611cedf18))
|
||||||
|
- Merge config crate into core crate ([ab728c7](https://git.kemitix.net/kemitix/git-next/commit/ab728c7364caa0c8481cd2a10c3fa57bdc7f2d16))
|
||||||
|
- Merge git create into core crate ([fa5fa80](https://git.kemitix.net/kemitix/git-next/commit/fa5fa809d99b70970d8f0f2f910afb99837e3913))
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- Restore unlinked test file ([3670979](https://git.kemitix.net/kemitix/git-next/commit/3670979b3c225745a7262d1d1623ccc0cf1b8ff5))
|
||||||
|
|
||||||
## [0.10.0] - 2024-07-16
|
## [0.10.0] - 2024-07-16
|
||||||
|
|
||||||
|
[41c8a31](https://git.kemitix.net/kemitix/git-next/commit/41c8a319b1344d2ce04bfa8f45eb9a267d8e9a3c)...[f8fefcd](https://git.kemitix.net/kemitix/git-next/commit/f8fefcdeddf556b28dc611b85db8e2b5ffbb570d)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
- Move server-default.toml inside crate that uses it ([639e561](https://git.kemitix.net/kemitix/git-next/commit/639e561be60a6e22eda14e2b44764eee6afb6ae7))
|
- Move server-default.toml inside crate that uses it ([639e561](https://git.kemitix.net/kemitix/git-next/commit/639e561be60a6e22eda14e2b44764eee6afb6ae7))
|
||||||
|
@ -22,6 +60,11 @@ All notable changes to this project will be documented in this file.
|
||||||
- Unregister webhooks form forge during shutdown ([b715755](https://git.kemitix.net/kemitix/git-next/commit/b715755b91cecd8fa6b67a58ac3e6fd322c9c005))
|
- Unregister webhooks form forge during shutdown ([b715755](https://git.kemitix.net/kemitix/git-next/commit/b715755b91cecd8fa6b67a58ac3e6fd322c9c005))
|
||||||
- Reload server config when file is touched ([33907a1](https://git.kemitix.net/kemitix/git-next/commit/33907a1d3284a2df27994f7da1ef65d3047f165f))
|
- Reload server config when file is touched ([33907a1](https://git.kemitix.net/kemitix/git-next/commit/33907a1d3284a2df27994f7da1ef65d3047f165f))
|
||||||
|
|
||||||
|
### Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Restore clean check and tag checkout to publish script ([95129dd](https://git.kemitix.net/kemitix/git-next/commit/95129ddeefa26db7cb538f2be2ab5b3609e9a175))
|
||||||
|
- Release 0.10.0 ([f8fefcd](https://git.kemitix.net/kemitix/git-next/commit/f8fefcdeddf556b28dc611b85db8e2b5ffbb570d))
|
||||||
|
|
||||||
### Build
|
### Build
|
||||||
|
|
||||||
- Add more metadata for crates.io ([69211a8](https://git.kemitix.net/kemitix/git-next/commit/69211a87a3aaba2c8e4037d5f1a8adbca185f13d))
|
- Add more metadata for crates.io ([69211a8](https://git.kemitix.net/kemitix/git-next/commit/69211a87a3aaba2c8e4037d5f1a8adbca185f13d))
|
||||||
|
|
29
Cargo.toml
29
Cargo.toml
|
@ -3,7 +3,7 @@ resolver = "2"
|
||||||
members = ["crates/*"]
|
members = ["crates/*"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.10.0" # Update git-next-* under workspace.dependencies
|
version = "0.11.0" # Update git-next-* under workspace.dependencies
|
||||||
|
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
@ -23,18 +23,21 @@ unwrap_used = "warn"
|
||||||
expect_used = "warn"
|
expect_used = "warn"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
git-next-core = { path = "crates/core", version = "0.10" }
|
git-next-core = { path = "crates/core", version = "0.11" }
|
||||||
git-next-server = { path = "crates/server", version = "0.10" }
|
git-next-server = { path = "crates/server", version = "0.11" }
|
||||||
git-next-server-actor = { path = "crates/server-actor", version = "0.10" }
|
git-next-server-actor = { path = "crates/server-actor", version = "0.11" }
|
||||||
git-next-config = { path = "crates/config", version = "0.10" }
|
git-next-forge = { path = "crates/forge", version = "0.11" }
|
||||||
git-next-git = { path = "crates/git", version = "0.10" }
|
git-next-forge-forgejo = { path = "crates/forge-forgejo", version = "0.11" }
|
||||||
git-next-forge = { path = "crates/forge", version = "0.10" }
|
git-next-forge-github = { path = "crates/forge-github", version = "0.11" }
|
||||||
git-next-forge-forgejo = { path = "crates/forge-forgejo", version = "0.10" }
|
git-next-repo-actor = { path = "crates/repo-actor", version = "0.11" }
|
||||||
git-next-forge-github = { path = "crates/forge-github", version = "0.10" }
|
git-next-webhook-actor = { path = "crates/webhook-actor", version = "0.11" }
|
||||||
git-next-repo-actor = { path = "crates/repo-actor", version = "0.10" }
|
git-next-file-watcher-actor = { path = "crates/file-watcher-actor", version = "0.11" }
|
||||||
git-next-webhook-actor = { path = "crates/webhook-actor", version = "0.10" }
|
|
||||||
git-next-file-watcher-actor = { path = "crates/file-watcher-actor", version = "0.10" }
|
# remove after 0.11.0
|
||||||
git-next-actor-macros = { path = "crates/actor-macros", version = "0.10" }
|
git-next-config = { path = "crates/config", version = "0.11" }
|
||||||
|
git-next-git = { path = "crates/git", version = "0.11" }
|
||||||
|
git-next-actor-macros = { path = "crates/actor-macros", version = "0.11" }
|
||||||
|
|
||||||
|
|
||||||
# CLI parsing
|
# CLI parsing
|
||||||
clap = { version = "4.5", features = ["cargo", "derive"] }
|
clap = { version = "4.5", features = ["cargo", "derive"] }
|
||||||
|
|
|
@ -14,3 +14,13 @@ impl TryFrom<gix::Url> for RemoteUrl {
|
||||||
Ok(Self(parsed))
|
Ok(Self(parsed))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl RemoteUrl {
|
||||||
|
pub fn matches(&self, other: &Self) -> bool {
|
||||||
|
tracing::debug!(host = ?self.host, user = ?self.user, fullname = ?self.fullname, token = ?self.token, "a");
|
||||||
|
tracing::debug!(host = ?other.host, user = ?other.user, fullname = ?other.fullname, token = ?other.token,"b");
|
||||||
|
self.host.eq(&other.host)
|
||||||
|
&& self.user.eq(&other.user)
|
||||||
|
&& self.fullname.eq(&other.fullname)
|
||||||
|
&& self.token.eq(&other.token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -81,9 +81,13 @@ impl RepoDetails {
|
||||||
let user = self.forge.user();
|
let user = self.forge.user();
|
||||||
use secrecy::ExposeSecret;
|
use secrecy::ExposeSecret;
|
||||||
let token = self.forge.token().expose_secret();
|
let token = self.forge.token().expose_secret();
|
||||||
|
let auth_delim = match token.is_empty() {
|
||||||
|
true => "",
|
||||||
|
false => ":",
|
||||||
|
};
|
||||||
let hostname = self.forge.hostname();
|
let hostname = self.forge.hostname();
|
||||||
let repo_path = &self.repo_path;
|
let repo_path = &self.repo_path;
|
||||||
format!("https://{user}:{token}@{hostname}/{repo_path}.git").into()
|
format!("https://{user}{auth_delim}{token}@{hostname}/{repo_path}.git").into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::result_large_err)]
|
#[allow(clippy::result_large_err)]
|
||||||
|
@ -118,7 +122,7 @@ impl RepoDetails {
|
||||||
tracing::debug!("No remote url to assert against");
|
tracing::debug!("No remote url to assert against");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
if found != expected {
|
if !found.matches(&expected) {
|
||||||
tracing::debug!(?found, ?expected, "urls differ");
|
tracing::debug!(?found, ?expected, "urls differ");
|
||||||
match self.gitdir.storage_path_type() {
|
match self.gitdir.storage_path_type() {
|
||||||
StoragePathType::External => {
|
StoragePathType::External => {
|
||||||
|
|
|
@ -23,13 +23,13 @@ pub fn validate_default_remotes(
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
info!(config = %remote_url, push = %push_remote, fetch = %fetch_remote, "Check remotes match");
|
info!(config = %remote_url, push = %push_remote, fetch = %fetch_remote, "Check remotes match");
|
||||||
if remote_url != push_remote {
|
if !remote_url.matches(&push_remote) {
|
||||||
return Err(Error::MismatchDefaultPushRemote {
|
return Err(Error::MismatchDefaultPushRemote {
|
||||||
found: push_remote,
|
found: push_remote,
|
||||||
expected: remote_url,
|
expected: remote_url,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if remote_url != fetch_remote {
|
if !remote_url.matches(&fetch_remote) {
|
||||||
return Err(Error::MismatchDefaultFetchRemote {
|
return Err(Error::MismatchDefaultFetchRemote {
|
||||||
found: fetch_remote,
|
found: fetch_remote,
|
||||||
expected: remote_url,
|
expected: remote_url,
|
||||||
|
|
|
@ -27,6 +27,8 @@ actix-rt = { workspace = true }
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
# Testing
|
# Testing
|
||||||
assert2 = { workspace = true }
|
assert2 = { workspace = true }
|
||||||
|
secrecy = { workspace = true }
|
||||||
|
test-log = { workspace = true }
|
||||||
|
|
||||||
[lints.clippy]
|
[lints.clippy]
|
||||||
nursery = { level = "warn", priority = -1 }
|
nursery = { level = "warn", priority = -1 }
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
//
|
//
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
|
|
||||||
use git_next_core::git::RepositoryFactory;
|
use git_next_core::git::RepositoryFactory;
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
//
|
//
|
||||||
use assert2::let_assert;
|
use assert2::let_assert;
|
||||||
use git::{repository::Direction, validation::remotes::validate_default_remotes};
|
use git_next_core::{
|
||||||
use git_next_config::{
|
self as core,
|
||||||
self as config, ForgeType, GitDir, Hostname, RepoBranches, RepoConfig, RepoConfigSource,
|
git::{self, repository::Direction, validation::remotes::validate_default_remotes},
|
||||||
RepoPath,
|
ApiToken, ForgeType, GitDir, Hostname, RepoBranches, RepoConfig, RepoConfigSource, RepoPath,
|
||||||
|
StoragePathType, User,
|
||||||
};
|
};
|
||||||
use git_next_git as git;
|
use secrecy::Secret;
|
||||||
|
|
||||||
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
|
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_repo_config_load() -> Result<()> {
|
fn test_repo_config_load() -> Result<()> {
|
||||||
|
@ -34,7 +35,7 @@ fn test_repo_config_load() -> Result<()> {
|
||||||
#[test]
|
#[test]
|
||||||
fn gitdir_should_display_as_pathbuf() {
|
fn gitdir_should_display_as_pathbuf() {
|
||||||
//given
|
//given
|
||||||
let gitdir = GitDir::from("foo/dir");
|
let gitdir = GitDir::new("foo/dir".into(), StoragePathType::External);
|
||||||
//when
|
//when
|
||||||
let result = format!("{}", gitdir);
|
let result = format!("{}", gitdir);
|
||||||
//then
|
//then
|
||||||
|
@ -48,50 +49,59 @@ fn gitdir_should_display_as_pathbuf() {
|
||||||
fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<()> {
|
fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<()> {
|
||||||
let cli_crate_dir = std::env::current_dir().map_err(git::validation::remotes::Error::Io)?;
|
let cli_crate_dir = std::env::current_dir().map_err(git::validation::remotes::Error::Io)?;
|
||||||
let_assert!(Some(Some(root)) = cli_crate_dir.parent().map(|p| p.parent()));
|
let_assert!(Some(Some(root)) = cli_crate_dir.parent().map(|p| p.parent()));
|
||||||
let mut repo_details = git::common::repo_details(
|
let mut repo_details = git::repo_details(
|
||||||
1,
|
1,
|
||||||
git::Generation::default(),
|
git::Generation::default(),
|
||||||
config::common::forge_details(1, ForgeType::MockForge),
|
core::common::forge_details(1, ForgeType::MockForge),
|
||||||
None,
|
None,
|
||||||
GitDir::new(root), // Server GitDir - should be ignored
|
GitDir::new(root.to_path_buf(), StoragePathType::External), // Server GitDir - should be ignored
|
||||||
);
|
);
|
||||||
repo_details.forge = repo_details
|
repo_details.forge = repo_details
|
||||||
.forge
|
.forge
|
||||||
|
.with_user(User::new("git".to_string()))
|
||||||
|
.with_token(ApiToken::new(Secret::new("".to_string())))
|
||||||
.with_hostname(Hostname::new("git.kemitix.net"));
|
.with_hostname(Hostname::new("git.kemitix.net"));
|
||||||
repo_details.repo_path = RepoPath::new("kemitix/git-next".to_string());
|
repo_details.repo_path = RepoPath::new("kemitix/git-next".to_string());
|
||||||
let gitdir = &repo_details.gitdir;
|
let open_repository = git::repository::factory::real().open(&repo_details)?;
|
||||||
let open_repository = git::repository::real().open(gitdir)?;
|
|
||||||
let_assert!(
|
let_assert!(
|
||||||
Some(found_git_remote) = open_repository.find_default_remote(Direction::Push),
|
Some(found_git_remote) = open_repository.find_default_remote(Direction::Push),
|
||||||
"Default Push Remote not found"
|
"Default Push Remote not found"
|
||||||
);
|
);
|
||||||
let config_git_remote = repo_details.git_remote();
|
let_assert!(Some(config_git_remote) = repo_details.remote_url());
|
||||||
|
|
||||||
assert_eq!(
|
assert!(
|
||||||
found_git_remote, config_git_remote,
|
found_git_remote.matches(&config_git_remote),
|
||||||
"Default Push Remote must match config"
|
"Default Push Remote must match config"
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> {
|
fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> {
|
||||||
let cli_crate_dir = std::env::current_dir().map_err(git::validation::remotes::Error::Io)?;
|
let cli_crate_dir = std::env::current_dir().map_err(git::validation::remotes::Error::Io)?;
|
||||||
let_assert!(Some(Some(root)) = cli_crate_dir.parent().map(|p| p.parent()));
|
let_assert!(Some(Some(root)) = cli_crate_dir.parent().map(|p| p.parent()));
|
||||||
let mut repo_details = git::common::repo_details(
|
let mut repo_details = git::repo_details(
|
||||||
1,
|
1,
|
||||||
git::Generation::default(),
|
git::Generation::default(),
|
||||||
config::common::forge_details(1, ForgeType::MockForge),
|
core::common::forge_details(1, ForgeType::MockForge),
|
||||||
None,
|
None,
|
||||||
GitDir::new(root), // Server GitDir - should be ignored
|
GitDir::new(root.to_path_buf(), StoragePathType::External), // Server GitDir - should be ignored
|
||||||
)
|
)
|
||||||
.with_repo_path(RepoPath::new("kemitix/git-next".to_string()));
|
.with_repo_path(RepoPath::new("kemitix/git-next".to_string()));
|
||||||
repo_details.forge = repo_details
|
repo_details.forge = repo_details
|
||||||
.forge
|
.forge
|
||||||
|
.with_user(User::new("git".to_string()))
|
||||||
|
.with_token(ApiToken::new(Secret::new("".to_string())))
|
||||||
.with_hostname(Hostname::new("git.kemitix.net"));
|
.with_hostname(Hostname::new("git.kemitix.net"));
|
||||||
let gitdir = &repo_details.gitdir;
|
tracing::debug!("opening...");
|
||||||
let repository = git::repository::real().open(gitdir)?;
|
let_assert!(
|
||||||
|
Ok(repository) = git::repository::factory::real().open(&repo_details),
|
||||||
|
"open repository"
|
||||||
|
);
|
||||||
|
tracing::debug!("open okay");
|
||||||
|
tracing::info!(?repository, "FOO");
|
||||||
|
tracing::info!(?repo_details, "BAR");
|
||||||
validate_default_remotes(&*repository, &repo_details)?;
|
validate_default_remotes(&*repository, &repo_details)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -103,16 +113,24 @@ fn gitdir_validate_should_fail_a_git_repo_with_wrong_remote() -> Result<()> {
|
||||||
Ok(cli_crate_dir) = std::env::current_dir().map_err(git::validation::remotes::Error::Io)
|
Ok(cli_crate_dir) = std::env::current_dir().map_err(git::validation::remotes::Error::Io)
|
||||||
);
|
);
|
||||||
let_assert!(Some(Some(root)) = cli_crate_dir.parent().map(|p| p.parent()));
|
let_assert!(Some(Some(root)) = cli_crate_dir.parent().map(|p| p.parent()));
|
||||||
let repo_details = git::common::repo_details(
|
let mut repo_details = git::repo_details(
|
||||||
1,
|
1,
|
||||||
git::Generation::default(),
|
git::Generation::default(),
|
||||||
config::common::forge_details(1, ForgeType::MockForge),
|
core::common::forge_details(1, ForgeType::MockForge),
|
||||||
None,
|
None,
|
||||||
GitDir::new(root), // Server GitDir - should be ignored
|
GitDir::new(root.to_path_buf(), StoragePathType::External), // Server GitDir - should be ignored
|
||||||
)
|
)
|
||||||
.with_repo_path(RepoPath::new("hello/world".to_string()));
|
.with_repo_path(RepoPath::new("kemitix/git-next".to_string()));
|
||||||
let gitdir = &repo_details.gitdir;
|
repo_details.forge = repo_details
|
||||||
let repository = git::repository::real().open(gitdir)?;
|
.forge
|
||||||
|
.with_user(User::new("git".to_string()))
|
||||||
|
.with_token(ApiToken::new(Secret::new("".to_string())))
|
||||||
|
.with_hostname(Hostname::new("git.kemitix.net"));
|
||||||
|
let repository = git::repository::factory::real().open(&repo_details)?;
|
||||||
|
let mut repo_details = repo_details.clone();
|
||||||
|
repo_details.forge = repo_details
|
||||||
|
.forge
|
||||||
|
.with_hostname(Hostname::new("code.kemitix.net"));
|
||||||
let_assert!(Err(_) = validate_default_remotes(&*repository, &repo_details));
|
let_assert!(Err(_) = validate_default_remotes(&*repository, &repo_details));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
Loading…
Reference in a new issue