Test push progress (#272)

This commit is contained in:
Stephan Dilly 2020-09-04 09:08:29 +02:00 committed by GitHub
parent 56959e6596
commit 17de5a900c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 392 additions and 76 deletions

6
Cargo.lock generated
View File

@ -407,8 +407,7 @@ checksum = "aaf91faf136cb47367fa430cd46e37a788775e7fa104f8b4bcb3861dc389b724"
[[package]] [[package]]
name = "git2" name = "git2"
version = "0.13.10" version = "0.13.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/rust-lang/git2-rs.git?rev=5fddf7e04dc76e70873569ca9f1de3287ec3edda#5fddf7e04dc76e70873569ca9f1de3287ec3edda"
checksum = "86d97249f21e9542caeee9f8e1d150905cd875bf723f5ff771bdb4852eb83a24"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"libc", "libc",
@ -549,8 +548,7 @@ checksum = "a9f8082297d534141b30c8d39e9b1773713ab50fdbe4ff30f750d063b3bfd701"
[[package]] [[package]]
name = "libgit2-sys" name = "libgit2-sys"
version = "0.12.12+1.0.1" version = "0.12.12+1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/rust-lang/git2-rs.git?rev=5fddf7e04dc76e70873569ca9f1de3287ec3edda#5fddf7e04dc76e70873569ca9f1de3287ec3edda"
checksum = "0100ae90655025134424939f1f60e27e879460d451dff6afedde4f8226cbebfc"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",

View File

@ -13,7 +13,8 @@ keywords = ["git"]
[dependencies] [dependencies]
scopetime = { path = "../scopetime", version = "0.1" } scopetime = { path = "../scopetime", version = "0.1" }
git2 = { version = "0.13", features = ["vendored-openssl"] } # git2 = { version = "0.13", features = ["vendored-openssl"] }
git2 = { git="https://github.com/rust-lang/git2-rs.git", rev="5fddf7e04dc76e70873569ca9f1de3287ec3edda", features = ["vendored-openssl"] }
rayon-core = "1.8" rayon-core = "1.8"
crossbeam-channel = "0.4" crossbeam-channel = "0.4"
log = "0.4" log = "0.4"

View File

@ -52,11 +52,11 @@ pub struct AsyncDiff {
impl AsyncDiff { impl AsyncDiff {
/// ///
pub fn new(sender: Sender<AsyncNotification>) -> Self { pub fn new(sender: &Sender<AsyncNotification>) -> Self {
Self { Self {
current: Arc::new(Mutex::new(Request(0, None))), current: Arc::new(Mutex::new(Request(0, None))),
last: Arc::new(Mutex::new(None)), last: Arc::new(Mutex::new(None)),
sender, sender: sender.clone(),
pending: Arc::new(AtomicUsize::new(0)), pending: Arc::new(AtomicUsize::new(0)),
} }
} }

View File

@ -11,6 +11,7 @@ pub mod cached;
mod commit_files; mod commit_files;
mod diff; mod diff;
mod error; mod error;
mod push;
mod revlog; mod revlog;
mod status; mod status;
pub mod sync; pub mod sync;
@ -19,6 +20,7 @@ mod tags;
pub use crate::{ pub use crate::{
commit_files::AsyncCommitFiles, commit_files::AsyncCommitFiles,
diff::{AsyncDiff, DiffParams, DiffType}, diff::{AsyncDiff, DiffParams, DiffType},
push::{AsyncPush, PushRequest},
revlog::{AsyncLog, FetchStatus}, revlog::{AsyncLog, FetchStatus},
status::{AsyncStatus, StatusParams}, status::{AsyncStatus, StatusParams},
sync::{ sync::{
@ -47,6 +49,8 @@ pub enum AsyncNotification {
CommitFiles, CommitFiles,
/// ///
Tags, Tags,
///
Push,
} }
/// current working director `./` /// current working director `./`

140
asyncgit/src/push.rs Normal file
View File

@ -0,0 +1,140 @@
use crate::{
error::{Error, Result},
sync, AsyncNotification, CWD,
};
use crossbeam_channel::Sender;
use std::sync::{Arc, Mutex};
#[derive(Clone, Debug)]
enum PushStates {
None,
// Packing,
// Pushing(usize, usize),
}
impl Default for PushStates {
fn default() -> Self {
PushStates::None
}
}
///
#[derive(Default, Clone, Debug)]
pub struct PushRequest {
///
pub remote: String,
///
pub branch: String,
}
#[derive(Default, Clone, Debug)]
struct PushState {
request: PushRequest,
state: PushStates,
}
///
pub struct AsyncPush {
state: Arc<Mutex<Option<PushState>>>,
last_result: Arc<Mutex<Option<String>>>,
sender: Sender<AsyncNotification>,
}
impl AsyncPush {
///
pub fn new(sender: &Sender<AsyncNotification>) -> Self {
Self {
state: Arc::new(Mutex::new(None)),
last_result: Arc::new(Mutex::new(None)),
sender: sender.clone(),
}
}
///
pub fn is_pending(&self) -> Result<bool> {
let state = self.state.lock()?;
Ok(state.is_some())
}
///
pub fn last_result(&self) -> Result<Option<String>> {
let res = self.last_result.lock()?;
Ok(res.clone())
}
///
pub fn request(&mut self, params: PushRequest) -> Result<()> {
log::trace!("request");
if self.is_pending()? {
return Ok(());
}
self.set_request(&params)?;
let arc_state = Arc::clone(&self.state);
let arc_res = Arc::clone(&self.last_result);
let sender = self.sender.clone();
rayon_core::spawn(move || {
//TODO: use channels to communicate progress
let res = sync::push_origin(
CWD,
params.remote.as_str(),
params.branch.as_str(),
);
Self::set_result(arc_res, res).expect("result error");
Self::clear_request(arc_state).expect("clear error");
sender
.send(AsyncNotification::Push)
.expect("error sending push");
});
Ok(())
}
fn set_request(&self, params: &PushRequest) -> Result<()> {
let mut state = self.state.lock()?;
if state.is_some() {
return Err(Error::Generic("pending request".into()));
}
*state = Some(PushState {
request: params.clone(),
..PushState::default()
});
Ok(())
}
fn clear_request(
state: Arc<Mutex<Option<PushState>>>,
) -> Result<()> {
let mut state = state.lock()?;
*state = None;
Ok(())
}
fn set_result(
arc_result: Arc<Mutex<Option<String>>>,
res: Result<()>,
) -> Result<()> {
let mut last_res = arc_result.lock()?;
*last_res = match res {
Ok(_) => None,
Err(e) => {
log::error!("push error: {}", e);
Some(e.to_string())
}
};
Ok(())
}
}

View File

@ -30,9 +30,7 @@ pub use hooks::{hooks_commit_msg, hooks_post_commit, HookResult};
pub use hunks::{reset_hunk, stage_hunk, unstage_hunk}; pub use hunks::{reset_hunk, stage_hunk, unstage_hunk};
pub use ignore::add_to_ignore; pub use ignore::add_to_ignore;
pub use logwalker::LogWalker; pub use logwalker::LogWalker;
pub use remotes::{ pub use remotes::{fetch_origin, get_remotes, push_origin};
fetch_origin, get_remotes, push_origin, remote_push_master,
};
pub use reset::{reset_stage, reset_workdir}; pub use reset::{reset_stage, reset_workdir};
pub use stash::{get_stashes, stash_apply, stash_drop, stash_save}; pub use stash::{get_stashes, stash_apply, stash_drop, stash_save};
pub use tags::{get_tags, CommitTags, Tags}; pub use tags::{get_tags, CommitTags, Tags};

View File

@ -16,18 +16,6 @@ pub fn get_remotes(repo_path: &str) -> Result<Vec<String>> {
Ok(remotes) Ok(remotes)
} }
///
pub fn remote_push_master(repo_path: &str) -> Result<()> {
scope_time!("remote_push_master");
let repo = utils::repo(repo_path)?;
let mut remote = repo.find_remote("origin")?;
remote.push(&["refs/heads/master"], None)?;
Ok(())
}
/// ///
pub fn fetch_origin(repo_path: &str, branch: &str) -> Result<usize> { pub fn fetch_origin(repo_path: &str, branch: &str) -> Result<usize> {
scope_time!("remote_fetch_master"); scope_time!("remote_fetch_master");
@ -44,14 +32,19 @@ pub fn fetch_origin(repo_path: &str, branch: &str) -> Result<usize> {
} }
/// ///
pub fn push_origin(repo_path: &str, branch: &str) -> Result<()> { pub fn push_origin(
repo_path: &str,
remote: &str,
branch: &str,
) -> Result<()> {
scope_time!("push_origin"); scope_time!("push_origin");
let repo = utils::repo(repo_path)?; let repo = utils::repo(repo_path)?;
let mut remote = repo.find_remote("origin")?; let mut remote = repo.find_remote(remote)?;
let mut options = PushOptions::new(); let mut options = PushOptions::new();
options.remote_callbacks(remote_callbacks()); options.remote_callbacks(remote_callbacks());
options.packbuilder_parallelism(0);
remote.push(&[branch], Some(&mut options))?; remote.push(&[branch], Some(&mut options))?;
@ -60,6 +53,17 @@ pub fn push_origin(repo_path: &str, branch: &str) -> Result<()> {
fn remote_callbacks<'a>() -> RemoteCallbacks<'a> { fn remote_callbacks<'a>() -> RemoteCallbacks<'a> {
let mut callbacks = RemoteCallbacks::new(); let mut callbacks = RemoteCallbacks::new();
callbacks.push_transfer_progress(|progress, total, bytes| {
log::debug!(
"progress: {}/{} ({} B)",
progress,
total,
bytes,
);
});
callbacks.pack_progress(|stage, current, total| {
log::debug!("packing: {:?} - {}/{}", stage, current, total);
});
callbacks.credentials(|url, username_from_url, allowed_types| { callbacks.credentials(|url, username_from_url, allowed_types| {
log::debug!( log::debug!(
"creds: '{}' {:?} ({:?})", "creds: '{}' {:?} ({:?})",

View File

@ -5,8 +5,8 @@ use crate::{
event_pump, CommandBlocking, CommandInfo, CommitComponent, event_pump, CommandBlocking, CommandInfo, CommitComponent,
Component, CreateBranchComponent, DrawableComponent, Component, CreateBranchComponent, DrawableComponent,
ExternalEditorComponent, HelpComponent, ExternalEditorComponent, HelpComponent,
InspectCommitComponent, MsgComponent, ResetComponent, InspectCommitComponent, MsgComponent, PushComponent,
StashMsgComponent, TagCommitComponent, ResetComponent, StashMsgComponent, TagCommitComponent,
}, },
input::{Input, InputEvent, InputState}, input::{Input, InputEvent, InputState},
keys::{KeyConfig, SharedKeyConfig}, keys::{KeyConfig, SharedKeyConfig},
@ -41,6 +41,7 @@ pub struct App {
stashmsg_popup: StashMsgComponent, stashmsg_popup: StashMsgComponent,
inspect_commit_popup: InspectCommitComponent, inspect_commit_popup: InspectCommitComponent,
external_editor_popup: ExternalEditorComponent, external_editor_popup: ExternalEditorComponent,
push_popup: PushComponent,
tag_commit_popup: TagCommitComponent, tag_commit_popup: TagCommitComponent,
create_branch_popup: CreateBranchComponent, create_branch_popup: CreateBranchComponent,
cmdbar: RefCell<CommandBar>, cmdbar: RefCell<CommandBar>,
@ -98,6 +99,12 @@ impl App {
theme.clone(), theme.clone(),
key_config.clone(), key_config.clone(),
), ),
push_popup: PushComponent::new(
&queue,
sender,
theme.clone(),
key_config.clone(),
),
tag_commit_popup: TagCommitComponent::new( tag_commit_popup: TagCommitComponent::new(
queue.clone(), queue.clone(),
theme.clone(), theme.clone(),
@ -225,21 +232,7 @@ impl App {
flags.insert(new_flags); flags.insert(new_flags);
} }
let new_flags = self.process_queue()?; self.process_queue(flags)?;
flags.insert(new_flags);
if flags.contains(NeedsUpdate::ALL) {
self.update()?;
}
//TODO: make this a queue event?
//NOTE: set when any tree component changed selection
if flags.contains(NeedsUpdate::DIFF) {
self.status_tab.update_diff()?;
self.inspect_commit_popup.update_diff()?;
}
if flags.contains(NeedsUpdate::COMMANDS) {
self.update_commands();
}
} else if let InputEvent::State(polling_state) = ev { } else if let InputEvent::State(polling_state) = ev {
self.external_editor_popup.hide(); self.external_editor_popup.hide();
if let InputState::Paused = polling_state { if let InputState::Paused = polling_state {
@ -293,10 +286,11 @@ impl App {
self.stashing_tab.update_git(ev)?; self.stashing_tab.update_git(ev)?;
self.revlog.update_git(ev)?; self.revlog.update_git(ev)?;
self.inspect_commit_popup.update_git(ev)?; self.inspect_commit_popup.update_git(ev)?;
self.push_popup.update_git(ev)?;
//TODO: better system for this //TODO: better system for this
// can we simply process the queue here and everyone just uses the queue to schedule a cmd update? // can we simply process the queue here and everyone just uses the queue to schedule a cmd update?
self.update_commands(); self.process_queue(NeedsUpdate::COMMANDS)?;
Ok(()) Ok(())
} }
@ -337,6 +331,7 @@ impl App {
stashmsg_popup, stashmsg_popup,
inspect_commit_popup, inspect_commit_popup,
external_editor_popup, external_editor_popup,
push_popup,
tag_commit_popup, tag_commit_popup,
create_branch_popup, create_branch_popup,
help, help,
@ -411,7 +406,28 @@ impl App {
self.cmdbar.borrow_mut().set_cmds(self.commands(false)); self.cmdbar.borrow_mut().set_cmds(self.commands(false));
} }
fn process_queue(&mut self) -> Result<NeedsUpdate> { fn process_queue(&mut self, flags: NeedsUpdate) -> Result<()> {
let mut flags = flags;
let new_flags = self.process_internal_events()?;
flags.insert(new_flags);
if flags.contains(NeedsUpdate::ALL) {
self.update()?;
}
//TODO: make this a queue event?
//NOTE: set when any tree component changed selection
if flags.contains(NeedsUpdate::DIFF) {
self.status_tab.update_diff()?;
self.inspect_commit_popup.update_diff()?;
}
if flags.contains(NeedsUpdate::COMMANDS) {
self.update_commands();
}
Ok(())
}
fn process_internal_events(&mut self) -> Result<NeedsUpdate> {
let mut flags = NeedsUpdate::empty(); let mut flags = NeedsUpdate::empty();
loop { loop {
@ -458,11 +474,6 @@ impl App {
flags flags
.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS); .insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);
} }
InternalEvent::ShowInfoMsg(msg) => {
self.msg.show_info(msg.as_str())?;
flags
.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);
}
InternalEvent::Update(u) => flags.insert(u), InternalEvent::Update(u) => flags.insert(u),
InternalEvent::OpenCommit => self.commit.show()?, InternalEvent::OpenCommit => self.commit.show()?,
InternalEvent::PopupStashing(opts) => { InternalEvent::PopupStashing(opts) => {
@ -486,6 +497,10 @@ impl App {
self.file_to_open = path; self.file_to_open = path;
flags.insert(NeedsUpdate::COMMANDS) flags.insert(NeedsUpdate::COMMANDS)
} }
InternalEvent::Push(branch) => {
self.push_popup.push(branch)?;
flags.insert(NeedsUpdate::ALL)
}
}; };
Ok(flags) Ok(flags)
@ -534,6 +549,7 @@ impl App {
res res
} }
//TODO: make this automatic, i keep forgetting to add popups here
fn any_popup_visible(&self) -> bool { fn any_popup_visible(&self) -> bool {
self.commit.is_visible() self.commit.is_visible()
|| self.help.is_visible() || self.help.is_visible()
@ -544,6 +560,7 @@ impl App {
|| self.external_editor_popup.is_visible() || self.external_editor_popup.is_visible()
|| self.tag_commit_popup.is_visible() || self.tag_commit_popup.is_visible()
|| self.create_branch_popup.is_visible() || self.create_branch_popup.is_visible()
|| self.push_popup.is_visible()
} }
fn draw_popups<B: Backend>( fn draw_popups<B: Backend>(
@ -570,6 +587,7 @@ impl App {
self.external_editor_popup.draw(f, size)?; self.external_editor_popup.draw(f, size)?;
self.tag_commit_popup.draw(f, size)?; self.tag_commit_popup.draw(f, size)?;
self.create_branch_popup.draw(f, size)?; self.create_branch_popup.draw(f, size)?;
self.push_popup.draw(f, size)?;
Ok(()) Ok(())
} }

View File

@ -173,7 +173,7 @@ impl InspectCommitComponent {
), ),
commit_id: None, commit_id: None,
tags: None, tags: None,
git_diff: AsyncDiff::new(sender.clone()), git_diff: AsyncDiff::new(sender),
visible: false, visible: false,
key_config, key_config,
} }

View File

@ -10,6 +10,7 @@ mod filetree;
mod help; mod help;
mod inspect_commit; mod inspect_commit;
mod msg; mod msg;
mod push;
mod reset; mod reset;
mod stashmsg; mod stashmsg;
mod tag_commit; mod tag_commit;
@ -31,6 +32,7 @@ pub use filetree::FileTreeComponent;
pub use help::HelpComponent; pub use help::HelpComponent;
pub use inspect_commit::InspectCommitComponent; pub use inspect_commit::InspectCommitComponent;
pub use msg::MsgComponent; pub use msg::MsgComponent;
pub use push::PushComponent;
pub use reset::ResetComponent; pub use reset::ResetComponent;
pub use stashmsg::StashMsgComponent; pub use stashmsg::StashMsgComponent;
pub use tag_commit::TagCommitComponent; pub use tag_commit::TagCommitComponent;

View File

@ -119,13 +119,4 @@ impl MsgComponent {
Ok(()) Ok(())
} }
///
pub fn show_info(&mut self, msg: &str) -> Result<()> {
self.title = strings::msg_title_info(&self.key_config);
self.msg = msg.to_string();
self.show()?;
Ok(())
}
} }

169
src/components/push.rs Normal file
View File

@ -0,0 +1,169 @@
use crate::{
components::{
visibility_blocking, CommandBlocking, CommandInfo, Component,
DrawableComponent,
},
keys::SharedKeyConfig,
queue::{InternalEvent, Queue},
strings,
ui::{self, style::SharedTheme},
};
use anyhow::Result;
use asyncgit::{AsyncNotification, AsyncPush, PushRequest};
use crossbeam_channel::Sender;
use crossterm::event::Event;
use tui::{
backend::Backend,
layout::Rect,
widgets::{Block, BorderType, Borders, Clear, Paragraph, Text},
Frame,
};
///
pub struct PushComponent {
visible: bool,
git_push: AsyncPush,
pending: bool,
queue: Queue,
theme: SharedTheme,
key_config: SharedKeyConfig,
}
impl PushComponent {
///
pub fn new(
queue: &Queue,
sender: &Sender<AsyncNotification>,
theme: SharedTheme,
key_config: SharedKeyConfig,
) -> Self {
Self {
queue: queue.clone(),
pending: false,
visible: false,
git_push: AsyncPush::new(sender),
theme,
key_config,
}
}
///
pub fn push(&mut self, branch: String) -> Result<()> {
self.pending = true;
self.git_push.request(PushRequest {
remote: String::from("origin"),
branch,
})?;
self.show()?;
Ok(())
}
///
pub fn update_git(
&mut self,
ev: AsyncNotification,
) -> Result<()> {
if self.is_visible() {
if let AsyncNotification::Push = ev {
self.update()?;
}
}
Ok(())
}
///
fn update(&mut self) -> Result<()> {
self.pending = self.git_push.is_pending()?;
if !self.pending {
if let Some(err) = self.git_push.last_result()? {
self.queue.borrow_mut().push_back(
InternalEvent::ShowErrorMsg(format!(
"push failed:\n{}",
err
)),
);
}
self.hide();
}
Ok(())
}
}
impl DrawableComponent for PushComponent {
fn draw<B: Backend>(
&self,
f: &mut Frame<B>,
_rect: Rect,
) -> Result<()> {
if self.visible {
let txt = vec![Text::Raw(strings::PUSH_POPUP_MSG.into())];
let area = ui::centered_rect_absolute(25, 3, f.size());
f.render_widget(Clear, area);
f.render_widget(
Paragraph::new(txt.iter())
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Thick)
.title_style(self.theme.title(true))
.border_style(self.theme.block(true)),
)
.style(self.theme.text_danger()),
area,
);
}
Ok(())
}
}
impl Component for PushComponent {
fn commands(
&self,
out: &mut Vec<CommandInfo>,
_force_all: bool,
) -> CommandBlocking {
if self.is_visible() {
out.clear();
}
out.push(CommandInfo::new(
strings::commands::close_msg(&self.key_config),
!self.pending,
self.visible,
));
visibility_blocking(self)
}
fn event(&mut self, ev: Event) -> Result<bool> {
if self.visible {
if let Event::Key(e) = ev {
if e == self.key_config.enter {
self.hide();
}
}
return Ok(true);
}
Ok(false)
}
fn is_visible(&self) -> bool {
self.visible
}
fn hide(&mut self) {
self.visible = false
}
fn show(&mut self) -> Result<()> {
self.visible = true;
Ok(())
}
}

View File

@ -39,8 +39,6 @@ pub enum InternalEvent {
/// ///
ShowErrorMsg(String), ShowErrorMsg(String),
/// ///
ShowInfoMsg(String),
///
Update(NeedsUpdate), Update(NeedsUpdate),
/// open commit msg input /// open commit msg input
OpenCommit, OpenCommit,
@ -56,6 +54,8 @@ pub enum InternalEvent {
CreateBranch, CreateBranch,
/// ///
OpenExternalEditor(Option<String>), OpenExternalEditor(Option<String>),
///
Push(String),
} }
/// ///

View File

@ -4,6 +4,8 @@ pub mod order {
pub static NAV: i8 = 1; pub static NAV: i8 = 1;
} }
pub static PUSH_POPUP_MSG: &str = "pushing...";
pub fn title_status(key_config: &SharedKeyConfig) -> String { pub fn title_status(key_config: &SharedKeyConfig) -> String {
format!( format!(
"Unstaged Changes [{}]", "Unstaged Changes [{}]",
@ -40,9 +42,6 @@ pub fn msg_opening_editor(_key_config: &SharedKeyConfig) -> String {
pub fn msg_title_error(_key_config: &SharedKeyConfig) -> String { pub fn msg_title_error(_key_config: &SharedKeyConfig) -> String {
"Error".to_string() "Error".to_string()
} }
pub fn msg_title_info(_key_config: &SharedKeyConfig) -> String {
"Info".to_string()
}
pub fn commit_title(_key_config: &SharedKeyConfig) -> String { pub fn commit_title(_key_config: &SharedKeyConfig) -> String {
"Commit".to_string() "Commit".to_string()
} }

View File

@ -137,7 +137,7 @@ impl Status {
key_config.clone(), key_config.clone(),
false, false,
), ),
git_diff: AsyncDiff::new(sender.clone()), git_diff: AsyncDiff::new(sender),
git_status_workdir: AsyncStatus::new(sender.clone()), git_status_workdir: AsyncStatus::new(sender.clone()),
git_status_stage: AsyncStatus::new(sender.clone()), git_status_stage: AsyncStatus::new(sender.clone()),
git_action_executed: false, git_action_executed: false,
@ -327,18 +327,10 @@ impl Status {
fn push(&self) { fn push(&self) {
if let Some(branch) = self.index_wd.branch_name() { if let Some(branch) = self.index_wd.branch_name() {
let branch = format!("refs/heads/{}", branch); let branch = format!("refs/heads/{}", branch);
if let Err(e) = sync::push_origin(CWD, branch.as_str()) {
self.queue.borrow_mut().push_back( self.queue
InternalEvent::ShowErrorMsg(format!( .borrow_mut()
"push failed:\n{}", .push_back(InternalEvent::Push(branch));
e
)),
);
} else {
self.queue.borrow_mut().push_back(
InternalEvent::ShowInfoMsg("pushed".to_string()),
);
}
} }
} }
} }