fetch/prune branches (#1000)

This commit is contained in:
Stephan Dilly 2021-11-23 22:14:37 +01:00 committed by GitHub
parent ee7ec691f0
commit 33ac72c8e7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 412 additions and 20 deletions

77
asyncgit/src/fetch_job.rs Normal file
View file

@ -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<BasicAuthCredential>),
Response(Result<()>),
}
///
#[derive(Clone, Default)]
pub struct AsyncFetchJob {
state: Arc<Mutex<Option<JobState>>>,
}
///
impl AsyncFetchJob {
///
pub fn new(
basic_credential: Option<BasicAuthCredential>,
) -> Self {
Self {
state: Arc::new(Mutex::new(Some(JobState::Request(
basic_credential,
)))),
}
}
///
pub fn result(&self) -> Option<Result<()>> {
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<Self::Notification, Self::Progress>,
) -> Result<Self::Notification> {
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)
}
}

View file

@ -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 `./`

View file

@ -28,14 +28,14 @@ pub struct FetchRequest {
struct FetchState {}
///
pub struct AsyncFetch {
pub struct AsyncPull {
state: Arc<Mutex<Option<FetchState>>>,
last_result: Arc<Mutex<Option<(usize, String)>>>,
progress: Arc<Mutex<Option<ProgressNotification>>>,
sender: Sender<AsyncGitNotification>,
}
impl AsyncFetch {
impl AsyncPull {
///
pub fn new(sender: &Sender<AsyncGitNotification>) -> 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");
});

View file

@ -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<BasicAuthCredential>,
progress_sender: Option<Sender<ProgressNotification>>,
) -> 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<BasicAuthCredential>,
progress_sender: &Option<Sender<ProgressPercent>>,
) -> Result<()> {
scope_time!("fetch_all");
let repo = utils::repo(repo_path)?;
let remotes = repo
.remotes()?
.iter()
.flatten()
.map(String::from)
.collect::<Vec<_>>();
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<BasicAuthCredential>,
progress_sender: Option<Sender<ProgressNotification>>,
) -> Result<usize> {
scope_time!("fetch_origin");
scope_time!("fetch");
let repo = utils::repo(repo_path)?;
let branch_ref = repo

View file

@ -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);

View file

@ -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);

211
src/components/fetch.rs Normal file
View file

@ -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<AsyncFetchJob>,
progress: Option<ProgressPercent>,
pending: bool,
queue: Queue,
theme: SharedTheme,
key_config: SharedKeyConfig,
input_cred: CredComponent,
}
impl FetchComponent {
///
pub fn new(
queue: &Queue,
sender: &Sender<AsyncGitNotification>,
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<BasicAuthCredential>) {
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<B: Backend>(
&self,
f: &mut Frame<B>,
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<CommandInfo>,
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<EventState> {
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(())
}
}

View file

@ -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;

View file

@ -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<RemoteProgress>,
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();

View file

@ -81,7 +81,6 @@ pub struct KeysListFile {
}
impl KeysListFile {
#[allow(dead_code)]
pub fn read_file(config_file: PathBuf) -> Result<Self> {
let mut f = File::open(config_file)?;
let mut buffer = Vec::new();

View file

@ -97,6 +97,8 @@ pub enum InternalEvent {
OpenFileFinder(Vec<TreeFile>),
///
FileFinderChanged(Option<PathBuf>),
///
FetchRemotes,
}
/// single threaded simple queue for components to communicate with each other

View file

@ -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,
)
}
}

View file

@ -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();
}