Skip to content

Commit

Permalink
feat: interactive configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
benpueschel committed Jul 7, 2024
1 parent df6d042 commit 85677d5
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 12 deletions.
36 changes: 28 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,7 @@ rather a tool to let you focus on what's important: your code.
# Quick Start

1. Install gritty using the [precompiled binaries](#precompiled-binaries) or by building from source.
2. Create a configuration file by running `gritty create-config`.
3. Add your remotes to the configuration file.
4. If you want to change the default `Keyring` secret storage, you can edit the
configuration file as described in the [Configuration](#configuration) section.
5. Authenticate with a remote using `gritty auth [remote]`.
This will prompt you for your username and password. Leave the username blank
to use an access token (required at this point in time). Gritty will store the
token in whatever secret storage you specified in the configuration file.
2. Interactively configure gritty by running `gritty create-config` (See [Create-Config](#create-config)).

Now you can use gritty to manage your repositories!
See the [Usage](#usage) and [Examples](#Examples) sections for more information
Expand Down Expand Up @@ -79,6 +72,33 @@ To see the available options for a subcommand, run:
gritty help [subcommand]
```

## Create-Config

The `create-config` subcommand will ask you a series of questions to create a
configuration file for gritty. This file is located at `~/.config/gritty/config.toml`
by default, but you can specify a different location if you like.

The questions are as follows:
1. Enter the config file path (default: ~/.config/gritty/config.toml). Leave blank for default.
2. How do you want to store your access tokens? (Keyring, SecretsFile, Plaintext)
- Keyring: uses the system keyring to store access tokens (HIGHLY RECOMMENDED).
- SecretsFile: stores access tokens in a plaintext file.
- Plaintext: stores access tokens directly in the config file.
3. Do you want to add a remote? (y/n). Answer 'y' to configure remotes.
- Enter the remote name (e.g. github, gh, gitea, awesome-sauce).
- Enter the provider (GitHub, Gitea).
- Enter the remote URL (e.g. https://github.com, https://gitea.example.com).
- Enter your username for the remote.
- Enter the clone protocol (ssh, https).
- Do you want to add authentication for this remote? (y/n). Answer 'y' to authenticate.
- Enter your token.
- Do you want to add another remote? (y/n). Answer 'y' to add another remote.
The process will repeat until you answer 'n'.

The configuration file will be automatically created at the specified path, if you've
added authentication for your remotes in the previous step, you can begin using gritty
right away.

After creating your configuration file, you can authenticate with a remote using:
```bash
gritty auth [remote]
Expand Down
2 changes: 1 addition & 1 deletion src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use structopt::StructOpt;
#[derive(Debug, Clone, StructOpt)]
#[structopt(name = "gritty", about = "A tool to manage remote git repositories.")]
pub enum Args {
#[structopt(about = "Create a default config")]
#[structopt(about = "Interactively configure gritty")]
CreateConfig,
#[structopt(about = "List repositories on a remote")]
List {
Expand Down
183 changes: 180 additions & 3 deletions src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::env;
use std::io::{stdin, stdout, Write};

use crate::config::Config;
use crate::config::{AuthConfig, Config, GitRemoteConfig, InlineSecrets, Secrets};
use crate::error::{Error, ErrorKind, Result};
use crate::log;
use crate::remote::{self, Remote, RepoCreateInfo};
Expand Down Expand Up @@ -38,11 +39,187 @@ fn get_input() -> Result<String> {
}

pub async fn create_config() -> Result<()> {
log::println("Creating default config...");
Config::save_default()?;
log::highlight("Welcome to ", "gritty", "!");
log::println("This command will ask you some questions to create a config file.");
log::end_line();

let mut config = Config::default();

// Config file path

log::print("Enter the path to the config file (default is '");
log::info(&config.path);
log::print("'): ");
let path = get_input()?;
if !path.is_empty() {
config.path = path;
}
log::end_line();

// Token storage

log::println("How do you want to store your tokens? (leave blank for default)");
#[cfg(feature = "keyring")]
let secrets = {
log::info("1. ");
log::print("Use the system keyring (");
log::info("highly recommended, default");
log::println(")");

log::highlight("", "2. ", "In a plaintext secrets file");
log::highlight("", "3. ", "In the config file");

log::print("> ");
let num = get_input()?;
match num.as_str() {
"2" => ask_for_secrets_file()?,
"3" => Secrets::Plaintext(InlineSecrets::default()),
_ => Secrets::Keyring,
}
};
#[cfg(not(feature = "keyring"))]
let secrets = {
log::info("1. ");
log::print("In a plaintext secrets file (");
log::info("default");
log::println(")");

log::highlight("", "2. ", "In the config file");

log::print("> ");
let num = get_input()?;
match num.as_str() {
"2" => Secrets::Plaintext(InlineSecrets::default()),
_ => ask_for_secrets_file()?,
}
};

config.secrets = secrets;
log::println("Token storage method set.");
log::end_line();

// Remotes

log::print("Do you want to add a remote? (y/N): ");
loop {
let input = get_input()?;
if input.eq_ignore_ascii_case("y") {
let (name, remote, token) = ask_for_remote()?;

config.remotes.insert(name.clone(), remote);
if let Some(token) = token {
// TODO: don't unwrap, also check for basic auth if ever supported
config.store_token(&name, &token.token.unwrap())?;
}

log::println("Remote added.");
log::print("Do you want to add another remote? (y/N): ");
continue;
}
// If the user didn't enter 'y', assume they meant 'n' and exit the loop
break;
}

log::println("Saving config...");
config.save()?;
Ok(())
}

fn ask_for_remote() -> Result<(String, GitRemoteConfig, Option<AuthConfig>)> {
log::print("Enter the name of the remote: ");
let name = get_input()?;
if name.is_empty() {
return Err(Error::other("Remote name cannot be empty."));
}
log::print("Enter the provider for the remote (");
log::info("github/gitea");
log::print("): ");
let provider = get_input()?;
if provider.is_empty() {
return Err(Error::other("Remote provider cannot be empty."));
}
let provider = match provider.to_lowercase().as_str() {
"github" => remote::Provider::GitHub,
"gitea" => remote::Provider::Gitea,
_ => {
return Err(Error::other(
"Remote provider must be either 'github' or 'gitea'.",
));
}
};

log::print("Enter the URL of the remote: ");
let url = get_input()?;
if url.is_empty() {
return Err(Error::other("Remote URL cannot be empty."));
}
log::print("Enter the username for the remote: ");
let username = get_input()?;
if username.is_empty() {
return Err(Error::other("Remote username cannot be empty."));
}

log::print("Enter the clone protocol ");
log::info("ssh/https");
log::print("): ");
let clone_protocol = get_input()?;
if clone_protocol.is_empty() {
return Err(Error::other("Clone protocol cannot be empty."));
}
let clone_protocol = match clone_protocol.to_lowercase().as_str() {
"ssh" => remote::CloneProtocol::SSH,
"https" => remote::CloneProtocol::HTTPS,
_ => {
return Err(Error::other(
"Clone protocol must be either 'ssh' or 'https'.",
));
}
};

// Authentication
log::print("Do you want to add authentication to this remote? (y/N): ");
let auth = get_input()?;
let auth = if auth.eq_ignore_ascii_case("y") {
log::print("Enter token: ");
stdout().flush()?;
let token = rpassword::read_password()?;
log::highlight("Token added to remote '", &name, "'.");
Some(AuthConfig {
username: None,
password: None,
token: Some(token),
})
} else {
None
};

Ok((
name,
GitRemoteConfig {
clone_protocol,
provider,
username,
url,
},
auth,
))
}

fn ask_for_secrets_file() -> Result<Secrets> {
let home = env::var("HOME").unwrap();
let xdg_config = env::var("XDG_CONFIG_HOME").unwrap_or(format!("{home}/.config"));

let mut path = format!("{xdg_config}/gritty/secrets.toml");
log::print("Enter the path to the secrets file (default is '");
log::info(&path);
log::print("'): ");
let input = get_input()?;
if !input.is_empty() {
path = input;
}
Ok(Secrets::SecretsFile(path))
}

pub async fn list_repositories(remote: &str) -> Result<()> {
log::print("Listing repositories on remote '");
log::info(remote);
Expand Down

0 comments on commit 85677d5

Please sign in to comment.