collin/anvil · f32fb115
chore: rename the anvil crate to anvil-cli
Collin Richards · 2026-06-10 08:28 UTC · f32fb115fa2e20b0df7207483eed5c39d7c67b3d · parent cc0e8932 · browse files
modifiedCargo.lock+14 −14
| ⋯ 119 unchanged lines | |||
| 120 | 120 | ] | |
| 121 | 121 | ||
| 122 | 122 | [[package]] | |
| 123 | - | name = "anvil" | |
| 123 | + | name = "anvil-ci" | |
| 124 | 124 | version = "0.0.0" | |
| 125 | 125 | dependencies = [ | |
| 126 | - | "anvil-ci", | |
| 127 | 126 | "anvil-core", | |
| 128 | - | "anvil-ssh", | |
| 129 | - | "anvil-web", | |
| 130 | - | "anyhow", | |
| 131 | - | "clap", | |
| 127 | + | "anvil-git", | |
| 128 | + | "bollard", | |
| 129 | + | "futures-util", | |
| 130 | + | "reqwest", | |
| 131 | + | "serde_json", | |
| 132 | + | "tar", | |
| 132 | 133 | "tokio", | |
| 133 | 134 | "tracing", | |
| 134 | - | "tracing-subscriber", | |
| 135 | 135 | ] | |
| 136 | 136 | ||
| 137 | 137 | [[package]] | |
| 138 | - | name = "anvil-ci" | |
| 138 | + | name = "anvil-cli" | |
| 139 | 139 | version = "0.0.0" | |
| 140 | 140 | dependencies = [ | |
| 141 | + | "anvil-ci", | |
| 141 | 142 | "anvil-core", | |
| 142 | - | "anvil-git", | |
| 143 | - | "bollard", | |
| 144 | - | "futures-util", | |
| 145 | - | "reqwest", | |
| 146 | - | "serde_json", | |
| 147 | - | "tar", | |
| 143 | + | "anvil-ssh", | |
| 144 | + | "anvil-web", | |
| 145 | + | "anyhow", | |
| 146 | + | "clap", | |
| 148 | 147 | "tokio", | |
| 149 | 148 | "tracing", | |
| 149 | + | "tracing-subscriber", | |
| 150 | 150 | ] | |
| 151 | 151 | ||
| 152 | 152 | [[package]] | |
| ⋯ 5115 unchanged lines | |||
addedcrates/anvil-cli/Cargo.toml+24 −0
| 1 | + | [package] | |
| 2 | + | name = "anvil-cli" | |
| 3 | + | version.workspace = true | |
| 4 | + | edition.workspace = true | |
| 5 | + | license.workspace = true | |
| 6 | + | repository.workspace = true | |
| 7 | + | rust-version.workspace = true | |
| 8 | + | description = "anvil git forge daemon and admin CLI." | |
| 9 | + | default-run = "anvild" | |
| 10 | + | ||
| 11 | + | [[bin]] | |
| 12 | + | name = "anvild" | |
| 13 | + | path = "src/main.rs" | |
| 14 | + | ||
| 15 | + | [dependencies] | |
| 16 | + | anvil-core.workspace = true | |
| 17 | + | anvil-web.workspace = true | |
| 18 | + | anvil-ssh.workspace = true | |
| 19 | + | anvil-ci.workspace = true | |
| 20 | + | tokio.workspace = true | |
| 21 | + | clap.workspace = true | |
| 22 | + | anyhow.workspace = true | |
| 23 | + | tracing.workspace = true | |
| 24 | + | tracing-subscriber.workspace = true |
addedcrates/anvil-cli/src/main.rs+221 −0
| 1 | + | //! `anvild` — the anvil git forge daemon and admin CLI. | |
| 2 | + | ||
| 3 | + | use anvil_core::{ | |
| 4 | + | App, | |
| 5 | + | Config, | |
| 6 | + | repos, | |
| 7 | + | ssh_keys, | |
| 8 | + | users, | |
| 9 | + | }; | |
| 10 | + | use anyhow::{ | |
| 11 | + | Context, | |
| 12 | + | Result, | |
| 13 | + | }; | |
| 14 | + | use clap::{ | |
| 15 | + | Parser, | |
| 16 | + | Subcommand, | |
| 17 | + | }; | |
| 18 | + | ||
| 19 | + | #[derive(Parser)] | |
| 20 | + | #[command(name = "anvild", version, about = "anvil git forge")] | |
| 21 | + | struct Cli { | |
| 22 | + | /// Path to the configuration file (TOML). Defaults are used if absent. | |
| 23 | + | #[arg(long, short, default_value = "anvil.toml", global = true)] | |
| 24 | + | config: String, | |
| 25 | + | ||
| 26 | + | /// Override the data directory from config. | |
| 27 | + | #[arg(long, global = true)] | |
| 28 | + | data_dir: Option<String>, | |
| 29 | + | ||
| 30 | + | #[command(subcommand)] | |
| 31 | + | command: Option<Command>, | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | #[derive(Subcommand)] | |
| 35 | + | enum Command { | |
| 36 | + | /// Run the server (default). | |
| 37 | + | Serve, | |
| 38 | + | /// Apply database migrations and exit. | |
| 39 | + | Migrate, | |
| 40 | + | /// Manage users. | |
| 41 | + | User { | |
| 42 | + | #[command(subcommand)] | |
| 43 | + | command: UserCommand, | |
| 44 | + | }, | |
| 45 | + | /// Manage repositories. | |
| 46 | + | Repo { | |
| 47 | + | #[command(subcommand)] | |
| 48 | + | command: RepoCommand, | |
| 49 | + | }, | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | #[derive(Subcommand)] | |
| 53 | + | enum UserCommand { | |
| 54 | + | /// Create a new user. | |
| 55 | + | Create { | |
| 56 | + | username: String, | |
| 57 | + | #[arg(long, default_value = "")] | |
| 58 | + | email: String, | |
| 59 | + | #[arg(long)] | |
| 60 | + | password: String, | |
| 61 | + | #[arg(long)] | |
| 62 | + | admin: bool, | |
| 63 | + | }, | |
| 64 | + | /// Register an SSH public key for a user (for git-over-SSH access). | |
| 65 | + | AddKey { | |
| 66 | + | username: String, | |
| 67 | + | /// The OpenSSH public key line. Mutually exclusive with --key-file. | |
| 68 | + | #[arg(long)] | |
| 69 | + | key: Option<String>, | |
| 70 | + | /// Path to a `.pub` file (e.g. ~/.ssh/id_ed25519.pub). | |
| 71 | + | #[arg(long)] | |
| 72 | + | key_file: Option<String>, | |
| 73 | + | #[arg(long, default_value = "")] | |
| 74 | + | title: String, | |
| 75 | + | }, | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | #[derive(Subcommand)] | |
| 79 | + | enum RepoCommand { | |
| 80 | + | /// Create a repository, given as `owner/name`. | |
| 81 | + | Create { | |
| 82 | + | /// Repository in `owner/name` form. | |
| 83 | + | path: String, | |
| 84 | + | #[arg(long, default_value = "")] | |
| 85 | + | description: String, | |
| 86 | + | #[arg(long)] | |
| 87 | + | private: bool, | |
| 88 | + | }, | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | #[tokio::main] | |
| 92 | + | async fn main() -> Result<()> { | |
| 93 | + | tracing_subscriber::fmt() | |
| 94 | + | .with_env_filter( | |
| 95 | + | tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()), | |
| 96 | + | ) | |
| 97 | + | .init(); | |
| 98 | + | ||
| 99 | + | let cli = Cli::parse(); | |
| 100 | + | ||
| 101 | + | let mut config = Config::load_or_default(&cli.config) | |
| 102 | + | .with_context(|| format!("loading config from {}", cli.config))?; | |
| 103 | + | if let Some(dir) = &cli.data_dir { | |
| 104 | + | config.data_dir = dir.into(); | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | match cli.command.unwrap_or(Command::Serve) { | |
| 108 | + | Command::Serve => serve(config).await, | |
| 109 | + | Command::Migrate => migrate(config).await, | |
| 110 | + | Command::User { command } => user(config, command).await, | |
| 111 | + | Command::Repo { command } => repo(config, command).await, | |
| 112 | + | } | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | async fn serve(config: Config) -> Result<()> { | |
| 116 | + | let mut app = App::bootstrap(config).await?; | |
| 117 | + | ||
| 118 | + | // Start the CI runner: it drains queued runs and processes new ones pushed | |
| 119 | + | // through `app.ci_tx` (set here so handlers can notify it). | |
| 120 | + | let (ci_tx, ci_rx) = tokio::sync::mpsc::unbounded_channel(); | |
| 121 | + | app.ci_tx = Some(ci_tx); | |
| 122 | + | tokio::spawn(anvil_ci::run_worker(app.clone(), ci_rx)); | |
| 123 | + | ||
| 124 | + | if app.config.ssh.enabled { | |
| 125 | + | // Run the HTTP and SSH servers concurrently; if either exits, stop. | |
| 126 | + | tokio::try_join!(anvil_web::serve(app.clone()), anvil_ssh::serve(app))?; | |
| 127 | + | } else { | |
| 128 | + | anvil_web::serve(app).await?; | |
| 129 | + | } | |
| 130 | + | Ok(()) | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | async fn migrate(config: Config) -> Result<()> { | |
| 134 | + | App::bootstrap(config).await?; | |
| 135 | + | println!("migrations applied"); | |
| 136 | + | Ok(()) | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | async fn user(config: Config, command: UserCommand) -> Result<()> { | |
| 140 | + | let app = App::bootstrap(config).await?; | |
| 141 | + | match command { | |
| 142 | + | UserCommand::Create { | |
| 143 | + | username, | |
| 144 | + | email, | |
| 145 | + | password, | |
| 146 | + | admin, | |
| 147 | + | } => { | |
| 148 | + | let user = users::create(&app.db, &username, &email, &password, admin).await?; | |
| 149 | + | println!( | |
| 150 | + | "created user {} (id {}){}", | |
| 151 | + | user.username, | |
| 152 | + | user.id, | |
| 153 | + | if user.is_admin { " [admin]" } else { "" } | |
| 154 | + | ); | |
| 155 | + | } | |
| 156 | + | UserCommand::AddKey { | |
| 157 | + | username, | |
| 158 | + | key, | |
| 159 | + | key_file, | |
| 160 | + | title, | |
| 161 | + | } => { | |
| 162 | + | let user = users::find_by_username(&app.db, &username) | |
| 163 | + | .await? | |
| 164 | + | .with_context(|| format!("no such user: {username}"))?; | |
| 165 | + | let openssh = match (key, key_file) { | |
| 166 | + | (Some(k), None) => k, | |
| 167 | + | (None, Some(path)) => std::fs::read_to_string(&path) | |
| 168 | + | .with_context(|| format!("reading key file {path}"))?, | |
| 169 | + | (Some(_), Some(_)) => anyhow::bail!("pass only one of --key / --key-file"), | |
| 170 | + | (None, None) => anyhow::bail!("pass --key or --key-file"), | |
| 171 | + | }; | |
| 172 | + | let (fingerprint, content) = ssh_keys::parse_public_key(&openssh)?; | |
| 173 | + | let saved = ssh_keys::add(&app.db, user.id, &title, &fingerprint, &content).await?; | |
| 174 | + | println!( | |
| 175 | + | "added ssh key for {} ({})", | |
| 176 | + | user.username, saved.fingerprint | |
| 177 | + | ); | |
| 178 | + | } | |
| 179 | + | } | |
| 180 | + | Ok(()) | |
| 181 | + | } | |
| 182 | + | ||
| 183 | + | async fn repo(config: Config, command: RepoCommand) -> Result<()> { | |
| 184 | + | let app = App::bootstrap(config).await?; | |
| 185 | + | match command { | |
| 186 | + | RepoCommand::Create { | |
| 187 | + | path, | |
| 188 | + | description, | |
| 189 | + | private, | |
| 190 | + | } => { | |
| 191 | + | let (owner_name, name) = path | |
| 192 | + | .split_once('/') | |
| 193 | + | .context("repository path must be in `owner/name` form")?; | |
| 194 | + | let owner = users::find_by_username(&app.db, owner_name) | |
| 195 | + | .await? | |
| 196 | + | .with_context(|| format!("no such user: {owner_name}"))?; | |
| 197 | + | let repo = repos::create( | |
| 198 | + | &app.db, | |
| 199 | + | &app.config.repositories_dir(), | |
| 200 | + | &owner, | |
| 201 | + | name, | |
| 202 | + | &description, | |
| 203 | + | private, | |
| 204 | + | ) | |
| 205 | + | .await?; | |
| 206 | + | println!( | |
| 207 | + | "created repository {}/{} (id {}) at {}", | |
| 208 | + | owner.username, | |
| 209 | + | repo.name, | |
| 210 | + | repo.id, | |
| 211 | + | anvil_core::storage::repo_path( | |
| 212 | + | &app.config.repositories_dir(), | |
| 213 | + | &owner.username, | |
| 214 | + | name | |
| 215 | + | ) | |
| 216 | + | .display() | |
| 217 | + | ); | |
| 218 | + | } | |
| 219 | + | } | |
| 220 | + | Ok(()) | |
| 221 | + | } |
deletedcrates/anvil/Cargo.toml+0 −24
| 1 | - | [package] | |
| 2 | - | name = "anvil" | |
| 3 | - | version.workspace = true | |
| 4 | - | edition.workspace = true | |
| 5 | - | license.workspace = true | |
| 6 | - | repository.workspace = true | |
| 7 | - | rust-version.workspace = true | |
| 8 | - | description = "anvil git forge daemon and admin CLI." | |
| 9 | - | default-run = "anvild" | |
| 10 | - | ||
| 11 | - | [[bin]] | |
| 12 | - | name = "anvild" | |
| 13 | - | path = "src/main.rs" | |
| 14 | - | ||
| 15 | - | [dependencies] | |
| 16 | - | anvil-core.workspace = true | |
| 17 | - | anvil-web.workspace = true | |
| 18 | - | anvil-ssh.workspace = true | |
| 19 | - | anvil-ci.workspace = true | |
| 20 | - | tokio.workspace = true | |
| 21 | - | clap.workspace = true | |
| 22 | - | anyhow.workspace = true | |
| 23 | - | tracing.workspace = true | |
| 24 | - | tracing-subscriber.workspace = true |
deletedcrates/anvil/src/main.rs+0 −221
| 1 | - | //! `anvild` — the anvil git forge daemon and admin CLI. | |
| 2 | - | ||
| 3 | - | use anvil_core::{ | |
| 4 | - | App, | |
| 5 | - | Config, | |
| 6 | - | repos, | |
| 7 | - | ssh_keys, | |
| 8 | - | users, | |
| 9 | - | }; | |
| 10 | - | use anyhow::{ | |
| 11 | - | Context, | |
| 12 | - | Result, | |
| 13 | - | }; | |
| 14 | - | use clap::{ | |
| 15 | - | Parser, | |
| 16 | - | Subcommand, | |
| 17 | - | }; | |
| 18 | - | ||
| 19 | - | #[derive(Parser)] | |
| 20 | - | #[command(name = "anvild", version, about = "anvil git forge")] | |
| 21 | - | struct Cli { | |
| 22 | - | /// Path to the configuration file (TOML). Defaults are used if absent. | |
| 23 | - | #[arg(long, short, default_value = "anvil.toml", global = true)] | |
| 24 | - | config: String, | |
| 25 | - | ||
| 26 | - | /// Override the data directory from config. | |
| 27 | - | #[arg(long, global = true)] | |
| 28 | - | data_dir: Option<String>, | |
| 29 | - | ||
| 30 | - | #[command(subcommand)] | |
| 31 | - | command: Option<Command>, | |
| 32 | - | } | |
| 33 | - | ||
| 34 | - | #[derive(Subcommand)] | |
| 35 | - | enum Command { | |
| 36 | - | /// Run the server (default). | |
| 37 | - | Serve, | |
| 38 | - | /// Apply database migrations and exit. | |
| 39 | - | Migrate, | |
| 40 | - | /// Manage users. | |
| 41 | - | User { | |
| 42 | - | #[command(subcommand)] | |
| 43 | - | command: UserCommand, | |
| 44 | - | }, | |
| 45 | - | /// Manage repositories. | |
| 46 | - | Repo { | |
| 47 | - | #[command(subcommand)] | |
| 48 | - | command: RepoCommand, | |
| 49 | - | }, | |
| 50 | - | } | |
| 51 | - | ||
| 52 | - | #[derive(Subcommand)] | |
| 53 | - | enum UserCommand { | |
| 54 | - | /// Create a new user. | |
| 55 | - | Create { | |
| 56 | - | username: String, | |
| 57 | - | #[arg(long, default_value = "")] | |
| 58 | - | email: String, | |
| 59 | - | #[arg(long)] | |
| 60 | - | password: String, | |
| 61 | - | #[arg(long)] | |
| 62 | - | admin: bool, | |
| 63 | - | }, | |
| 64 | - | /// Register an SSH public key for a user (for git-over-SSH access). | |
| 65 | - | AddKey { | |
| 66 | - | username: String, | |
| 67 | - | /// The OpenSSH public key line. Mutually exclusive with --key-file. | |
| 68 | - | #[arg(long)] | |
| 69 | - | key: Option<String>, | |
| 70 | - | /// Path to a `.pub` file (e.g. ~/.ssh/id_ed25519.pub). | |
| 71 | - | #[arg(long)] | |
| 72 | - | key_file: Option<String>, | |
| 73 | - | #[arg(long, default_value = "")] | |
| 74 | - | title: String, | |
| 75 | - | }, | |
| 76 | - | } | |
| 77 | - | ||
| 78 | - | #[derive(Subcommand)] | |
| 79 | - | enum RepoCommand { | |
| 80 | - | /// Create a repository, given as `owner/name`. | |
| 81 | - | Create { | |
| 82 | - | /// Repository in `owner/name` form. | |
| 83 | - | path: String, | |
| 84 | - | #[arg(long, default_value = "")] | |
| 85 | - | description: String, | |
| 86 | - | #[arg(long)] | |
| 87 | - | private: bool, | |
| 88 | - | }, | |
| 89 | - | } | |
| 90 | - | ||
| 91 | - | #[tokio::main] | |
| 92 | - | async fn main() -> Result<()> { | |
| 93 | - | tracing_subscriber::fmt() | |
| 94 | - | .with_env_filter( | |
| 95 | - | tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()), | |
| 96 | - | ) | |
| 97 | - | .init(); | |
| 98 | - | ||
| 99 | - | let cli = Cli::parse(); | |
| 100 | - | ||
| 101 | - | let mut config = Config::load_or_default(&cli.config) | |
| 102 | - | .with_context(|| format!("loading config from {}", cli.config))?; | |
| 103 | - | if let Some(dir) = &cli.data_dir { | |
| 104 | - | config.data_dir = dir.into(); | |
| 105 | - | } | |
| 106 | - | ||
| 107 | - | match cli.command.unwrap_or(Command::Serve) { | |
| 108 | - | Command::Serve => serve(config).await, | |
| 109 | - | Command::Migrate => migrate(config).await, | |
| 110 | - | Command::User { command } => user(config, command).await, | |
| 111 | - | Command::Repo { command } => repo(config, command).await, | |
| 112 | - | } | |
| 113 | - | } | |
| 114 | - | ||
| 115 | - | async fn serve(config: Config) -> Result<()> { | |
| 116 | - | let mut app = App::bootstrap(config).await?; | |
| 117 | - | ||
| 118 | - | // Start the CI runner: it drains queued runs and processes new ones pushed | |
| 119 | - | // through `app.ci_tx` (set here so handlers can notify it). | |
| 120 | - | let (ci_tx, ci_rx) = tokio::sync::mpsc::unbounded_channel(); | |
| 121 | - | app.ci_tx = Some(ci_tx); | |
| 122 | - | tokio::spawn(anvil_ci::run_worker(app.clone(), ci_rx)); | |
| 123 | - | ||
| 124 | - | if app.config.ssh.enabled { | |
| 125 | - | // Run the HTTP and SSH servers concurrently; if either exits, stop. | |
| 126 | - | tokio::try_join!(anvil_web::serve(app.clone()), anvil_ssh::serve(app))?; | |
| 127 | - | } else { | |
| 128 | - | anvil_web::serve(app).await?; | |
| 129 | - | } | |
| 130 | - | Ok(()) | |
| 131 | - | } | |
| 132 | - | ||
| 133 | - | async fn migrate(config: Config) -> Result<()> { | |
| 134 | - | App::bootstrap(config).await?; | |
| 135 | - | println!("migrations applied"); | |
| 136 | - | Ok(()) | |
| 137 | - | } | |
| 138 | - | ||
| 139 | - | async fn user(config: Config, command: UserCommand) -> Result<()> { | |
| 140 | - | let app = App::bootstrap(config).await?; | |
| 141 | - | match command { | |
| 142 | - | UserCommand::Create { | |
| 143 | - | username, | |
| 144 | - | email, | |
| 145 | - | password, | |
| 146 | - | admin, | |
| 147 | - | } => { | |
| 148 | - | let user = users::create(&app.db, &username, &email, &password, admin).await?; | |
| 149 | - | println!( | |
| 150 | - | "created user {} (id {}){}", | |
| 151 | - | user.username, | |
| 152 | - | user.id, | |
| 153 | - | if user.is_admin { " [admin]" } else { "" } | |
| 154 | - | ); | |
| 155 | - | } | |
| 156 | - | UserCommand::AddKey { | |
| 157 | - | username, | |
| 158 | - | key, | |
| 159 | - | key_file, | |
| 160 | - | title, | |
| 161 | - | } => { | |
| 162 | - | let user = users::find_by_username(&app.db, &username) | |
| 163 | - | .await? | |
| 164 | - | .with_context(|| format!("no such user: {username}"))?; | |
| 165 | - | let openssh = match (key, key_file) { | |
| 166 | - | (Some(k), None) => k, | |
| 167 | - | (None, Some(path)) => std::fs::read_to_string(&path) | |
| 168 | - | .with_context(|| format!("reading key file {path}"))?, | |
| 169 | - | (Some(_), Some(_)) => anyhow::bail!("pass only one of --key / --key-file"), | |
| 170 | - | (None, None) => anyhow::bail!("pass --key or --key-file"), | |
| 171 | - | }; | |
| 172 | - | let (fingerprint, content) = ssh_keys::parse_public_key(&openssh)?; | |
| 173 | - | let saved = ssh_keys::add(&app.db, user.id, &title, &fingerprint, &content).await?; | |
| 174 | - | println!( | |
| 175 | - | "added ssh key for {} ({})", | |
| 176 | - | user.username, saved.fingerprint | |
| 177 | - | ); | |
| 178 | - | } | |
| 179 | - | } | |
| 180 | - | Ok(()) | |
| 181 | - | } | |
| 182 | - | ||
| 183 | - | async fn repo(config: Config, command: RepoCommand) -> Result<()> { | |
| 184 | - | let app = App::bootstrap(config).await?; | |
| 185 | - | match command { | |
| 186 | - | RepoCommand::Create { | |
| 187 | - | path, | |
| 188 | - | description, | |
| 189 | - | private, | |
| 190 | - | } => { | |
| 191 | - | let (owner_name, name) = path | |
| 192 | - | .split_once('/') | |
| 193 | - | .context("repository path must be in `owner/name` form")?; | |
| 194 | - | let owner = users::find_by_username(&app.db, owner_name) | |
| 195 | - | .await? | |
| 196 | - | .with_context(|| format!("no such user: {owner_name}"))?; | |
| 197 | - | let repo = repos::create( | |
| 198 | - | &app.db, | |
| 199 | - | &app.config.repositories_dir(), | |
| 200 | - | &owner, | |
| 201 | - | name, | |
| 202 | - | &description, | |
| 203 | - | private, | |
| 204 | - | ) | |
| 205 | - | .await?; | |
| 206 | - | println!( | |
| 207 | - | "created repository {}/{} (id {}) at {}", | |
| 208 | - | owner.username, | |
| 209 | - | repo.name, | |
| 210 | - | repo.id, | |
| 211 | - | anvil_core::storage::repo_path( | |
| 212 | - | &app.config.repositories_dir(), | |
| 213 | - | &owner.username, | |
| 214 | - | name | |
| 215 | - | ) | |
| 216 | - | .display() | |
| 217 | - | ); | |
| 218 | - | } | |
| 219 | - | } | |
| 220 | - | Ok(()) | |
| 221 | - | } |