refactoring to use quit crate
This commit is contained in:
parent
8d1d3e514b
commit
6d3ff502a9
6 changed files with 140 additions and 59 deletions
26
Cargo.lock
generated
26
Cargo.lock
generated
|
@ -146,6 +146,15 @@ version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colored"
|
||||||
|
version = "3.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
|
@ -539,6 +548,21 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quit"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c090c608233d81bd6b90e718cf34506c60a10e633dff2292c3d1029e798d669b"
|
||||||
|
dependencies = [
|
||||||
|
"quit_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quit_macros"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d4b27a0dd5d08ad7af2d17952fb360ec9c30eeade0b32df7a3c9b099ff37564"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.40"
|
version = "1.0.40"
|
||||||
|
@ -553,9 +577,11 @@ name = "refractr"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
|
"colored",
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
"git2",
|
"git2",
|
||||||
"hex",
|
"hex",
|
||||||
|
"quit",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
|
|
@ -5,9 +5,11 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.5.29", features = ["derive"] }
|
clap = { version = "4.5.29", features = ["derive"] }
|
||||||
|
colored = "3.0.0"
|
||||||
ctrlc = "3.4.5"
|
ctrlc = "3.4.5"
|
||||||
git2 = "0.20.0"
|
git2 = "0.20.0"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
|
quit = "2.0.0"
|
||||||
serde = "1.0.217"
|
serde = "1.0.217"
|
||||||
serde_derive = "1.0.217"
|
serde_derive = "1.0.217"
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
|
|
|
@ -1,13 +1,25 @@
|
||||||
|
use colored::Colorize;
|
||||||
|
|
||||||
#[macro_export]
|
pub enum ExitCode {
|
||||||
macro_rules! freak_out {
|
ParseError = 2,
|
||||||
($msg:expr) => {
|
FilesystemError = 3,
|
||||||
return Err($msg)
|
RepositoryError = 4,
|
||||||
};
|
RemoteError = 5,
|
||||||
|
PushError = 6
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ReturnData {
|
||||||
|
pub code: ExitCode,
|
||||||
|
pub msg: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn error(msg: String, code: ExitCode) {
|
||||||
|
eprintln!("{} {}", "error:".red().bold(), msg);
|
||||||
|
quit::with_code(code as u8)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn warning(msg: String) {
|
pub fn warning(msg: String) {
|
||||||
eprintln!("warning: {}", msg)
|
eprintln!("{} {}", "warning:".yellow().bold(), msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verbose(level: u8, msg_lvl: u8, msg: String) {
|
pub fn verbose(level: u8, msg_lvl: u8, msg: String) {
|
||||||
|
@ -18,5 +30,5 @@ pub fn verbose(level: u8, msg_lvl: u8, msg: String) {
|
||||||
}
|
}
|
||||||
|
|
||||||
prefix += "> ";
|
prefix += "> ";
|
||||||
eprintln!("{}{}", prefix, msg);
|
eprintln!("{}{}", prefix.purple().bold(), msg);
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,24 +107,32 @@ pub fn read_config(paths: Vec<PathBuf>, refractr: &Refractr) -> Result<Vec<Confi
|
||||||
common::verbose(refractr.verbose, 1, format!("Reading config file: \"{}\"", String::from(path.to_string_lossy())));
|
common::verbose(refractr.verbose, 1, format!("Reading config file: \"{}\"", String::from(path.to_string_lossy())));
|
||||||
let mut data = String::new();
|
let mut data = String::new();
|
||||||
let mut file = match File::open(path.as_path()) {
|
let mut file = match File::open(path.as_path()) {
|
||||||
Err(e) => return Err(format!("refractr: unable to open {}: {}", path.as_path().display(), e)),
|
Err(e) => return Err(format!("unable to open {}: {}", path.as_path().display(), e)),
|
||||||
Ok(file) => file
|
Ok(file) => file
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = file.read_to_string(&mut data) {
|
if let Err(e) = file.read_to_string(&mut data) {
|
||||||
return Err(format!("refractr: unable to read {}: {}", path.as_path().display(), e))
|
return Err(format!("unable to read {}: {}", path.as_path().display(), e))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let config: Config = match toml::from_str(&data) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Err(format!("issues parsing toml file {}: {}", path.as_path().display(), e))
|
||||||
|
};
|
||||||
|
|
||||||
let config_file = ConfigFile {
|
let config_file = ConfigFile {
|
||||||
path: match fs::canonicalize(&path) {
|
path: match fs::canonicalize(&path) {
|
||||||
Err(_) => return Err(format!("refractr: cannot get absolute path of config file: {}", path.as_path().display())),
|
Err(_) => return Err(format!("cannot get absolute path of config file: {}", path.as_path().display())),
|
||||||
Ok(abs) => abs.to_string_lossy().to_string()
|
Ok(abs) => abs.to_string_lossy().to_string()
|
||||||
},
|
},
|
||||||
file: match fs::metadata(&path) {
|
file: match fs::metadata(&path) {
|
||||||
Err(_) => return Err(format!("refractr: cannot obtain metadata for config file: {}", path.as_path().display())),
|
Err(_) => return Err(format!("cannot obtain metadata for config file: {}", path.as_path().display())),
|
||||||
Ok(metadata) => metadata
|
Ok(metadata) => metadata
|
||||||
},
|
},
|
||||||
config: verify_config(toml::from_str(&data).unwrap())
|
config: match verify_config(&config) {
|
||||||
|
Err(e) => return Err(format!("invalid config {}: {}", path.as_path().display(), e)),
|
||||||
|
Ok(_) => config
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut dup = false;
|
let mut dup = false;
|
||||||
|
@ -146,25 +154,37 @@ pub fn read_config(paths: Vec<PathBuf>, refractr: &Refractr) -> Result<Vec<Confi
|
||||||
return Ok(config_files);
|
return Ok(config_files);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_config(config: Config) -> Config {
|
fn verify_config(config: &Config) -> Result<(), String> {
|
||||||
if config.schedule.enabled {
|
if config.schedule.enabled {
|
||||||
assert_ne!(config.schedule.interval, None);
|
match config.schedule.interval {
|
||||||
assert!(config.schedule.interval.unwrap() >= 60);
|
Some(i) => {
|
||||||
|
if i < 60 {
|
||||||
|
return Err(format!("schedule is enabled, but less than 60"))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => return Err(format!("schedule is enabled, but no interval was defined"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_ne!("", match &config.work_dir {
|
match &config.work_dir {
|
||||||
Some(path) => format!("{}", path),
|
Some(path) => format!("{}", path),
|
||||||
None => {
|
None => {
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
match env::var("TEMP") {
|
match env::var("TEMP") {
|
||||||
Ok(val) => val,
|
Ok(val) => val,
|
||||||
Err(_) => format!("")
|
Err(_) => return Err(format!("cannot determine the default temp dir"))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
format!("/tmp/refractr")
|
format!("/tmp/refractr")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
return config;
|
if !&config.from.starts_with("ssh://")
|
||||||
|
&& !&config.from.starts_with("https://")
|
||||||
|
&& !&config.from.starts_with("http://") {
|
||||||
|
return Err(format!("'from' value does not use a supported protocol"))
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
19
src/main.rs
19
src/main.rs
|
@ -44,6 +44,7 @@ fn get_config_default() -> &'static str {
|
||||||
return "/usr/local/etc/refractr/config.toml";
|
return "/usr/local/etc/refractr/config.toml";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[quit::main]
|
||||||
fn main() -> Result<(), String> {
|
fn main() -> Result<(), String> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
let refractr = Refractr {
|
let refractr = Refractr {
|
||||||
|
@ -77,19 +78,21 @@ fn main() -> Result<(), String> {
|
||||||
Err(_) => common::warning(format!("failed to get process username")),
|
Err(_) => common::warning(format!("failed to get process username")),
|
||||||
}
|
}
|
||||||
|
|
||||||
common::verbose(refractr.verbose, 1, format!("Level {} verbosity enabled", refractr.verbose.to_string()));
|
common::verbose(refractr.verbose, 1, format!("refractr started with level {} verbosity enabled", refractr.verbose.to_string()));
|
||||||
common::verbose(refractr.verbose, 3, format!("Process ID: {}", refractr.pid));
|
common::verbose(refractr.verbose, 3, format!("Process ID: {}", refractr.pid));
|
||||||
common::verbose(refractr.verbose, 3, format!("Running in Docker: {}", refractr.docker));
|
common::verbose(refractr.verbose, 3, format!("Running in Docker: {}", refractr.docker));
|
||||||
|
common::verbose(refractr.verbose, 3, format!("System is UNIX(-like): {}", refractr.unix));
|
||||||
common::verbose(refractr.verbose, 2, format!("Checking for create flag"));
|
common::verbose(refractr.verbose, 2, format!("Checking for create flag"));
|
||||||
if args.create {
|
if args.create {
|
||||||
common::verbose(refractr.verbose, 3, format!("Printing sample config"));
|
common::verbose(refractr.verbose, 3, format!("Printing sample config"));
|
||||||
let example = include_str!("example/config.toml");
|
let example = include_str!("example/config.toml");
|
||||||
println!("{}", example);
|
println!("{}", example);
|
||||||
Ok(())
|
return Ok(())
|
||||||
} else {
|
}
|
||||||
|
|
||||||
let cfgs = match config::read_config(args.config, &refractr) {
|
let cfgs = match config::read_config(args.config, &refractr) {
|
||||||
Ok(cfgs) => cfgs,
|
Ok(cfgs) => cfgs,
|
||||||
Err(e) => freak_out!(e)
|
Err(e) => return Err(e)
|
||||||
};
|
};
|
||||||
if refractr.verbose >= 2 {
|
if refractr.verbose >= 2 {
|
||||||
// no need to loop over configs if verbose is not at the correct level
|
// no need to loop over configs if verbose is not at the correct level
|
||||||
|
@ -99,7 +102,11 @@ fn main() -> Result<(), String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
common::verbose(refractr.verbose, 1, format!("Config file(s) read successfully"));
|
common::verbose(refractr.verbose, 1, format!("Config file(s) read successfully"));
|
||||||
refractr.run(cfgs)
|
match refractr.run(cfgs) {
|
||||||
}
|
Ok(_) => (),
|
||||||
|
Err(e) => common::error(format!("{}", e.msg), e.code)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
use crate::freak_out;
|
use crate::common::{self, ReturnData, ExitCode};
|
||||||
use crate::common;
|
|
||||||
use crate::config::{Config, ConfigFile};
|
use crate::config::{Config, ConfigFile};
|
||||||
|
|
||||||
use git2::{FetchOptions, CertificateCheckStatus, Cred, PushOptions, RemoteCallbacks, Repository};
|
use git2::{FetchOptions, CertificateCheckStatus, Cred, PushOptions, RemoteCallbacks, Repository};
|
||||||
use git2::{Error, ErrorCode};
|
use git2::{Error, ErrorCode};
|
||||||
use git2::build::{RepoBuilder, CheckoutBuilder};
|
use git2::build::RepoBuilder;
|
||||||
use hex;
|
use hex;
|
||||||
use sha2::{Sha256, Digest};
|
use sha2::{Sha256, Digest};
|
||||||
use std::env;
|
use std::env;
|
||||||
|
@ -33,7 +32,7 @@ struct OpenedRepository {
|
||||||
impl Refractr {
|
impl Refractr {
|
||||||
fn set_up_work_dir(&self, work_dir: PathBuf) -> Result<String, String> {
|
fn set_up_work_dir(&self, work_dir: PathBuf) -> Result<String, String> {
|
||||||
if let Err(e) = fs::create_dir_all(&work_dir) {
|
if let Err(e) = fs::create_dir_all(&work_dir) {
|
||||||
freak_out!(format!("could not create working directory: {}: {}", work_dir.to_string_lossy().to_string(), e))
|
return Err(format!("could not create working directory: {}: {}", work_dir.to_string_lossy().to_string(), e))
|
||||||
}
|
}
|
||||||
Ok(work_dir.to_string_lossy().to_string())
|
Ok(work_dir.to_string_lossy().to_string())
|
||||||
}
|
}
|
||||||
|
@ -121,7 +120,7 @@ impl Refractr {
|
||||||
common::warning(format!("remote {} already exists, skipping", remote_id));
|
common::warning(format!("remote {} already exists, skipping", remote_id));
|
||||||
remote_list.push(remote_id)
|
remote_list.push(remote_id)
|
||||||
} else {
|
} else {
|
||||||
freak_out!(format!("failed to create remote: {}", e));
|
return Err(format!("failed to create remote: {}", e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -180,7 +179,7 @@ impl Refractr {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn looper(&self, repos: Vec<OpenedRepository>) -> Result<(), String> {
|
fn looper(&self, repos: Vec<OpenedRepository>) -> Result<(), ReturnData> {
|
||||||
let mut current_ints = Vec::new();
|
let mut current_ints = Vec::new();
|
||||||
let running = Arc::new(AtomicBool::new(true));
|
let running = Arc::new(AtomicBool::new(true));
|
||||||
let r = running.clone();
|
let r = running.clone();
|
||||||
|
@ -224,8 +223,8 @@ impl Refractr {
|
||||||
&repos[i].cfg,
|
&repos[i].cfg,
|
||||||
&repos[i].repo,
|
&repos[i].repo,
|
||||||
&repos[i].remotes) {
|
&repos[i].remotes) {
|
||||||
return Err(e)
|
common::error(e, ExitCode::PushError)
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
do_break = false;
|
do_break = false;
|
||||||
|
@ -238,7 +237,7 @@ impl Refractr {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(&self, cfgs: Vec<ConfigFile>) -> Result<(), String> {
|
pub fn run(&self, cfgs: Vec<ConfigFile>) -> Result<(), ReturnData> {
|
||||||
common::verbose(self.verbose, 3, format!("Starting main refractr loop"));
|
common::verbose(self.verbose, 3, format!("Starting main refractr loop"));
|
||||||
let mut loop_repos = Vec::new();
|
let mut loop_repos = Vec::new();
|
||||||
|
|
||||||
|
@ -248,7 +247,7 @@ impl Refractr {
|
||||||
let work_dir = self.set_up_work_dir(match &cfg.config.work_dir {
|
let work_dir = self.set_up_work_dir(match &cfg.config.work_dir {
|
||||||
None => {
|
None => {
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
PathBuf::from(format!("{}\\refractr\"", env::var("TEMP").unwrap()))
|
PathBuf::from(format!("{}\\refractr", env::var("TEMP").unwrap()))
|
||||||
} else {
|
} else {
|
||||||
PathBuf::from("/tmp/refractr")
|
PathBuf::from("/tmp/refractr")
|
||||||
}
|
}
|
||||||
|
@ -257,7 +256,10 @@ impl Refractr {
|
||||||
});
|
});
|
||||||
let path_str = match work_dir {
|
let path_str = match work_dir {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(e) => return Err(e)
|
Err(e) => return Err(ReturnData {
|
||||||
|
code: ExitCode::FilesystemError,
|
||||||
|
msg: e
|
||||||
|
})
|
||||||
};
|
};
|
||||||
common::verbose(
|
common::verbose(
|
||||||
self.verbose,
|
self.verbose,
|
||||||
|
@ -265,19 +267,17 @@ impl Refractr {
|
||||||
format!("Created working directory: {}", &path_str));
|
format!("Created working directory: {}", &path_str));
|
||||||
let repo_name = match &cfg.config.from.split("/").last() {
|
let repo_name = match &cfg.config.from.split("/").last() {
|
||||||
Some(split) => split.to_string(),
|
Some(split) => split.to_string(),
|
||||||
None => freak_out!(format!("failed to parse repository name"))
|
None => return Err(ReturnData {
|
||||||
|
code: ExitCode::ParseError,
|
||||||
|
msg: format!("failed to parse repository name")
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
// make initial clone
|
|
||||||
common::verbose(
|
|
||||||
self.verbose,
|
|
||||||
1,
|
|
||||||
format!("Cloning repository: {}", &cfg.config.from));
|
|
||||||
|
|
||||||
let mut builder = RepoBuilder::new();
|
let mut builder = RepoBuilder::new();
|
||||||
let mut cb = RemoteCallbacks::new();
|
let mut cb = RemoteCallbacks::new();
|
||||||
let mut fo = FetchOptions::new();
|
let mut fo = FetchOptions::new();
|
||||||
|
|
||||||
|
// make initial clone
|
||||||
if cfg.config.from.starts_with("ssh://") {
|
if cfg.config.from.starts_with("ssh://") {
|
||||||
let key_string = cfg.config.git.ssh_identity_file.clone();
|
let key_string = cfg.config.git.ssh_identity_file.clone();
|
||||||
cb.credentials(move |_,_,_| Cred::ssh_key(
|
cb.credentials(move |_,_,_| Cred::ssh_key(
|
||||||
|
@ -304,17 +304,28 @@ impl Refractr {
|
||||||
builder.fetch_options(fo);
|
builder.fetch_options(fo);
|
||||||
|
|
||||||
let repo_dir = format!("{}{}{}", &path_str, MAIN_SEPARATOR_STR, repo_name);
|
let repo_dir = format!("{}{}{}", &path_str, MAIN_SEPARATOR_STR, repo_name);
|
||||||
|
common::verbose(
|
||||||
|
self.verbose,
|
||||||
|
1,
|
||||||
|
format!("Cloning repository {} to {}", &cfg.config.from, &repo_dir));
|
||||||
let repo = match builder.clone(&cfg.config.from, Path::new(&repo_dir)) {
|
let repo = match builder.clone(&cfg.config.from, Path::new(&repo_dir)) {
|
||||||
Ok(repo) => repo,
|
Ok(repo) => repo,
|
||||||
Err(_) => {
|
Err(e) => {
|
||||||
|
println!("{}", e);
|
||||||
common::warning(format!("found existing repo at {}, attempting to use", repo_dir));
|
common::warning(format!("found existing repo at {}, attempting to use", repo_dir));
|
||||||
match self.fast_forward(&repo_dir, &cfg.config.branches) {
|
match self.fast_forward(&repo_dir, &cfg.config.branches) {
|
||||||
Ok(_) => if let Ok(repo) = Repository::open(Path::new(&repo_dir)) {
|
Ok(_) => if let Ok(repo) = Repository::open(Path::new(&repo_dir)) {
|
||||||
repo
|
repo
|
||||||
} else {
|
} else {
|
||||||
freak_out!(format!("failed to obtain existing repo"))
|
return Err(ReturnData {
|
||||||
|
code: ExitCode::RepositoryError,
|
||||||
|
msg: format!("failed to obtain existing repo")
|
||||||
|
})
|
||||||
},
|
},
|
||||||
Err(e) => freak_out!(format!("failed to obtain existing repo: {}", e))
|
Err(e) => return Err(ReturnData {
|
||||||
|
code: ExitCode::RepositoryError,
|
||||||
|
msg: format!("failed to obtain existing repo: {}", e)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -324,10 +335,13 @@ impl Refractr {
|
||||||
|
|
||||||
let remotes = match self.make_remotes(&repo, &cfg) {
|
let remotes = match self.make_remotes(&repo, &cfg) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => return Err(e)
|
Err(e) => return Err(ReturnData {
|
||||||
|
code: ExitCode::RemoteError,
|
||||||
|
msg: e
|
||||||
|
})
|
||||||
};
|
};
|
||||||
if let Err(e) = self.push_remotes(&cfg.config, &repo, &remotes) {
|
if let Err(e) = self.push_remotes(&cfg.config, &repo, &remotes) {
|
||||||
return Err(e)
|
common::error(e, ExitCode::PushError);
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.config.schedule.enabled {
|
if cfg.config.schedule.enabled {
|
||||||
|
|
Loading…
Add table
Reference in a new issue