diff options
author | Ellie Huxtable <e@elm.sh> | 2021-04-20 17:07:11 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-04-20 16:07:11 +0000 |
commit | 34888827f8a06de835cbe5833a06914f28cce514 (patch) | |
tree | 8b56f20e50065cd2c222d5e8e067ec55cf1947a1 /src | |
parent | f6de558070c4ed4dbecf4bbbf4693e396a5577dc (diff) |
Switch to Warp + SQLx, use async, switch to Rust stable (#36)
* Switch to warp + sql, use async and stable rust
* Update CI to use stable
Diffstat (limited to 'src')
-rw-r--r-- | src/api.rs | 42 | ||||
-rw-r--r-- | src/command/history.rs | 8 | ||||
-rw-r--r-- | src/command/login.rs | 7 | ||||
-rw-r--r-- | src/command/mod.rs | 8 | ||||
-rw-r--r-- | src/command/search.rs | 110 | ||||
-rw-r--r-- | src/command/server.rs | 6 | ||||
-rw-r--r-- | src/command/sync.rs | 4 | ||||
-rw-r--r-- | src/local/api_client.rs | 87 | ||||
-rw-r--r-- | src/local/database.rs | 8 | ||||
-rw-r--r-- | src/local/import.rs | 7 | ||||
-rw-r--r-- | src/local/sync.rs | 36 | ||||
-rw-r--r-- | src/main.rs | 43 | ||||
-rw-r--r-- | src/remote/database.rs | 22 | ||||
-rw-r--r-- | src/remote/mod.rs | 5 | ||||
-rw-r--r-- | src/remote/server.rs | 61 | ||||
-rw-r--r-- | src/remote/views.rs | 185 | ||||
-rw-r--r-- | src/schema.rs | 30 | ||||
-rw-r--r-- | src/server/auth.rs (renamed from src/remote/auth.rs) | 2 | ||||
-rw-r--r-- | src/server/database.rs | 202 | ||||
-rw-r--r-- | src/server/handlers/history.rs | 89 | ||||
-rw-r--r-- | src/server/handlers/mod.rs | 6 | ||||
-rw-r--r-- | src/server/handlers/user.rs | 140 | ||||
-rw-r--r-- | src/server/mod.rs | 23 | ||||
-rw-r--r-- | src/server/models.rs (renamed from src/remote/models.rs) | 43 | ||||
-rw-r--r-- | src/server/router.rs | 121 | ||||
-rw-r--r-- | src/settings.rs | 2 | ||||
-rw-r--r-- | src/shell/atuin.zsh | 1 |
27 files changed, 832 insertions, 466 deletions
@@ -1,8 +1,9 @@ use chrono::Utc; -// This is shared between the client and the server, and has the data structures -// representing the requests/responses for each method. -// TODO: Properly define responses rather than using json! +#[derive(Debug, Serialize, Deserialize)] +pub struct UserResponse { + pub username: String, +} #[derive(Debug, Serialize, Deserialize)] pub struct RegisterRequest { @@ -12,12 +13,22 @@ pub struct RegisterRequest { } #[derive(Debug, Serialize, Deserialize)] +pub struct RegisterResponse { + pub session: String, +} + +#[derive(Debug, Serialize, Deserialize)] pub struct LoginRequest { pub username: String, pub password: String, } #[derive(Debug, Serialize, Deserialize)] +pub struct LoginResponse { + pub session: String, +} + +#[derive(Debug, Serialize, Deserialize)] pub struct AddHistoryRequest { pub id: String, pub timestamp: chrono::DateTime<Utc>, @@ -31,6 +42,29 @@ pub struct CountResponse { } #[derive(Debug, Serialize, Deserialize)] -pub struct ListHistoryResponse { +pub struct SyncHistoryRequest { + pub sync_ts: chrono::DateTime<chrono::FixedOffset>, + pub history_ts: chrono::DateTime<chrono::FixedOffset>, + pub host: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SyncHistoryResponse { pub history: Vec<String>, } + +#[derive(Debug, Serialize, Deserialize)] +pub struct ErrorResponse { + pub reason: String, +} + +impl ErrorResponse { + pub fn reply(reason: &str, status: warp::http::StatusCode) -> impl warp::Reply { + warp::reply::with_status( + warp::reply::json(&ErrorResponse { + reason: String::from(reason), + }), + status, + ) + } +} diff --git a/src/command/history.rs b/src/command/history.rs index 3b4a717c..627efae4 100644 --- a/src/command/history.rs +++ b/src/command/history.rs @@ -53,7 +53,7 @@ fn print_list(h: &[History]) { } impl Cmd { - pub fn run(&self, settings: &Settings, db: &mut impl Database) -> Result<()> { + pub async fn run(&self, settings: &Settings, db: &mut (impl Database + Send)) -> Result<()> { match self { Self::Start { command: words } => { let command = words.join(" "); @@ -69,6 +69,10 @@ impl Cmd { } Self::End { id, exit } => { + if id.trim() == "" { + return Ok(()); + } + let mut h = db.load(id)?; h.exit = *exit; h.duration = chrono::Utc::now().timestamp_nanos() - h.timestamp.timestamp_nanos(); @@ -82,7 +86,7 @@ impl Cmd { } Ok(Fork::Child) => { debug!("running periodic background sync"); - sync::sync(settings, false, db)?; + sync::sync(settings, false, db).await?; } Err(_) => println!("Fork failed"), } diff --git a/src/command/login.rs b/src/command/login.rs index 4f58b77f..636ac0d3 100644 --- a/src/command/login.rs +++ b/src/command/login.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::fs::File; use std::io::prelude::*; -use eyre::Result; +use eyre::{eyre, Result}; use structopt::StructOpt; use crate::settings::Settings; @@ -28,8 +28,13 @@ impl Cmd { let url = format!("{}/login", settings.local.sync_address); let client = reqwest::blocking::Client::new(); + let resp = client.post(url).json(&map).send()?; + if resp.status() != reqwest::StatusCode::OK { + return Err(eyre!("invalid login details")); + } + let session = resp.json::<HashMap<String, String>>()?; let session = session["session"].clone(); diff --git a/src/command/mod.rs b/src/command/mod.rs index eeb11a87..cd857e9f 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -63,16 +63,16 @@ pub fn uuid_v4() -> String { } impl AtuinCmd { - pub fn run(self, db: &mut impl Database, settings: &Settings) -> Result<()> { + pub async fn run<T: Database + Send>(self, db: &mut T, settings: &Settings) -> Result<()> { match self { - Self::History(history) => history.run(settings, db), + Self::History(history) => history.run(settings, db).await, Self::Import(import) => import.run(db), - Self::Server(server) => server.run(settings), + Self::Server(server) => server.run(settings).await, Self::Stats(stats) => stats.run(db, settings), Self::Init => init::init(), Self::Search { query } => search::run(&query, db), - Self::Sync { force } => sync::run(settings, force, db), + Self::Sync { force } => sync::run(settings, force, db).await, Self::Login(l) => l.run(settings), Self::Register(r) => register::run( settings, diff --git a/src/command/search.rs b/src/command/search.rs index b9f3987c..d7b477da 100644 --- a/src/command/search.rs +++ b/src/command/search.rs @@ -1,6 +1,8 @@ use eyre::Result; use itertools::Itertools; use std::io::stdout; +use std::time::Duration; + use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use tui::{ backend::TermionBackend, @@ -26,6 +28,78 @@ struct State { results_state: ListState, } +#[allow(clippy::clippy::cast_sign_loss)] +impl State { + fn durations(&self) -> Vec<String> { + self.results + .iter() + .map(|h| { + let duration = + Duration::from_millis(std::cmp::max(h.duration, 0) as u64 / 1_000_000); + let duration = humantime::format_duration(duration).to_string(); + let duration: Vec<&str> = duration.split(' ').collect(); + + duration[0].to_string() + }) + .collect() + } + + fn render_results<T: tui::backend::Backend>( + &mut self, + f: &mut tui::Frame<T>, + r: tui::layout::Rect, + ) { + let durations = self.durations(); + let max_length = durations + .iter() + .fold(0, |largest, i| std::cmp::max(largest, i.len())); + + let results: Vec<ListItem> = self + .results + .iter() + .enumerate() + .map(|(i, m)| { + let command = m.command.to_string().replace("\n", " ").replace("\t", " "); + + let mut command = Span::raw(command); + + let mut duration = durations[i].clone(); + + while duration.len() < max_length { + duration.push(' '); + } + + let duration = Span::styled( + duration, + Style::default().fg(if m.exit == 0 || m.duration == -1 { + Color::Green + } else { + Color::Red + }), + ); + + if let Some(selected) = self.results_state.selected() { + if selected == i { + command.style = + Style::default().fg(Color::Red).add_modifier(Modifier::BOLD); + } + } + + let spans = Spans::from(vec![duration, Span::raw(" "), command]); + + ListItem::new(spans) + }) + .collect(); + + let results = List::new(results) + .block(Block::default().borders(Borders::ALL).title("History")) + .start_corner(Corner::BottomLeft) + .highlight_symbol(">> "); + + f.render_stateful_widget(results, r, &mut self.results_state); + } +} + fn query_results(app: &mut State, db: &mut impl Database) { let results = match app.input.as_str() { "" => db.list(), @@ -48,7 +122,11 @@ fn key_handler(input: Key, db: &mut impl Database, app: &mut State) -> Option<St Key::Esc | Key::Char('\n') => { let i = app.results_state.selected().unwrap_or(0); - return Some(app.results.get(i).unwrap().command.clone()); + return Some( + app.results + .get(i) + .map_or("".to_string(), |h| h.command.clone()), + ); } Key::Char(c) => { app.input.push(c); @@ -163,32 +241,8 @@ fn select_history(query: &[String], db: &mut impl Database) -> Result<String> { let help = Text::from(Spans::from(help)); let help = Paragraph::new(help); - let input = Paragraph::new(app.input.as_ref()) - .block(Block::default().borders(Borders::ALL).title("Search")); - - let results: Vec<ListItem> = app - .results - .iter() - .enumerate() - .map(|(i, m)| { - let mut content = - Span::raw(m.command.to_string().replace("\n", " ").replace("\t", " ")); - - if let Some(selected) = app.results_state.selected() { - if selected == i { - content.style = - Style::default().fg(Color::Red).add_modifier(Modifier::BOLD); - } - } - - ListItem::new(content) - }) - .collect(); - - let results = List::new(results) - .block(Block::default().borders(Borders::ALL).title("History")) - .start_corner(Corner::BottomLeft) - .highlight_symbol(">> "); + let input = Paragraph::new(app.input.clone()) + .block(Block::default().borders(Borders::ALL).title("Query")); let stats = Paragraph::new(Text::from(Span::raw(format!( "history count: {}", @@ -199,8 +253,8 @@ fn select_history(query: &[String], db: &mut impl Database) -> Result<String> { f.render_widget(title, top_left_chunks[0]); f.render_widget(help, top_left_chunks[1]); + app.render_results(f, chunks[1]); f.render_widget(stats, top_right_chunks[0]); - f.render_stateful_widget(results, chunks[1], &mut app.results_state); f.render_widget(input, chunks[2]); f.set_cursor( diff --git a/src/command/server.rs b/src/command/server.rs index bf757948..a7835092 100644 --- a/src/command/server.rs +++ b/src/command/server.rs @@ -1,7 +1,7 @@ use eyre::Result; use structopt::StructOpt; -use crate::remote::server; +use crate::server; use crate::settings::Settings; #[derive(StructOpt)] @@ -20,7 +20,7 @@ pub enum Cmd { } impl Cmd { - pub fn run(&self, settings: &Settings) -> Result<()> { + pub async fn run(&self, settings: &Settings) -> Result<()> { match self { Self::Start { host, port } => { let host = host.as_ref().map_or( @@ -29,7 +29,7 @@ impl Cmd { ); let port = port.map_or(settings.server.port, |p| p); - server::launch(settings, host, port) + server::launch(settings, host, port).await } } } diff --git a/src/command/sync.rs b/src/command/sync.rs index facbe578..88217b3c 100644 --- a/src/command/sync.rs +++ b/src/command/sync.rs @@ -4,8 +4,8 @@ use crate::local::database::Database; use crate::local::sync; use crate::settings::Settings; -pub fn run(settings: &Settings, force: bool, db: &mut impl Database) -> Result<()> { - sync::sync(settings, force, db)?; +pub async fn run(settings: &Settings, force: bool, db: &mut (impl Database + Send)) -> Result<()> { + sync::sync(settings, force, db).await?; println!( "Sync complete! {} items in database, force: {}", db.history_count()?, diff --git a/src/local/api_client.rs b/src/local/api_client.rs index 434c07ba..1b64a295 100644 --- a/src/local/api_client.rs +++ b/src/local/api_client.rs @@ -1,93 +1,94 @@ use chrono::Utc; use eyre::Result; -use reqwest::header::AUTHORIZATION; +use reqwest::header::{HeaderMap, AUTHORIZATION}; +use reqwest::Url; +use sodiumoxide::crypto::secretbox; -use crate::api::{AddHistoryRequest, CountResponse, ListHistoryResponse}; -use crate::local::encryption::{decrypt, load_key}; +use crate::api::{AddHistoryRequest, CountResponse, SyncHistoryResponse}; +use crate::local::encryption::decrypt; use crate::local::history::History; -use crate::settings::Settings; use crate::utils::hash_str; pub struct Client<'a> { - settings: &'a Settings, + sync_addr: &'a str, + token: &'a str, + key: secretbox::Key, + client: reqwest::Client, } impl<'a> Client<'a> { - pub const fn new(settings: &'a Settings) -> Self { - Client { settings } + pub fn new(sync_addr: &'a str, token: &'a str, key: secretbox::Key) -> Self { + Client { + sync_addr, + token, + key, + client: reqwest::Client::new(), + } } - pub fn count(&self) -> Result<i64> { - let url = format!("{}/sync/count", self.settings.local.sync_address); - let client = reqwest::blocking::Client::new(); + pub async fn count(&self) -> Result<i64> { + let url = format!("{}/sync/count", self.sync_addr); + let url = Url::parse(url.as_str())?; + let token = format!("Token {}", self.token); + let token = token.parse()?; - let resp = client - .get(url) - .header( - AUTHORIZATION, - format!("Token {}", self.settings.local.session_token), - ) - .send()?; + let mut headers = HeaderMap::new(); + headers.insert(AUTHORIZATION, token); + + let resp = self.client.get(url).headers(headers).send().await?; - let count = resp.json::<CountResponse>()?; + let count = resp.json::<CountResponse>().await?; Ok(count.count) } - pub fn get_history( + pub async fn get_history( &self, sync_ts: chrono::DateTime<Utc>, history_ts: chrono::DateTime<Utc>, host: Option<String>, ) -> Result<Vec<History>> { - let key = load_key(self.settings)?; - let host = match host { None => hash_str(&format!("{}:{}", whoami::hostname(), whoami::username())), Some(h) => h, }; - // this allows for syncing between users on the same machine let url = format!( "{}/sync/history?sync_ts={}&history_ts={}&host={}", - self.settings.local.sync_address, - sync_ts.to_rfc3339(), - history_ts.to_rfc3339(), + self.sync_addr, + urlencoding::encode(sync_ts.to_rfc3339().as_str()), + urlencoding::encode(history_ts.to_rfc3339().as_str()), host, ); - let client = reqwest::blocking::Client::new(); - let resp = client + let resp = self + .client .get(url) - .header( - AUTHORIZATION, - format!("Token {}", self.settings.local.session_token), - ) - .send()?; + .header(AUTHORIZATION, format!("Token {}", self.token)) + .send() + .await?; - let history = resp.json::<ListHistoryResponse>()?; + let history = resp.json::<SyncHistoryResponse>().await?; let history = history .history .iter() .map(|h| serde_json::from_str(h).expect("invalid base64")) - .map(|h| decrypt(&h, &key).expect("failed to decrypt history! check your key")) + .map(|h| decrypt(&h, &self.key).expect("failed to decrypt history! check your key")) .collect(); Ok(history) } - pub fn post_history(&self, history: &[AddHistoryRequest]) -> Result<()> { - let client = reqwest::blocking::Client::new(); + pub async fn post_history(&self, history: &[AddHistoryRequest]) -> Result<()> { + let url = format!("{}/history", self.sync_addr); + let url = Url::parse(url.as_str())?; - let url = format!("{}/history", self.settings.local.sync_address); - client + self.client .post(url) .json(history) - .header( - AUTHORIZATION, - format!("Token {}", self.settings.local.session_token), - ) - .send()?; + .header(AUTHORIZATION, format!("Token {}", self.token)) + .send() + .await?; Ok(()) } diff --git a/src/local/database.rs b/src/local/database.rs index 977f11cc..abc22bb8 100644 --- a/src/local/database.rs +++ b/src/local/database.rs @@ -215,9 +215,9 @@ impl Database for Sqlite { } fn before(&self, timestamp: chrono::DateTime<Utc>, count: i64) -> Result<Vec<History>> { - let mut stmt = self.conn.prepare( - "SELECT * FROM history where timestamp <= ? order by timestamp desc limit ?", - )?; + let mut stmt = self + .conn + .prepare("SELECT * FROM history where timestamp < ? order by timestamp desc limit ?")?; let history_iter = stmt.query_map(params![timestamp.timestamp_nanos(), count], |row| { history_from_sqlite_row(None, row) @@ -236,7 +236,7 @@ impl Database for Sqlite { fn prefix_search(&self, query: &str) -> Result<Vec<History>> { self.query( - "select * from history where command like ?1 || '%' order by timestamp asc", + "select * from history where command like ?1 || '%' order by timestamp asc limit 1000", &[query], ) } diff --git a/src/local/import.rs b/src/local/import.rs index d0f679c9..3b0b2a69 100644 --- a/src/local/import.rs +++ b/src/local/import.rs @@ -7,6 +7,7 @@ use std::{fs::File, path::Path}; use chrono::prelude::*; use chrono::Utc; use eyre::{eyre, Result}; +use itertools::Itertools; use super::history::History; @@ -42,8 +43,8 @@ impl Zsh { fn parse_extended(line: &str, counter: i64) -> History { let line = line.replacen(": ", "", 2); - let (time, duration) = line.split_once(':').unwrap(); - let (duration, command) = duration.split_once(';').unwrap(); + let (time, duration) = line.splitn(2, ':').collect_tuple().unwrap(); + let (duration, command) = duration.splitn(2, ';').collect_tuple().unwrap(); let time = time .parse::<i64>() @@ -60,7 +61,7 @@ fn parse_extended(line: &str, counter: i64) -> History { time, command.trim_end().to_string(), String::from("unknown"), - -1, + 0, // assume 0, we have no way of knowing :( duration, None, None, diff --git a/src/local/sync.rs b/src/local/sync.rs index c22d2f27..e0feb759 100644 --- a/src/local/sync.rs +++ b/src/local/sync.rs @@ -20,12 +20,12 @@ use crate::{api::AddHistoryRequest, utils::hash_str}; // Check if remote has things we don't, and if so, download them. // Returns (num downloaded, total local) -fn sync_download( +async fn sync_download( force: bool, - client: &api_client::Client, - db: &mut impl Database, + client: &api_client::Client<'_>, + db: &mut (impl Database + Send), ) -> Result<(i64, i64)> { - let remote_count = client.count()?; + let remote_count = client.count().await?; let initial_local = db.history_count()?; let mut local_count = initial_local; @@ -41,7 +41,9 @@ fn sync_download( let host = if force { Some(String::from("")) } else { None }; while remote_count > local_count { - let page = client.get_history(last_sync, last_timestamp, host.clone())?; + let page = client + .get_history(last_sync, last_timestamp, host.clone()) + .await?; if page.len() < HISTORY_PAGE_SIZE.try_into().unwrap() { break; @@ -71,13 +73,13 @@ fn sync_download( } // Check if we have things remote doesn't, and if so, upload them -fn sync_upload( +async fn sync_upload( settings: &Settings, _force: bool, - client: &api_client::Client, - db: &mut impl Database, + client: &api_client::Client<'_>, + db: &mut (impl Database + Send), ) -> Result<()> { - let initial_remote_count = client.count()?; + let initial_remote_count = client.count().await?; let mut remote_count = initial_remote_count; let local_count = db.history_count()?; @@ -111,21 +113,25 @@ fn sync_upload( } // anything left over outside of the 100 block size - client.post_history(&buffer)?; + client.post_history(&buffer).await?; cursor = buffer.last().unwrap().timestamp; - remote_count = client.count()?; + remote_count = client.count().await?; } Ok(()) } -pub fn sync(settings: &Settings, force: bool, db: &mut impl Database) -> Result<()> { - let client = api_client::Client::new(settings); +pub async fn sync(settings: &Settings, force: bool, db: &mut (impl Database + Send)) -> Result<()> { + let client = api_client::Client::new( + settings.local.sync_address.as_str(), + settings.local.session_token.as_str(), + load_key(settings)?, + ); - sync_upload(settings, force, &client, db)?; + sync_upload(settings, force, &client, db).await?; - let download = sync_download(force, &client, db)?; + let download = sync_download(force, &client, db).await?; debug!("sync downloaded {}", download.0); diff --git a/src/main.rs b/src/main.rs index 94c7366d..0045a943 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,10 @@ -#![feature(proc_macro_hygiene)] -#![feature(decl_macro)] #![warn(clippy::pedantic, clippy::nursery)] #![allow(clippy::use_self)] // not 100% reliable use std::path::PathBuf; use eyre::{eyre, Result}; +use fern::colors::{Color, ColoredLevelConfig}; use human_panic::setup_panic; use structopt::{clap::AppSettings, StructOpt}; @@ -13,20 +12,8 @@ use structopt::{clap::AppSettings, StructOpt}; extern crate log; #[macro_use] -extern crate rocket; - -#[macro_use] extern crate serde_derive; -#[macro_use] -extern crate diesel; - -#[macro_use] -extern crate diesel_migrations; - -#[macro_use] -extern crate rocket_contrib; - use command::AtuinCmd; use local::database::Sqlite; use settings::Settings; @@ -34,12 +21,10 @@ use settings::Settings; mod api; mod command; mod local; -mod remote; +mod server; mod settings; mod utils; -pub mod schema; - #[derive(StructOpt)] #[structopt( author = "Ellie Huxtable <e@elm.sh>", @@ -56,7 +41,7 @@ struct Atuin { } impl Atuin { - fn run(self, settings: &Settings) -> Result<()> { + async fn run(self, settings: &Settings) -> Result<()> { let db_path = if let Some(db_path) = self.db { let path = db_path .to_str() @@ -69,26 +54,32 @@ impl Atuin { let mut db = Sqlite::new(db_path)?; - self.atuin.run(&mut db, settings) + self.atuin.run(&mut db, settings).await } } -fn main() -> Result<()> { - setup_panic!(); - let settings = Settings::new()?; +#[tokio::main] +async fn main() -> Result<()> { + let colors = ColoredLevelConfig::new() + .warn(Color::Yellow) + .error(Color::Red); fern::Dispatch::new() - .format(|out, message, record| { + .format(move |out, message, record| { out.finish(format_args!( "{} [{}] {}", - chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), - record.level(), + chrono::Local::now().to_rfc3339(), + colors.color(record.level()), message )) }) .level(log::LevelFilter::Info) + .level_for("sqlx", log::LevelFilter::Warn) .chain(std::io::stdout()) .apply()?; - Atuin::from_args().run(&settings) + let settings = Settings::new()?; + setup_panic!(); + + Atuin::from_args().run(&settings).await } diff --git a/src/remote/database.rs b/src/remote/database.rs deleted file mode 100644 index 03973ca1..00000000 --- a/src/remote/database.rs +++ /dev/null @@ -1,22 +0,0 @@ -use diesel::pg::PgConnection; -use diesel::prelude::*; -use eyre::{eyre, Result}; - -use crate::settings::Settings; - -#[database("atuin")] -pub struct AtuinDbConn(diesel::PgConnection); - -// TODO: connection pooling -pub fn establish_connection(settings: &Settings) -> Result<PgConnection> { - if settings.server.db_uri == "default_uri" { - Err(eyre!( - "Please configure your database! Set db_uri in config.toml" - )) - } else { - let database_url = &settings.server.db_uri; - let conn = PgConnection::establish(database_url)?; - - Ok(conn) - } -} diff --git a/src/remote/mod.rs b/src/remote/mod.rs deleted file mode 100644 index 7147b88e..00000000 --- a/src/remote/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mo |