fix: guard shared background-job channels against concurrent overwrite
Bulk-import, import-directory, remove-git-from-all, create-repo, publish-existing, create-PR, create-issue, profile-push, and profile-pull all set state.pull_all_rx/create_repo_rx/profile_pull_rx unconditionally. Starting a second one of these before the first finished silently dropped the first job's progress/result (its sends now went to an orphaned receiver), and for the workspace-mutating jobs (remove-git-all racing an in-flight clone) could corrupt repos on disk. Each spawn site now checks the channel is free first and tells the user to wait instead.
This commit is contained in:
1 parent
a87db8433c
commit
f5fc22eae7
2 files changed
+36
No files matched your search
@@ -101,6 +101,10 @@ pub fn try_confirm(state: &mut HubState, modal: Modal) -> Option<Modal> {
|
|||||||
state.status_msg = Some(format!("Path does not exist: {}", src.display()));
|
state.status_msg = Some(format!("Path does not exist: {}", src.display()));
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
if state.pull_all_rx.is_some() {
|
||||||
|
state.status_msg = Some("A workspace job is already running — wait for it to finish.".into());
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let workspace = state.workspace_root.clone();
|
let workspace = state.workspace_root.clone();
|
||||||
let (tx, rx) = std::sync::mpsc::channel::<String>();
|
let (tx, rx) = std::sync::mpsc::channel::<String>();
|
||||||
state.pull_all_rx = Some(rx);
|
state.pull_all_rx = Some(rx);
|
||||||
@@ -186,6 +190,10 @@ pub fn try_confirm(state: &mut HubState, modal: Modal) -> Option<Modal> {
|
|||||||
|
|
||||||
// ── Remove .git from all workspace repos ──────────────────────────────
|
// ── Remove .git from all workspace repos ──────────────────────────────
|
||||||
Modal::Confirm { tag: ConfirmTag::RemoveGitAll, .. } => {
|
Modal::Confirm { tag: ConfirmTag::RemoveGitAll, .. } => {
|
||||||
|
if state.pull_all_rx.is_some() {
|
||||||
|
state.status_msg = Some("A workspace job is already running — wait for it to finish.".into());
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let workspace = state.workspace_root.clone();
|
let workspace = state.workspace_root.clone();
|
||||||
let (tx, rx) = std::sync::mpsc::channel::<String>();
|
let (tx, rx) = std::sync::mpsc::channel::<String>();
|
||||||
state.pull_all_rx = Some(rx);
|
state.pull_all_rx = Some(rx);
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ pub fn spawn_create_repo(
|
|||||||
commands: Vec<String>,
|
commands: Vec<String>,
|
||||||
owner: Option<String>,
|
owner: Option<String>,
|
||||||
) {
|
) {
|
||||||
|
if state.create_repo_rx.is_some() {
|
||||||
|
state.status_msg = Some("A create/publish/PR/issue job is already running — wait for it to finish.".into());
|
||||||
|
return;
|
||||||
|
}
|
||||||
let (tx, rx) = std::sync::mpsc::channel::<CreateRepoMsg>();
|
let (tx, rx) = std::sync::mpsc::channel::<CreateRepoMsg>();
|
||||||
state.create_repo_rx = Some(rx);
|
state.create_repo_rx = Some(rx);
|
||||||
state.modal = Modal::Info {
|
state.modal = Modal::Info {
|
||||||
@@ -74,6 +78,10 @@ pub fn spawn_create_repo(
|
|||||||
/// Like `spawn_create_repo`, but for a folder that already has git commits —
|
/// Like `spawn_create_repo`, but for a folder that already has git commits —
|
||||||
/// only creates the remote (if missing) and pushes; never stages or commits.
|
/// only creates the remote (if missing) and pushes; never stages or commits.
|
||||||
pub fn spawn_publish_existing(state: &mut HubState, path: std::path::PathBuf, display_name: String, owner: Option<String>) {
|
pub fn spawn_publish_existing(state: &mut HubState, path: std::path::PathBuf, display_name: String, owner: Option<String>) {
|
||||||
|
if state.create_repo_rx.is_some() {
|
||||||
|
state.status_msg = Some("A create/publish/PR/issue job is already running — wait for it to finish.".into());
|
||||||
|
return;
|
||||||
|
}
|
||||||
let (tx, rx) = std::sync::mpsc::channel::<CreateRepoMsg>();
|
let (tx, rx) = std::sync::mpsc::channel::<CreateRepoMsg>();
|
||||||
state.create_repo_rx = Some(rx);
|
state.create_repo_rx = Some(rx);
|
||||||
state.modal = Modal::Info {
|
state.modal = Modal::Info {
|
||||||
@@ -121,6 +129,10 @@ pub fn spawn_publish_existing(state: &mut HubState, path: std::path::PathBuf, di
|
|||||||
/// creation, but the generic Progress/Done shape and the `Modal::Info` polling
|
/// creation, but the generic Progress/Done shape and the `Modal::Info` polling
|
||||||
/// already wired up in `keys.rs` apply just as well here.
|
/// already wired up in `keys.rs` apply just as well here.
|
||||||
pub fn spawn_create_pr(state: &mut HubState, repo: String, title: String, head: String, base: String) {
|
pub fn spawn_create_pr(state: &mut HubState, repo: String, title: String, head: String, base: String) {
|
||||||
|
if state.create_repo_rx.is_some() {
|
||||||
|
state.status_msg = Some("A create/publish/PR/issue job is already running — wait for it to finish.".into());
|
||||||
|
return;
|
||||||
|
}
|
||||||
let (tx, rx) = std::sync::mpsc::channel::<CreateRepoMsg>();
|
let (tx, rx) = std::sync::mpsc::channel::<CreateRepoMsg>();
|
||||||
state.create_repo_rx = Some(rx);
|
state.create_repo_rx = Some(rx);
|
||||||
state.modal = Modal::Info {
|
state.modal = Modal::Info {
|
||||||
@@ -160,6 +172,10 @@ pub fn spawn_create_pr(state: &mut HubState, repo: String, title: String, head:
|
|||||||
/// Opens an issue via the Quick Actions panel. Same shared-channel pattern as
|
/// Opens an issue via the Quick Actions panel. Same shared-channel pattern as
|
||||||
/// `spawn_create_pr` above.
|
/// `spawn_create_pr` above.
|
||||||
pub fn spawn_create_issue(state: &mut HubState, repo: String, title: String) {
|
pub fn spawn_create_issue(state: &mut HubState, repo: String, title: String) {
|
||||||
|
if state.create_repo_rx.is_some() {
|
||||||
|
state.status_msg = Some("A create/publish/PR/issue job is already running — wait for it to finish.".into());
|
||||||
|
return;
|
||||||
|
}
|
||||||
let (tx, rx) = std::sync::mpsc::channel::<CreateRepoMsg>();
|
let (tx, rx) = std::sync::mpsc::channel::<CreateRepoMsg>();
|
||||||
state.create_repo_rx = Some(rx);
|
state.create_repo_rx = Some(rx);
|
||||||
state.modal = Modal::Info {
|
state.modal = Modal::Info {
|
||||||
@@ -330,6 +346,10 @@ pub fn spawn_convention_preview(state: &mut HubState, repo: std::path::PathBuf,
|
|||||||
/// `Modal::Info` pattern; reuses `CreateRepoMsg` since a push needs no
|
/// `Modal::Info` pattern; reuses `CreateRepoMsg` since a push needs no
|
||||||
/// payload back, just success/failure.
|
/// payload back, just success/failure.
|
||||||
pub fn spawn_profile_push(state: &mut HubState, snap: crate::profile::SettingsSnapshot) {
|
pub fn spawn_profile_push(state: &mut HubState, snap: crate::profile::SettingsSnapshot) {
|
||||||
|
if state.create_repo_rx.is_some() {
|
||||||
|
state.status_msg = Some("A create/publish/PR/issue job is already running — wait for it to finish.".into());
|
||||||
|
return;
|
||||||
|
}
|
||||||
let (tx, rx) = std::sync::mpsc::channel::<CreateRepoMsg>();
|
let (tx, rx) = std::sync::mpsc::channel::<CreateRepoMsg>();
|
||||||
state.create_repo_rx = Some(rx);
|
state.create_repo_rx = Some(rx);
|
||||||
let repo_name = crate::profile::SETTINGS_REPO_NAME.to_string();
|
let repo_name = crate::profile::SETTINGS_REPO_NAME.to_string();
|
||||||
@@ -364,6 +384,10 @@ pub fn spawn_profile_push(state: &mut HubState, snap: crate::profile::SettingsSn
|
|||||||
/// instead of `CreateRepoMsg`.
|
/// instead of `CreateRepoMsg`.
|
||||||
pub fn spawn_profile_pull(state: &mut HubState) {
|
pub fn spawn_profile_pull(state: &mut HubState) {
|
||||||
use super::state::ProfilePullMsg;
|
use super::state::ProfilePullMsg;
|
||||||
|
if state.profile_pull_rx.is_some() {
|
||||||
|
state.status_msg = Some("A settings pull is already running — wait for it to finish.".into());
|
||||||
|
return;
|
||||||
|
}
|
||||||
let (tx, rx) = std::sync::mpsc::channel::<ProfilePullMsg>();
|
let (tx, rx) = std::sync::mpsc::channel::<ProfilePullMsg>();
|
||||||
state.profile_pull_rx = Some(rx);
|
state.profile_pull_rx = Some(rx);
|
||||||
let repo_name = crate::profile::SETTINGS_REPO_NAME.to_string();
|
let repo_name = crate::profile::SETTINGS_REPO_NAME.to_string();
|
||||||
@@ -411,6 +435,10 @@ pub fn open_bulk_import_picker(state: &mut HubState, acc_idx: usize) {
|
|||||||
/// it auto-imports it immediately, since landing on this flow already means
|
/// it auto-imports it immediately, since landing on this flow already means
|
||||||
/// "I trust this account's data" and a second confirmation would be noise.
|
/// "I trust this account's data" and a second confirmation would be noise.
|
||||||
pub fn spawn_bulk_import(state: &mut HubState, acc_idx: usize, affiliation: crate::providers::RepoAffiliation) {
|
pub fn spawn_bulk_import(state: &mut HubState, acc_idx: usize, affiliation: crate::providers::RepoAffiliation) {
|
||||||
|
if state.pull_all_rx.is_some() {
|
||||||
|
state.status_msg = Some("A workspace job is already running — wait for it to finish.".into());
|
||||||
|
return;
|
||||||
|
}
|
||||||
let Some(acc) = state.accounts.get(acc_idx).cloned() else { return; };
|
let Some(acc) = state.accounts.get(acc_idx).cloned() else { return; };
|
||||||
let (tx, rx) = std::sync::mpsc::channel::<String>();
|
let (tx, rx) = std::sync::mpsc::channel::<String>();
|
||||||
state.pull_all_rx = Some(rx);
|
state.pull_all_rx = Some(rx);
|
||||||
|
|||||||
Reference in new issue
Block a user