diff --git a/asyncgit/src/fetch_job.rs b/asyncgit/src/fetch_job.rs new file mode 100644 index 0000000000..0daf667e44 --- /dev/null +++ b/asyncgit/src/fetch_job.rs @@ -0,0 +1,77 @@ +//! + +use crate::{ + asyncjob::{AsyncJob, RunParams}, + error::Result, + sync::cred::BasicAuthCredential, + sync::remotes::fetch_all, + AsyncGitNotification, ProgressPercent, CWD, +}; + +use std::sync::{Arc, Mutex}; + +enum JobState { + Request(Option), + Response(Result<()>), +} + +/// +#[derive(Clone, Default)] +pub struct AsyncFetchJob { + state: Arc>>, +} + +/// +impl AsyncFetchJob { + /// + pub fn new( + basic_credential: Option, + ) -> Self { + Self { + state: Arc::new(Mutex::new(Some(JobState::Request( + basic_credential, + )))), + } + } + + /// + pub fn result(&self) -> Option> { + if let Ok(mut state) = self.state.lock() { + if let Some(state) = state.take() { + return match state { + JobState::Request(_) => None, + JobState::Response(result) => Some(result), + }; + } + } + + None + } +} + +impl AsyncJob for AsyncFetchJob { + type Notification = AsyncGitNotification; + type Progress = ProgressPercent; + + fn run( + &mut self, + _params: RunParams, + ) -> Result { + if let Ok(mut state) = self.state.lock() { + *state = state.take().map(|state| match state { + JobState::Request(basic_credentials) => { + //TODO: support progress + let result = + fetch_all(CWD, &basic_credentials, &None); + + JobState::Response(result) + } + JobState::Response(result) => { + JobState::Response(result) + } + }); + } + + Ok(AsyncGitNotification::Fetch) + } +} diff --git a/asyncgit/src/lib.rs b/asyncgit/src/lib.rs index 2f3c674ff6..2df5bdc9a5 100644 --- a/asyncgit/src/lib.rs +++ b/asyncgit/src/lib.rs @@ -28,8 +28,9 @@ pub mod cached; mod commit_files; mod diff; mod error; -mod fetch; +mod fetch_job; mod progress; +mod pull; mod push; mod push_tags; pub mod remote_progress; @@ -44,8 +45,9 @@ pub use crate::{ commit_files::{AsyncCommitFiles, CommitFilesParams}, diff::{AsyncDiff, DiffParams, DiffType}, error::{Error, Result}, - fetch::{AsyncFetch, FetchRequest}, + fetch_job::AsyncFetchJob, progress::ProgressPercent, + pull::{AsyncPull, FetchRequest}, push::{AsyncPush, PushRequest}, push_tags::{AsyncPushTags, PushTagsRequest}, remote_progress::{RemoteProgress, RemoteProgressState}, @@ -83,11 +85,13 @@ pub enum AsyncGitNotification { /// PushTags, /// - Fetch, + Pull, /// Blame, /// RemoteTags, + /// + Fetch, } /// current working directory `./` diff --git a/asyncgit/src/fetch.rs b/asyncgit/src/pull.rs similarity index 96% rename from asyncgit/src/fetch.rs rename to asyncgit/src/pull.rs index d7fb4506fc..30562835f5 100644 --- a/asyncgit/src/fetch.rs +++ b/asyncgit/src/pull.rs @@ -28,14 +28,14 @@ pub struct FetchRequest { struct FetchState {} /// -pub struct AsyncFetch { +pub struct AsyncPull { state: Arc>>, last_result: Arc>>, progress: Arc>>, sender: Sender, } -impl AsyncFetch { +impl AsyncPull { /// pub fn new(sender: &Sender) -> Self { Self { @@ -84,7 +84,7 @@ impl AsyncFetch { let (progress_sender, receiver) = unbounded(); let handle = RemoteProgress::spawn_receiver_thread( - AsyncGitNotification::Fetch, + AsyncGitNotification::Pull, sender.clone(), receiver, arc_progress, @@ -108,7 +108,7 @@ impl AsyncFetch { Self::clear_request(&arc_state).expect("clear error"); sender - .send(AsyncGitNotification::Fetch) + .send(AsyncGitNotification::Pull) .expect("AsyncNotification error"); }); diff --git a/asyncgit/src/sync/remotes/mod.rs b/asyncgit/src/sync/remotes/mod.rs index 133c761c3d..f4419f8039 100644 --- a/asyncgit/src/sync/remotes/mod.rs +++ b/asyncgit/src/sync/remotes/mod.rs @@ -10,6 +10,7 @@ use crate::{ cred::BasicAuthCredential, remotes::push::ProgressNotification, utils, }, + ProgressPercent, }; use crossbeam_channel::Sender; use git2::{BranchType, FetchOptions, Repository}; @@ -75,14 +76,68 @@ pub(crate) fn get_default_remote_in_repo( Err(Error::NoDefaultRemoteFound) } -/// fetches from upstream/remote for `branch` +/// +fn fetch_from_remote( + repo_path: &str, + remote: &str, + basic_credential: Option, + progress_sender: Option>, +) -> Result<()> { + let repo = utils::repo(repo_path)?; + + let mut remote = repo.find_remote(remote)?; + + let mut options = FetchOptions::new(); + let callbacks = Callbacks::new(progress_sender, basic_credential); + options.prune(git2::FetchPrune::On); + options.remote_callbacks(callbacks.callbacks()); + remote.fetch(&[] as &[&str], Some(&mut options), None)?; + + Ok(()) +} + +/// updates/prunes all branches from all remotes +pub fn fetch_all( + repo_path: &str, + basic_credential: &Option, + progress_sender: &Option>, +) -> Result<()> { + scope_time!("fetch_all"); + + let repo = utils::repo(repo_path)?; + let remotes = repo + .remotes()? + .iter() + .flatten() + .map(String::from) + .collect::>(); + let remotes_count = remotes.len(); + + for (idx, remote) in remotes.into_iter().enumerate() { + fetch_from_remote( + repo_path, + &remote, + basic_credential.clone(), + None, + )?; + + if let Some(sender) = progress_sender { + let progress = ProgressPercent::new(idx, remotes_count); + sender.send(progress)?; + } + } + + Ok(()) +} + +/// fetches from upstream/remote for local `branch` pub(crate) fn fetch( repo_path: &str, branch: &str, basic_credential: Option, progress_sender: Option>, ) -> Result { - scope_time!("fetch_origin"); + scope_time!("fetch"); let repo = utils::repo(repo_path)?; let branch_ref = repo diff --git a/src/app.rs b/src/app.rs index 91e02e3049..74a5b7d936 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6,11 +6,12 @@ use crate::{ BranchListComponent, CommandBlocking, CommandInfo, CommitComponent, CompareCommitsComponent, Component, ConfirmComponent, CreateBranchComponent, DrawableComponent, - ExternalEditorComponent, FileFindPopup, HelpComponent, - InspectCommitComponent, MsgComponent, OptionsPopupComponent, - PullComponent, PushComponent, PushTagsComponent, - RenameBranchComponent, RevisionFilesPopup, SharedOptions, - StashMsgComponent, TagCommitComponent, TagListComponent, + ExternalEditorComponent, FetchComponent, FileFindPopup, + HelpComponent, InspectCommitComponent, MsgComponent, + OptionsPopupComponent, PullComponent, PushComponent, + PushTagsComponent, RenameBranchComponent, RevisionFilesPopup, + SharedOptions, StashMsgComponent, TagCommitComponent, + TagListComponent, }, input::{Input, InputEvent, InputState}, keys::{KeyConfig, SharedKeyConfig}, @@ -55,6 +56,7 @@ pub struct App { push_popup: PushComponent, push_tags_popup: PushTagsComponent, pull_popup: PullComponent, + fetch_popup: FetchComponent, tag_commit_popup: TagCommitComponent, create_branch_popup: CreateBranchComponent, rename_branch_popup: RenameBranchComponent, @@ -158,6 +160,12 @@ impl App { theme.clone(), key_config.clone(), ), + fetch_popup: FetchComponent::new( + &queue, + sender, + theme.clone(), + key_config.clone(), + ), tag_commit_popup: TagCommitComponent::new( queue.clone(), theme.clone(), @@ -389,6 +397,7 @@ impl App { self.push_popup.update_git(ev)?; self.push_tags_popup.update_git(ev)?; self.pull_popup.update_git(ev); + self.fetch_popup.update_git(ev); self.select_branch_popup.update_git(ev)?; } @@ -421,6 +430,7 @@ impl App { || self.push_popup.any_work_pending() || self.push_tags_popup.any_work_pending() || self.pull_popup.any_work_pending() + || self.fetch_popup.any_work_pending() || self.revision_files_popup.any_work_pending() || self.tags_popup.any_work_pending() } @@ -453,6 +463,7 @@ impl App { push_popup, push_tags_popup, pull_popup, + fetch_popup, tag_commit_popup, create_branch_popup, rename_branch_popup, @@ -489,6 +500,7 @@ impl App { push_popup, push_tags_popup, pull_popup, + fetch_popup, options_popup, reset, msg @@ -696,6 +708,14 @@ impl App { } flags.insert(NeedsUpdate::ALL); } + InternalEvent::FetchRemotes => { + if let Err(error) = self.fetch_popup.fetch() { + self.queue.push(InternalEvent::ShowErrorMsg( + error.to_string(), + )); + } + flags.insert(NeedsUpdate::ALL); + } InternalEvent::PushTags => { self.push_tags_popup.push_tags()?; flags.insert(NeedsUpdate::ALL); diff --git a/src/components/branchlist.rs b/src/components/branchlist.rs index 64bccd1336..548c1f0065 100644 --- a/src/components/branchlist.rs +++ b/src/components/branchlist.rs @@ -196,6 +196,12 @@ impl Component for BranchListComponent { true, self.local, )); + + out.push(CommandInfo::new( + strings::commands::fetch_remotes(&self.key_config), + true, + !self.local, + )); } visibility_blocking(self) } @@ -290,6 +296,8 @@ impl Component for BranchListComponent { self.queue .push(InternalEvent::CompareCommits(b, None)); } + } else if e == self.key_config.keys.pull && !self.local { + self.queue.push(InternalEvent::FetchRemotes); } else if e == self.key_config.keys.cmd_bar_toggle { //do not consume if its the more key return Ok(EventState::NotConsumed); diff --git a/src/components/fetch.rs b/src/components/fetch.rs new file mode 100644 index 0000000000..1d24d898e9 --- /dev/null +++ b/src/components/fetch.rs @@ -0,0 +1,211 @@ +use crate::{ + components::{ + cred::CredComponent, visibility_blocking, CommandBlocking, + CommandInfo, Component, DrawableComponent, EventState, + }, + keys::SharedKeyConfig, + queue::{InternalEvent, NeedsUpdate, Queue}, + strings, + ui::{self, style::SharedTheme}, +}; +use anyhow::Result; +use asyncgit::{ + asyncjob::AsyncSingleJob, + sync::cred::{ + extract_username_password, need_username_password, + BasicAuthCredential, + }, + AsyncFetchJob, AsyncGitNotification, ProgressPercent, +}; +use crossbeam_channel::Sender; +use crossterm::event::Event; +use tui::{ + backend::Backend, + layout::Rect, + text::Span, + widgets::{Block, BorderType, Borders, Clear, Gauge}, + Frame, +}; + +/// +pub struct FetchComponent { + visible: bool, + async_fetch: AsyncSingleJob, + progress: Option, + pending: bool, + queue: Queue, + theme: SharedTheme, + key_config: SharedKeyConfig, + input_cred: CredComponent, +} + +impl FetchComponent { + /// + pub fn new( + queue: &Queue, + sender: &Sender, + theme: SharedTheme, + key_config: SharedKeyConfig, + ) -> Self { + Self { + queue: queue.clone(), + pending: false, + visible: false, + async_fetch: AsyncSingleJob::new(sender.clone()), + progress: None, + input_cred: CredComponent::new( + theme.clone(), + key_config.clone(), + ), + theme, + key_config, + } + } + + /// + pub fn fetch(&mut self) -> Result<()> { + self.show()?; + if need_username_password()? { + let cred = + extract_username_password().unwrap_or_else(|_| { + BasicAuthCredential::new(None, None) + }); + if cred.is_complete() { + self.fetch_all(Some(cred)); + } else { + self.input_cred.set_cred(cred); + self.input_cred.show()?; + } + } else { + self.fetch_all(None); + } + + Ok(()) + } + + fn fetch_all(&mut self, cred: Option) { + self.pending = true; + self.progress = None; + self.progress = Some(ProgressPercent::empty()); + self.async_fetch.spawn(AsyncFetchJob::new(cred)); + } + + /// + pub const fn any_work_pending(&self) -> bool { + self.pending + } + + /// + pub fn update_git(&mut self, ev: AsyncGitNotification) { + if self.is_visible() && ev == AsyncGitNotification::Fetch { + self.update(); + } + } + + /// + fn update(&mut self) { + self.pending = self.async_fetch.is_pending(); + self.progress = self.async_fetch.progress(); + + if !self.pending { + self.hide(); + self.queue + .push(InternalEvent::Update(NeedsUpdate::BRANCHES)); + } + } +} + +impl DrawableComponent for FetchComponent { + fn draw( + &self, + f: &mut Frame, + rect: Rect, + ) -> Result<()> { + if self.visible { + let progress = self.progress.unwrap_or_default().progress; + + let area = ui::centered_rect_absolute(30, 3, f.size()); + + f.render_widget(Clear, area); + f.render_widget( + Gauge::default() + .block( + Block::default() + .title(Span::styled( + strings::FETCH_POPUP_MSG, + self.theme.title(true), + )) + .borders(Borders::ALL) + .border_type(BorderType::Thick) + .border_style(self.theme.block(true)), + ) + .gauge_style(self.theme.push_gauge()) + .percent(u16::from(progress)), + area, + ); + self.input_cred.draw(f, rect)?; + } + + Ok(()) + } +} + +impl Component for FetchComponent { + fn commands( + &self, + out: &mut Vec, + force_all: bool, + ) -> CommandBlocking { + if self.is_visible() || force_all { + if !force_all { + out.clear(); + } + + if self.input_cred.is_visible() { + return self.input_cred.commands(out, force_all); + } + 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 { + if self.visible { + if let Event::Key(_) = ev { + if self.input_cred.is_visible() { + self.input_cred.event(ev)?; + + if self.input_cred.get_cred().is_complete() + || !self.input_cred.is_visible() + { + self.fetch_all(Some( + self.input_cred.get_cred().clone(), + )); + self.input_cred.hide(); + } + } + } + return Ok(EventState::Consumed); + } + Ok(EventState::NotConsumed) + } + + fn is_visible(&self) -> bool { + self.visible + } + + fn hide(&mut self) { + self.visible = false; + } + + fn show(&mut self) -> Result<()> { + self.visible = true; + + Ok(()) + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs index 3dcf25c55d..6e040276a1 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -10,6 +10,7 @@ mod create_branch; mod cred; mod diff; mod externaleditor; +mod fetch; mod file_find_popup; mod filetree; mod help; @@ -42,6 +43,7 @@ pub use compare_commits::CompareCommitsComponent; pub use create_branch::CreateBranchComponent; pub use diff::DiffComponent; pub use externaleditor::ExternalEditorComponent; +pub use fetch::FetchComponent; pub use file_find_popup::FileFindPopup; pub use help::HelpComponent; pub use inspect_commit::InspectCommitComponent; diff --git a/src/components/pull.rs b/src/components/pull.rs index 7e807e013a..a45bb110cf 100644 --- a/src/components/pull.rs +++ b/src/components/pull.rs @@ -19,7 +19,7 @@ use asyncgit::{ }, get_default_remote, }, - AsyncFetch, AsyncGitNotification, FetchRequest, RemoteProgress, + AsyncGitNotification, AsyncPull, FetchRequest, RemoteProgress, CWD, }; use crossbeam_channel::Sender; @@ -35,7 +35,7 @@ use tui::{ /// pub struct PullComponent { visible: bool, - git_fetch: AsyncFetch, + git_fetch: AsyncPull, progress: Option, pending: bool, branch: String, @@ -58,7 +58,7 @@ impl PullComponent { pending: false, visible: false, branch: String::new(), - git_fetch: AsyncFetch::new(sender), + git_fetch: AsyncPull::new(sender), progress: None, input_cred: CredComponent::new( theme.clone(), @@ -111,7 +111,7 @@ impl PullComponent { /// pub fn update_git(&mut self, ev: AsyncGitNotification) { - if self.is_visible() && ev == AsyncGitNotification::Fetch { + if self.is_visible() && ev == AsyncGitNotification::Pull { if let Err(error) = self.update() { self.pending = false; self.hide(); diff --git a/src/keys/key_list_file.rs b/src/keys/key_list_file.rs index 7290d510a9..451c8c82ac 100644 --- a/src/keys/key_list_file.rs +++ b/src/keys/key_list_file.rs @@ -81,7 +81,6 @@ pub struct KeysListFile { } impl KeysListFile { - #[allow(dead_code)] pub fn read_file(config_file: PathBuf) -> Result { let mut f = File::open(config_file)?; let mut buffer = Vec::new(); diff --git a/src/queue.rs b/src/queue.rs index 1160730e9e..63c8a9ee41 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -97,6 +97,8 @@ pub enum InternalEvent { OpenFileFinder(Vec), /// FileFinderChanged(Option), + /// + FetchRemotes, } /// single threaded simple queue for components to communicate with each other diff --git a/src/strings.rs b/src/strings.rs index a4931444aa..f056097408 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -10,6 +10,7 @@ pub mod order { pub static PUSH_POPUP_MSG: &str = "Push"; pub static FORCE_PUSH_POPUP_MSG: &str = "Force Push"; pub static PULL_POPUP_MSG: &str = "Pull"; +pub static FETCH_POPUP_MSG: &str = "Fetch"; pub static PUSH_POPUP_PROGRESS_NONE: &str = "preparing..."; pub static PUSH_POPUP_STATES_ADDING: &str = "adding objects (1/3)"; pub static PUSH_POPUP_STATES_DELTAS: &str = "deltas (2/3)"; @@ -1266,4 +1267,17 @@ pub mod commands { CMD_GROUP_GENERAL, ) } + + pub fn fetch_remotes( + key_config: &SharedKeyConfig, + ) -> CommandText { + CommandText::new( + format!( + "Fetch [{}]", + key_config.get_hint(key_config.keys.pull), + ), + "fetch/prune", + CMD_GROUP_BRANCHES, + ) + } } diff --git a/src/tabs/status.rs b/src/tabs/status.rs index e5c1434b0f..6fa2d10970 100644 --- a/src/tabs/status.rs +++ b/src/tabs/status.rs @@ -401,7 +401,7 @@ impl Status { AsyncGitNotification::Diff => self.update_diff()?, AsyncGitNotification::Status => self.update_status()?, AsyncGitNotification::Push - | AsyncGitNotification::Fetch + | AsyncGitNotification::Pull | AsyncGitNotification::CommitFiles => { self.branch_compare(); }