Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ authors = [
"Finley Thomalla <finley@thomalla.ch>",
"Danny Tatom <its.danny@hey.com>",
]
description = "An interactive CLI for creating conventional commits."
documentation = "https://docs.rs/koji"
description = "An interactive CLI for creating conventional commits."
repository = "https://github.com/cococonscious/koji"
license = "MIT"

Expand Down Expand Up @@ -35,10 +35,10 @@ xdg = "3.0"
gix = "0.80.0"

[dev-dependencies]
git2 = "0.20.4"
assert_cmd = "2.0.16"
predicates = "3.1.2"
tempfile = "3.12.0"
git2 = "0.20.3"

[target.'cfg(not(windows))'.dev-dependencies]
rexpect = "0.6.2"
Expand Down
2 changes: 2 additions & 0 deletions meta/config/default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ breaking_changes = true
issues = true
emoji = false
sign = false
force_scope = false
allow_empty_scope = true

[[commit_types]]
name = "feat"
Expand Down
1 change: 1 addition & 0 deletions src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ fn main() -> Result<()> {
sign,
_user_config_path: None,
_current_dir: Some(current_dir.clone()),
..Default::default()
}))?;

// Get answers from interactive prompt
Expand Down
70 changes: 70 additions & 0 deletions src/lib/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ pub struct Config {
pub autocomplete: bool,
pub breaking_changes: bool,
pub commit_types: IndexMap<String, CommitType>,
pub commit_scopes: IndexMap<String, CommitScope>,
pub emoji: bool,
pub issues: bool,
pub sign: bool,
pub force_scope: bool,
pub allow_empty_scope: bool,
pub workdir: PathBuf,
}

Expand All @@ -26,15 +29,25 @@ pub struct CommitType {
pub name: String,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
pub struct CommitScope {
pub name: String,
pub description: Option<String>,
}

#[derive(Clone, Debug, Deserialize)]
struct ConfigTOML {
pub autocomplete: bool,
pub breaking_changes: bool,
#[serde(default)]
commit_types: Vec<CommitType>,
#[serde(default)]
commit_scopes: Vec<CommitScope>,
pub emoji: bool,
pub issues: bool,
pub sign: bool,
pub force_scope: bool,
pub allow_empty_scope: bool,
}

#[derive(Default)]
Expand All @@ -45,6 +58,8 @@ pub struct ConfigArgs {
pub emoji: Option<bool>,
pub issues: Option<bool>,
pub sign: Option<bool>,
pub force_scope: Option<bool>,
pub allow_empty_scope: Option<bool>,
pub _user_config_path: Option<PathBuf>,
pub _current_dir: Option<PathBuf>,
}
Expand All @@ -59,6 +74,8 @@ impl Config {
emoji,
issues,
sign,
force_scope,
allow_empty_scope,
_user_config_path,
_current_dir,
} = args.unwrap_or_default();
Expand Down Expand Up @@ -101,13 +118,22 @@ impl Config {
commit_types.insert(commit_type.name.clone(), commit_type.to_owned());
}

// Gather up commit scopes
let mut commit_scopes = IndexMap::new();
for commit_scope in config.commit_scopes.iter() {
commit_scopes.insert(commit_scope.name.clone(), commit_scope.to_owned());
}

Ok(Config {
autocomplete: autocomplete.unwrap_or(config.autocomplete),
breaking_changes: breaking_changes.unwrap_or(config.breaking_changes),
commit_types,
commit_scopes,
emoji: emoji.unwrap_or(config.emoji),
issues: issues.unwrap_or(config.issues),
sign: sign.unwrap_or(config.sign),
force_scope: force_scope.unwrap_or(config.force_scope),
allow_empty_scope: allow_empty_scope.unwrap_or(config.allow_empty_scope),
workdir,
})
}
Expand Down Expand Up @@ -285,4 +311,48 @@ mod tests {

Ok(())
}

#[test]
fn test_commit_scopes() -> Result<(), Box<dyn Error>> {
let tempdir = tempfile::tempdir()?;
std::fs::write(
tempdir.path().join(".koji.toml"),
"[[commit_scopes]]\nname=\"app\"\ndescription=\"Application code\"",
)?;
let config = Config::new(Some(ConfigArgs {
_current_dir: Some(tempdir.path().to_path_buf()),
..Default::default()
}))?;
assert!(config.commit_scopes.get("app").is_some());
assert_eq!(
config.commit_scopes.get("app"),
Some(&CommitScope {
name: "app".into(),
description: Some("Application code".into())
})
);
tempdir.close()?;
Ok(())
}
#[test]
fn test_commit_scopes_from_config() -> Result<(), Box<dyn Error>> {
let tempdir_config = tempfile::tempdir()?;
std::fs::create_dir(tempdir_config.path().join("koji"))?;
std::fs::write(
tempdir_config.path().join("koji").join("config.toml"),
"[[commit_scopes]]\nname=\"server\"\ndescription=\"Server code\"\n[[commit_scopes]]\nname=\"shared\"",
)?;
let tempdir_current = tempfile::tempdir()?;
let config = Config::new(Some(ConfigArgs {
_user_config_path: Some(tempdir_config.path().to_path_buf()),
_current_dir: Some(tempdir_current.path().to_path_buf()),
..Default::default()
}))?;
assert!(config.commit_scopes.get("server").is_some());
assert!(config.commit_scopes.get("shared").is_some());
assert_eq!(config.commit_scopes.len(), 2);
tempdir_current.close()?;
tempdir_config.close()?;
Ok(())
}
}
50 changes: 45 additions & 5 deletions src/lib/questions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ fn prompt_type(config: &Config) -> Result<String> {
}

#[derive(Debug, Clone)]
struct ScopeAutocompleter {
config: Config,
pub struct ScopeAutocompleter {
pub config: Config,
}

impl ScopeAutocompleter {
Expand Down Expand Up @@ -124,13 +124,28 @@ impl ScopeAutocompleter {

Ok(scopes)
}

pub fn get_config_scopes(&self) -> Vec<String> {
self.config.commit_scopes.keys().cloned().collect()
}

pub fn get_all_scopes(&self) -> Vec<String> {
let mut scopes = self.get_config_scopes();
let existing_scopes = self.get_existing_scopes().unwrap_or_default();
// Add existing scopes that aren't already in the config scopes
for scope in existing_scopes {
if !scopes.contains(&scope) {
scopes.push(scope);
}
}
scopes
}
}

impl Autocomplete for ScopeAutocompleter {
fn get_suggestions(&mut self, input: &str) -> Result<Vec<String>, CustomUserError> {
let existing_scopes = self.get_existing_scopes().unwrap_or_default();

Ok(existing_scopes
let all_scopes = self.get_all_scopes();
Ok(all_scopes
.iter()
.filter(|s| s.contains(input))
.cloned()
Expand All @@ -148,6 +163,21 @@ impl Autocomplete for ScopeAutocompleter {
}

fn prompt_scope(config: &Config) -> Result<Option<String>> {
if config.force_scope && !config.commit_scopes.is_empty() {
let scope_values: Vec<String> = config.commit_scopes.keys().cloned().collect();

let prompt = Select::new("What's the scope of this change?", scope_values)
.with_render_config(get_render_config());

let result = if config.allow_empty_scope {
prompt.prompt_skippable()?
} else {
Some(prompt.prompt()?)
};

return Ok(result);
}

let mut scope_autocompleter = ScopeAutocompleter {
config: config.clone(),
};
Expand All @@ -173,6 +203,16 @@ fn prompt_scope(config: &Config) -> Result<Option<String>> {
selected_scope = selected_scope.with_autocomplete(scope_autocompleter);
}

if !config.allow_empty_scope {
selected_scope = selected_scope.with_validator(|input: &str| {
if input.trim().is_empty() {
Ok(Validation::Invalid("A scope is required".into()))
} else {
Ok(Validation::Valid)
}
});
}

if let Some(scope) = selected_scope.prompt_skippable()? {
if scope.is_empty() {
return Ok(None);
Expand Down
Loading