1748 lines
60 KiB
Rust
1748 lines
60 KiB
Rust
use super::*;
|
|
use gitcomet_core::error::{Error, ErrorKind};
|
|
use gitcomet_core::services::{GitBackend, GitRepository, Result};
|
|
use std::path::Path;
|
|
use std::sync::Arc;
|
|
use std::time::SystemTime;
|
|
|
|
struct TestBackend;
|
|
|
|
impl GitBackend for TestBackend {
|
|
fn open(&self, _workdir: &Path) -> Result<Arc<dyn GitRepository>> {
|
|
Err(Error::new(ErrorKind::Unsupported(
|
|
"Test backend does not open repositories",
|
|
)))
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn commit_menu_has_add_tag_entry(cx: &mut gpui::TestAppContext) {
|
|
let (store, events) = AppStore::new(Arc::new(TestBackend));
|
|
let (view, cx) =
|
|
cx.add_window_view(|window, cx| GitCometView::new(store, events, None, window, cx));
|
|
|
|
let repo_id = RepoId(1);
|
|
let commit_id = CommitId("deadbeefdeadbeef".to_string());
|
|
let workdir = std::env::temp_dir().join(format!(
|
|
"gitcomet_ui_test_{}_commit_menu_tag",
|
|
std::process::id()
|
|
));
|
|
|
|
cx.update(|_window, app| {
|
|
view.update(app, |this, cx| {
|
|
let mut repo = RepoState::new_opening(
|
|
repo_id,
|
|
gitcomet_core::domain::RepoSpec {
|
|
workdir: workdir.clone(),
|
|
},
|
|
);
|
|
repo.log = Loadable::Ready(
|
|
gitcomet_core::domain::LogPage {
|
|
commits: vec![gitcomet_core::domain::Commit {
|
|
id: commit_id.clone(),
|
|
parent_ids: vec![],
|
|
summary: "Hello".to_string(),
|
|
author: "Alice".to_string(),
|
|
time: SystemTime::UNIX_EPOCH,
|
|
}],
|
|
next_cursor: None,
|
|
}
|
|
.into(),
|
|
);
|
|
repo.tags = Loadable::Ready(Arc::new(vec![]));
|
|
|
|
this.state = Arc::new(AppState {
|
|
repos: vec![repo],
|
|
active_repo: Some(repo_id),
|
|
..Default::default()
|
|
});
|
|
cx.notify();
|
|
});
|
|
});
|
|
|
|
cx.update(|_window, app| {
|
|
let model = view
|
|
.update(app, |this, cx| {
|
|
this.popover_host.update(cx, |host, cx| {
|
|
host.context_menu_model(
|
|
&PopoverKind::CommitMenu {
|
|
repo_id,
|
|
commit_id: commit_id.clone(),
|
|
},
|
|
cx,
|
|
)
|
|
})
|
|
})
|
|
.expect("expected commit context menu model");
|
|
|
|
let add_tag_action = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry { label, action, .. } if label.as_ref() == "Add tag…" => {
|
|
Some((**action).clone())
|
|
}
|
|
_ => None,
|
|
});
|
|
|
|
let Some(ContextMenuAction::OpenPopover { kind }) = add_tag_action else {
|
|
panic!("expected Add tag… to open a popover");
|
|
};
|
|
|
|
let PopoverKind::CreateTagPrompt {
|
|
repo_id: rid,
|
|
target,
|
|
} = kind
|
|
else {
|
|
panic!("expected Add tag… to open CreateTagPrompt");
|
|
};
|
|
|
|
assert_eq!(rid, repo_id);
|
|
assert_eq!(target, commit_id.as_ref().to_string());
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn commit_file_menu_has_open_file_entries(cx: &mut gpui::TestAppContext) {
|
|
let (store, events) = AppStore::new(Arc::new(TestBackend));
|
|
let (view, cx) =
|
|
cx.add_window_view(|window, cx| GitCometView::new(store, events, None, window, cx));
|
|
|
|
let repo_id = RepoId(2);
|
|
let commit_id = CommitId("deadbeefdeadbeef".to_string());
|
|
let path = std::path::PathBuf::from("src/main.rs");
|
|
|
|
cx.update(|_window, app| {
|
|
let model = view
|
|
.update(app, |this, cx| {
|
|
this.popover_host.update(cx, |host, cx| {
|
|
host.context_menu_model(
|
|
&PopoverKind::CommitFileMenu {
|
|
repo_id,
|
|
commit_id: commit_id.clone(),
|
|
path: path.clone(),
|
|
},
|
|
cx,
|
|
)
|
|
})
|
|
})
|
|
.expect("expected commit file context menu model");
|
|
|
|
let open_file_action = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry { label, action, .. } if label.as_ref() == "Open file" => {
|
|
Some((**action).clone())
|
|
}
|
|
_ => None,
|
|
});
|
|
match open_file_action {
|
|
Some(ContextMenuAction::OpenFile {
|
|
repo_id: rid,
|
|
path: p,
|
|
}) => {
|
|
assert_eq!(rid, repo_id);
|
|
assert_eq!(p, path);
|
|
}
|
|
_ => panic!("expected Open file entry with OpenFile action"),
|
|
}
|
|
|
|
let open_location_action = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry { label, action, .. }
|
|
if label.as_ref() == "Open file location" =>
|
|
{
|
|
Some((**action).clone())
|
|
}
|
|
_ => None,
|
|
});
|
|
match open_location_action {
|
|
Some(ContextMenuAction::OpenFileLocation {
|
|
repo_id: rid,
|
|
path: p,
|
|
}) => {
|
|
assert_eq!(rid, repo_id);
|
|
assert_eq!(p, path);
|
|
}
|
|
_ => panic!("expected Open file location entry with OpenFileLocation action"),
|
|
}
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn status_file_menu_has_open_file_entries(cx: &mut gpui::TestAppContext) {
|
|
let (store, events) = AppStore::new(Arc::new(TestBackend));
|
|
let (view, cx) =
|
|
cx.add_window_view(|window, cx| GitCometView::new(store, events, None, window, cx));
|
|
|
|
let repo_id = RepoId(3);
|
|
let workdir = std::env::temp_dir().join(format!(
|
|
"gitcomet_ui_test_{}_status_menu_open_file",
|
|
std::process::id()
|
|
));
|
|
let path = std::path::PathBuf::from("a.txt");
|
|
|
|
cx.update(|_window, app| {
|
|
view.update(app, |this, _cx| {
|
|
let mut repo = RepoState::new_opening(
|
|
repo_id,
|
|
gitcomet_core::domain::RepoSpec {
|
|
workdir: workdir.clone(),
|
|
},
|
|
);
|
|
repo.status = Loadable::Ready(
|
|
gitcomet_core::domain::RepoStatus {
|
|
staged: vec![],
|
|
unstaged: vec![gitcomet_core::domain::FileStatus {
|
|
path: path.clone(),
|
|
kind: gitcomet_core::domain::FileStatusKind::Modified,
|
|
conflict: None,
|
|
}],
|
|
}
|
|
.into(),
|
|
);
|
|
|
|
this.state = Arc::new(AppState {
|
|
repos: vec![repo],
|
|
active_repo: Some(repo_id),
|
|
..Default::default()
|
|
});
|
|
});
|
|
});
|
|
|
|
cx.update(|_window, app| {
|
|
let model = view
|
|
.update(app, |this, cx| {
|
|
this.popover_host.update(cx, |host, cx| {
|
|
host.context_menu_model(
|
|
&PopoverKind::StatusFileMenu {
|
|
repo_id,
|
|
area: DiffArea::Unstaged,
|
|
path: path.clone(),
|
|
},
|
|
cx,
|
|
)
|
|
})
|
|
})
|
|
.expect("expected status file context menu model");
|
|
|
|
let open_file_action = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry { label, action, .. } if label.as_ref() == "Open file" => {
|
|
Some((**action).clone())
|
|
}
|
|
_ => None,
|
|
});
|
|
match open_file_action {
|
|
Some(ContextMenuAction::OpenFile {
|
|
repo_id: rid,
|
|
path: p,
|
|
}) => {
|
|
assert_eq!(rid, repo_id);
|
|
assert_eq!(p, path);
|
|
}
|
|
_ => panic!("expected Open file entry with OpenFile action"),
|
|
}
|
|
|
|
let open_location_action = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry { label, action, .. }
|
|
if label.as_ref() == "Open file location" =>
|
|
{
|
|
Some((**action).clone())
|
|
}
|
|
_ => None,
|
|
});
|
|
match open_location_action {
|
|
Some(ContextMenuAction::OpenFileLocation {
|
|
repo_id: rid,
|
|
path: p,
|
|
}) => {
|
|
assert_eq!(rid, repo_id);
|
|
assert_eq!(p, path);
|
|
}
|
|
_ => panic!("expected Open file location entry with OpenFileLocation action"),
|
|
}
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn diff_editor_menu_has_open_file_entries(cx: &mut gpui::TestAppContext) {
|
|
let (store, events) = AppStore::new(Arc::new(TestBackend));
|
|
let (view, cx) =
|
|
cx.add_window_view(|window, cx| GitCometView::new(store, events, None, window, cx));
|
|
|
|
let repo_id = RepoId(4);
|
|
let path = std::path::PathBuf::from("a.txt");
|
|
|
|
cx.update(|_window, app| {
|
|
let model = view
|
|
.update(app, |this, cx| {
|
|
this.popover_host.update(cx, |host, cx| {
|
|
host.context_menu_model(
|
|
&PopoverKind::DiffEditorMenu {
|
|
repo_id,
|
|
area: DiffArea::Unstaged,
|
|
path: Some(path.clone()),
|
|
hunk_patch: None,
|
|
hunks_count: 0,
|
|
lines_patch: None,
|
|
discard_lines_patch: None,
|
|
lines_count: 0,
|
|
copy_text: Some("x".to_string()),
|
|
},
|
|
cx,
|
|
)
|
|
})
|
|
})
|
|
.expect("expected diff editor context menu model");
|
|
|
|
let open_file_action = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry { label, action, .. } if label.as_ref() == "Open file" => {
|
|
Some((**action).clone())
|
|
}
|
|
_ => None,
|
|
});
|
|
match open_file_action {
|
|
Some(ContextMenuAction::OpenFile {
|
|
repo_id: rid,
|
|
path: p,
|
|
}) => {
|
|
assert_eq!(rid, repo_id);
|
|
assert_eq!(p, path);
|
|
}
|
|
_ => panic!("expected Open file entry with OpenFile action"),
|
|
}
|
|
|
|
let open_location_action = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry { label, action, .. }
|
|
if label.as_ref() == "Open file location" =>
|
|
{
|
|
Some((**action).clone())
|
|
}
|
|
_ => None,
|
|
});
|
|
match open_location_action {
|
|
Some(ContextMenuAction::OpenFileLocation {
|
|
repo_id: rid,
|
|
path: p,
|
|
}) => {
|
|
assert_eq!(rid, repo_id);
|
|
assert_eq!(p, path);
|
|
}
|
|
_ => panic!("expected Open file location entry with OpenFileLocation action"),
|
|
}
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn file_preview_context_menu_matches_diff_editor_actions(cx: &mut gpui::TestAppContext) {
|
|
let (store, events) = AppStore::new(Arc::new(TestBackend));
|
|
let (view, cx) =
|
|
cx.add_window_view(|window, cx| GitCometView::new(store, events, None, window, cx));
|
|
|
|
let repo_id = RepoId(44);
|
|
let workdir = std::env::temp_dir().join(format!(
|
|
"gitcomet_ui_test_{}_preview_context_menu",
|
|
std::process::id()
|
|
));
|
|
let path = std::path::PathBuf::from("added.txt");
|
|
std::fs::create_dir_all(&workdir).expect("create preview test workdir");
|
|
std::fs::write(workdir.join(&path), "alpha\nbeta\n").expect("write preview test file");
|
|
|
|
cx.update(|_window, app| {
|
|
view.update(app, |this, cx| {
|
|
let mut repo = RepoState::new_opening(
|
|
repo_id,
|
|
gitcomet_core::domain::RepoSpec {
|
|
workdir: workdir.clone(),
|
|
},
|
|
);
|
|
repo.status = Loadable::Ready(
|
|
gitcomet_core::domain::RepoStatus {
|
|
staged: vec![gitcomet_core::domain::FileStatus {
|
|
path: path.clone(),
|
|
kind: gitcomet_core::domain::FileStatusKind::Added,
|
|
conflict: None,
|
|
}],
|
|
unstaged: vec![],
|
|
}
|
|
.into(),
|
|
);
|
|
repo.diff_state.diff_target = Some(gitcomet_core::domain::DiffTarget::WorkingTree {
|
|
path: path.clone(),
|
|
area: DiffArea::Staged,
|
|
});
|
|
repo.diff_state.diff_file =
|
|
Loadable::Ready(Some(Arc::new(gitcomet_core::domain::FileDiffText {
|
|
path: path.clone(),
|
|
old: None,
|
|
new: Some("alpha\nbeta\n".to_string()),
|
|
})));
|
|
|
|
let next_state = Arc::new(AppState {
|
|
repos: vec![repo],
|
|
active_repo: Some(repo_id),
|
|
..Default::default()
|
|
});
|
|
this._ui_model.update(cx, |model, cx| {
|
|
model.set_state(next_state, cx);
|
|
});
|
|
});
|
|
});
|
|
|
|
cx.update(|window, app| {
|
|
let main_pane = view.read(app).main_pane.clone();
|
|
main_pane.update(app, |pane, cx| {
|
|
pane.try_populate_worktree_preview_from_diff_file(cx);
|
|
pane.open_diff_editor_context_menu(
|
|
1,
|
|
DiffTextRegion::Inline,
|
|
point(px(24.0), px(24.0)),
|
|
window,
|
|
cx,
|
|
);
|
|
});
|
|
});
|
|
|
|
// Flush deferred popover open from MainPaneView::open_popover_at.
|
|
cx.update(|window, app| {
|
|
let _ = window.draw(app);
|
|
});
|
|
|
|
cx.update(|_window, app| {
|
|
view.update(app, |this, cx| {
|
|
this.popover_host.update(cx, |host, cx| {
|
|
let Some(popover_kind) = host.popover.clone() else {
|
|
panic!("expected file preview right-click to open a context menu");
|
|
};
|
|
|
|
match &popover_kind {
|
|
PopoverKind::DiffEditorMenu {
|
|
repo_id: rid,
|
|
area,
|
|
path: menu_path,
|
|
copy_text,
|
|
..
|
|
} => {
|
|
assert_eq!(*rid, repo_id);
|
|
assert_eq!(*area, DiffArea::Staged);
|
|
assert_eq!(menu_path, &Some(path.clone()));
|
|
assert_eq!(copy_text, &Some("beta".to_string()));
|
|
}
|
|
_ => panic!("expected DiffEditorMenu popover for file preview"),
|
|
}
|
|
|
|
let model = host
|
|
.context_menu_model(&popover_kind, cx)
|
|
.expect("expected diff editor menu model");
|
|
|
|
let labels: Vec<String> = model
|
|
.items
|
|
.iter()
|
|
.filter_map(|item| match item {
|
|
ContextMenuItem::Entry { label, .. } => Some(label.to_string()),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
for expected in [
|
|
"Unstage line",
|
|
"Unstage hunk",
|
|
"Open file",
|
|
"Open file location",
|
|
"Copy",
|
|
] {
|
|
assert!(
|
|
labels.iter().any(|label| label == expected),
|
|
"expected {expected} entry in preview context menu"
|
|
);
|
|
}
|
|
|
|
let open_file_action = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry { label, action, .. }
|
|
if label.as_ref() == "Open file" =>
|
|
{
|
|
Some((**action).clone())
|
|
}
|
|
_ => None,
|
|
});
|
|
match open_file_action {
|
|
Some(ContextMenuAction::OpenFile {
|
|
repo_id: rid,
|
|
path: p,
|
|
}) => {
|
|
assert_eq!(rid, repo_id);
|
|
assert_eq!(p, path);
|
|
}
|
|
_ => panic!("expected Open file action in preview context menu"),
|
|
}
|
|
|
|
let copy_action = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry { label, action, .. } if label.as_ref() == "Copy" => {
|
|
Some((**action).clone())
|
|
}
|
|
_ => None,
|
|
});
|
|
match copy_action {
|
|
Some(ContextMenuAction::CopyText { text }) => {
|
|
assert_eq!(text, "beta");
|
|
}
|
|
_ => panic!("expected Copy action in preview context menu"),
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn tag_menu_lists_delete_entries_for_commit_tags(cx: &mut gpui::TestAppContext) {
|
|
let (store, events) = AppStore::new(Arc::new(TestBackend));
|
|
let (view, cx) =
|
|
cx.add_window_view(|window, cx| GitCometView::new(store, events, None, window, cx));
|
|
|
|
let repo_id = RepoId(2);
|
|
let commit_id = CommitId("0123456789abcdef".to_string());
|
|
let other_commit = CommitId("aaaaaaaaaaaaaaaa".to_string());
|
|
let workdir =
|
|
std::env::temp_dir().join(format!("gitcomet_ui_test_{}_tag_menu", std::process::id()));
|
|
|
|
cx.update(|_window, app| {
|
|
view.update(app, |this, cx| {
|
|
let mut repo = RepoState::new_opening(
|
|
repo_id,
|
|
gitcomet_core::domain::RepoSpec {
|
|
workdir: workdir.clone(),
|
|
},
|
|
);
|
|
repo.log = Loadable::Ready(
|
|
gitcomet_core::domain::LogPage {
|
|
commits: vec![gitcomet_core::domain::Commit {
|
|
id: commit_id.clone(),
|
|
parent_ids: vec![],
|
|
summary: "Hello".to_string(),
|
|
author: "Alice".to_string(),
|
|
time: SystemTime::UNIX_EPOCH,
|
|
}],
|
|
next_cursor: None,
|
|
}
|
|
.into(),
|
|
);
|
|
repo.tags = Loadable::Ready(Arc::new(vec![
|
|
gitcomet_core::domain::Tag {
|
|
name: "release".to_string(),
|
|
target: commit_id.clone(),
|
|
},
|
|
gitcomet_core::domain::Tag {
|
|
name: "v1.0.0".to_string(),
|
|
target: commit_id.clone(),
|
|
},
|
|
gitcomet_core::domain::Tag {
|
|
name: "other".to_string(),
|
|
target: other_commit,
|
|
},
|
|
]));
|
|
|
|
let state = Arc::new(AppState {
|
|
repos: vec![repo],
|
|
active_repo: Some(repo_id),
|
|
..Default::default()
|
|
});
|
|
this.state = Arc::clone(&state);
|
|
this._ui_model
|
|
.update(cx, |model, cx| model.set_state(state, cx));
|
|
cx.notify();
|
|
});
|
|
});
|
|
|
|
cx.update(|_window, app| {
|
|
let model = view
|
|
.update(app, |this, cx| {
|
|
this.popover_host.update(cx, |host, cx| {
|
|
host.context_menu_model(
|
|
&PopoverKind::TagMenu {
|
|
repo_id,
|
|
commit_id: commit_id.clone(),
|
|
},
|
|
cx,
|
|
)
|
|
})
|
|
})
|
|
.expect("expected tag context menu model");
|
|
|
|
for name in ["release", "v1.0.0"] {
|
|
let expected_label = format!("Delete tag {name}");
|
|
let delete_action = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry { label, action, .. }
|
|
if label.as_ref() == expected_label.as_str() =>
|
|
{
|
|
Some((**action).clone())
|
|
}
|
|
_ => None,
|
|
});
|
|
match delete_action {
|
|
Some(ContextMenuAction::DeleteTag {
|
|
repo_id: rid,
|
|
name: n,
|
|
}) => {
|
|
assert_eq!(rid, repo_id);
|
|
assert_eq!(n, name);
|
|
}
|
|
_ => panic!("expected Delete tag {name} action"),
|
|
}
|
|
}
|
|
|
|
let has_other = model.items.iter().any(|item| match item {
|
|
ContextMenuItem::Entry { label, .. } => label.as_ref() == "Delete tag other",
|
|
_ => false,
|
|
});
|
|
assert!(
|
|
!has_other,
|
|
"tag menu should only show tags on the clicked commit"
|
|
);
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn tag_menu_lists_remote_push_and_delete_entries(cx: &mut gpui::TestAppContext) {
|
|
let (store, events) = AppStore::new(Arc::new(TestBackend));
|
|
let (view, cx) =
|
|
cx.add_window_view(|window, cx| GitCometView::new(store, events, None, window, cx));
|
|
|
|
let repo_id = RepoId(20);
|
|
let commit_id = CommitId("fedcba9876543210".to_string());
|
|
let workdir = std::env::temp_dir().join(format!(
|
|
"gitcomet_ui_test_{}_tag_menu_remote",
|
|
std::process::id()
|
|
));
|
|
|
|
cx.update(|_window, app| {
|
|
view.update(app, |this, cx| {
|
|
let mut repo = RepoState::new_opening(
|
|
repo_id,
|
|
gitcomet_core::domain::RepoSpec {
|
|
workdir: workdir.clone(),
|
|
},
|
|
);
|
|
repo.tags = Loadable::Ready(Arc::new(vec![gitcomet_core::domain::Tag {
|
|
name: "v2.0.0".to_string(),
|
|
target: commit_id.clone(),
|
|
}]));
|
|
repo.remotes = Loadable::Ready(Arc::new(vec![
|
|
gitcomet_core::domain::Remote {
|
|
name: "upstream".to_string(),
|
|
url: None,
|
|
},
|
|
gitcomet_core::domain::Remote {
|
|
name: "origin".to_string(),
|
|
url: None,
|
|
},
|
|
]));
|
|
repo.remote_tags = Loadable::Ready(Arc::new(vec![gitcomet_core::domain::RemoteTag {
|
|
remote: "origin".to_string(),
|
|
name: "v2.0.0".to_string(),
|
|
target: commit_id.clone(),
|
|
}]));
|
|
|
|
let state = Arc::new(AppState {
|
|
repos: vec![repo],
|
|
active_repo: Some(repo_id),
|
|
..Default::default()
|
|
});
|
|
this.state = Arc::clone(&state);
|
|
this._ui_model
|
|
.update(cx, |model, cx| model.set_state(state, cx));
|
|
cx.notify();
|
|
});
|
|
});
|
|
|
|
cx.update(|_window, app| {
|
|
let model = view
|
|
.update(app, |this, cx| {
|
|
this.popover_host.update(cx, |host, cx| {
|
|
host.context_menu_model(
|
|
&PopoverKind::TagMenu {
|
|
repo_id,
|
|
commit_id: commit_id.clone(),
|
|
},
|
|
cx,
|
|
)
|
|
})
|
|
})
|
|
.expect("expected tag context menu model");
|
|
|
|
for remote in ["origin", "upstream"] {
|
|
let push_label = format!("Push tag v2.0.0 to {remote}");
|
|
let push_action = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry { label, action, .. }
|
|
if label.as_ref() == push_label.as_str() =>
|
|
{
|
|
Some((**action).clone())
|
|
}
|
|
_ => None,
|
|
});
|
|
match push_action {
|
|
Some(ContextMenuAction::PushTag {
|
|
repo_id: rid,
|
|
remote: r,
|
|
name,
|
|
}) => {
|
|
assert_eq!(rid, repo_id);
|
|
assert_eq!(r, remote);
|
|
assert_eq!(name, "v2.0.0");
|
|
}
|
|
_ => panic!("expected Push tag action for remote {remote}"),
|
|
}
|
|
}
|
|
|
|
let delete_label = "Delete tag v2.0.0 from origin";
|
|
let delete_action = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry { label, action, .. } if label.as_ref() == delete_label => {
|
|
Some((**action).clone())
|
|
}
|
|
_ => None,
|
|
});
|
|
match delete_action {
|
|
Some(ContextMenuAction::DeleteRemoteTag {
|
|
repo_id: rid,
|
|
remote: r,
|
|
name,
|
|
}) => {
|
|
assert_eq!(rid, repo_id);
|
|
assert_eq!(r, "origin");
|
|
assert_eq!(name, "v2.0.0");
|
|
}
|
|
_ => panic!("expected Delete remote tag action for origin"),
|
|
}
|
|
|
|
let has_upstream_delete = model.items.iter().any(|item| match item {
|
|
ContextMenuItem::Entry { label, .. } => {
|
|
label.as_ref() == "Delete tag v2.0.0 from upstream"
|
|
}
|
|
_ => false,
|
|
});
|
|
assert!(
|
|
!has_upstream_delete,
|
|
"did not expect delete remote tag action for upstream without tag"
|
|
);
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn remote_menu_lists_fetch_and_prune_actions(cx: &mut gpui::TestAppContext) {
|
|
let (store, events) = AppStore::new(Arc::new(TestBackend));
|
|
let (view, cx) =
|
|
cx.add_window_view(|window, cx| GitCometView::new(store, events, None, window, cx));
|
|
|
|
let repo_id = RepoId(21);
|
|
let remote_name = "origin".to_string();
|
|
let workdir = std::env::temp_dir().join(format!(
|
|
"gitcomet_ui_test_{}_remote_menu_prune",
|
|
std::process::id()
|
|
));
|
|
|
|
cx.update(|_window, app| {
|
|
view.update(app, |this, cx| {
|
|
let mut repo = RepoState::new_opening(
|
|
repo_id,
|
|
gitcomet_core::domain::RepoSpec {
|
|
workdir: workdir.clone(),
|
|
},
|
|
);
|
|
repo.remotes = Loadable::Ready(Arc::new(vec![gitcomet_core::domain::Remote {
|
|
name: remote_name.clone(),
|
|
url: None,
|
|
}]));
|
|
|
|
let state = Arc::new(AppState {
|
|
repos: vec![repo],
|
|
active_repo: Some(repo_id),
|
|
..Default::default()
|
|
});
|
|
this.state = Arc::clone(&state);
|
|
this._ui_model
|
|
.update(cx, |model, cx| model.set_state(state, cx));
|
|
cx.notify();
|
|
});
|
|
});
|
|
|
|
cx.update(|_window, app| {
|
|
let model = view
|
|
.update(app, |this, cx| {
|
|
this.popover_host.update(cx, |host, cx| {
|
|
host.context_menu_model(
|
|
&PopoverKind::remote(
|
|
repo_id,
|
|
RemotePopoverKind::Menu {
|
|
name: remote_name.clone(),
|
|
},
|
|
),
|
|
cx,
|
|
)
|
|
})
|
|
})
|
|
.expect("expected remote context menu model");
|
|
|
|
let fetch = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry { label, action, .. } if label.as_ref() == "Fetch all" => {
|
|
Some((**action).clone())
|
|
}
|
|
_ => None,
|
|
});
|
|
assert!(matches!(
|
|
fetch,
|
|
Some(ContextMenuAction::FetchAll { repo_id: rid }) if rid == repo_id
|
|
));
|
|
|
|
let prune_branches = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry { label, action, .. }
|
|
if label.as_ref() == "Prune merged branches" =>
|
|
{
|
|
Some((**action).clone())
|
|
}
|
|
_ => None,
|
|
});
|
|
assert!(matches!(
|
|
prune_branches,
|
|
Some(ContextMenuAction::PruneMergedBranches { repo_id: rid }) if rid == repo_id
|
|
));
|
|
|
|
let prune_tags = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry { label, action, .. }
|
|
if label.as_ref() == "Prune local tags" =>
|
|
{
|
|
Some((**action).clone())
|
|
}
|
|
_ => None,
|
|
});
|
|
assert!(matches!(
|
|
prune_tags,
|
|
Some(ContextMenuAction::PruneLocalTags { repo_id: rid }) if rid == repo_id
|
|
));
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn local_branch_menu_has_pull_merge_and_squash_actions(cx: &mut gpui::TestAppContext) {
|
|
let (store, events) = AppStore::new(Arc::new(TestBackend));
|
|
let (view, cx) =
|
|
cx.add_window_view(|window, cx| GitCometView::new(store, events, None, window, cx));
|
|
|
|
let repo_id = RepoId(22);
|
|
let branch_name = "feature/awesome".to_string();
|
|
let workdir = std::env::temp_dir().join(format!(
|
|
"gitcomet_ui_test_{}_local_branch_menu_merge",
|
|
std::process::id()
|
|
));
|
|
|
|
cx.update(|_window, app| {
|
|
view.update(app, |this, cx| {
|
|
let mut repo = RepoState::new_opening(
|
|
repo_id,
|
|
gitcomet_core::domain::RepoSpec {
|
|
workdir: workdir.clone(),
|
|
},
|
|
);
|
|
repo.head_branch = Loadable::Ready("main".to_string());
|
|
|
|
let state = Arc::new(AppState {
|
|
repos: vec![repo],
|
|
active_repo: Some(repo_id),
|
|
..Default::default()
|
|
});
|
|
this.state = Arc::clone(&state);
|
|
this._ui_model
|
|
.update(cx, |model, cx| model.set_state(state, cx));
|
|
cx.notify();
|
|
});
|
|
});
|
|
|
|
cx.update(|_window, app| {
|
|
let model = view
|
|
.update(app, |this, cx| {
|
|
this.popover_host.update(cx, |host, cx| {
|
|
host.context_menu_model(
|
|
&PopoverKind::BranchMenu {
|
|
repo_id,
|
|
section: BranchSection::Local,
|
|
name: branch_name.clone(),
|
|
},
|
|
cx,
|
|
)
|
|
})
|
|
})
|
|
.expect("expected branch context menu model");
|
|
|
|
let pull_entry = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry {
|
|
label,
|
|
action,
|
|
disabled,
|
|
..
|
|
} if label.as_ref() == "Pull into current" => Some(((**action).clone(), *disabled)),
|
|
_ => None,
|
|
});
|
|
|
|
match pull_entry {
|
|
Some((
|
|
ContextMenuAction::PullBranch {
|
|
repo_id: rid,
|
|
remote,
|
|
branch,
|
|
},
|
|
disabled,
|
|
)) => {
|
|
assert_eq!(rid, repo_id);
|
|
assert_eq!(remote, ".");
|
|
assert_eq!(branch, branch_name);
|
|
assert!(!disabled);
|
|
}
|
|
_ => panic!("expected Pull into current entry with PullBranch action"),
|
|
}
|
|
|
|
let merge_entry = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry {
|
|
label,
|
|
action,
|
|
disabled,
|
|
..
|
|
} if label.as_ref() == "Merge into current" => Some(((**action).clone(), *disabled)),
|
|
_ => None,
|
|
});
|
|
|
|
match merge_entry {
|
|
Some((
|
|
ContextMenuAction::MergeRef {
|
|
repo_id: rid,
|
|
reference,
|
|
},
|
|
disabled,
|
|
)) => {
|
|
assert_eq!(rid, repo_id);
|
|
assert_eq!(reference, branch_name);
|
|
assert!(!disabled);
|
|
}
|
|
_ => panic!("expected Merge into current entry with MergeRef action"),
|
|
}
|
|
|
|
let squash_entry = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry {
|
|
label,
|
|
action,
|
|
disabled,
|
|
..
|
|
} if label.as_ref() == "Squash into current" => Some(((**action).clone(), *disabled)),
|
|
_ => None,
|
|
});
|
|
|
|
match squash_entry {
|
|
Some((
|
|
ContextMenuAction::SquashRef {
|
|
repo_id: rid,
|
|
reference,
|
|
},
|
|
disabled,
|
|
)) => {
|
|
assert_eq!(rid, repo_id);
|
|
assert_eq!(reference, branch_name);
|
|
assert!(!disabled);
|
|
}
|
|
_ => panic!("expected Squash into current entry with SquashRef action"),
|
|
}
|
|
|
|
let has_pull_into_current = model.items.iter().any(|item| match item {
|
|
ContextMenuItem::Entry { label, .. } => label.as_ref() == "Pull into current",
|
|
_ => false,
|
|
});
|
|
assert!(has_pull_into_current);
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn remote_branch_menu_has_pull_merge_and_squash_actions(cx: &mut gpui::TestAppContext) {
|
|
let (store, events) = AppStore::new(Arc::new(TestBackend));
|
|
let (view, cx) =
|
|
cx.add_window_view(|window, cx| GitCometView::new(store, events, None, window, cx));
|
|
|
|
let repo_id = RepoId(23);
|
|
let branch_name = "origin/feature/awesome".to_string();
|
|
let workdir = std::env::temp_dir().join(format!(
|
|
"gitcomet_ui_test_{}_remote_branch_menu_merge",
|
|
std::process::id()
|
|
));
|
|
|
|
cx.update(|_window, app| {
|
|
view.update(app, |this, cx| {
|
|
let mut repo = RepoState::new_opening(
|
|
repo_id,
|
|
gitcomet_core::domain::RepoSpec {
|
|
workdir: workdir.clone(),
|
|
},
|
|
);
|
|
repo.head_branch = Loadable::Ready("main".to_string());
|
|
|
|
let state = Arc::new(AppState {
|
|
repos: vec![repo],
|
|
active_repo: Some(repo_id),
|
|
..Default::default()
|
|
});
|
|
this.state = Arc::clone(&state);
|
|
this._ui_model
|
|
.update(cx, |model, cx| model.set_state(state, cx));
|
|
cx.notify();
|
|
});
|
|
});
|
|
|
|
cx.update(|_window, app| {
|
|
let model = view
|
|
.update(app, |this, cx| {
|
|
this.popover_host.update(cx, |host, cx| {
|
|
host.context_menu_model(
|
|
&PopoverKind::BranchMenu {
|
|
repo_id,
|
|
section: BranchSection::Remote,
|
|
name: branch_name.clone(),
|
|
},
|
|
cx,
|
|
)
|
|
})
|
|
})
|
|
.expect("expected branch context menu model");
|
|
|
|
let pull_entry = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry {
|
|
label,
|
|
action,
|
|
disabled,
|
|
..
|
|
} if label.as_ref() == "Pull into current" => Some(((**action).clone(), *disabled)),
|
|
_ => None,
|
|
});
|
|
|
|
match pull_entry {
|
|
Some((
|
|
ContextMenuAction::PullBranch {
|
|
repo_id: rid,
|
|
remote,
|
|
branch,
|
|
},
|
|
disabled,
|
|
)) => {
|
|
assert_eq!(rid, repo_id);
|
|
assert_eq!(remote, "origin");
|
|
assert_eq!(branch, "feature/awesome");
|
|
assert!(!disabled);
|
|
}
|
|
_ => panic!("expected Pull into current entry with PullBranch action"),
|
|
}
|
|
|
|
let merge_entry = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry {
|
|
label,
|
|
action,
|
|
disabled,
|
|
..
|
|
} if label.as_ref() == "Merge into current" => Some(((**action).clone(), *disabled)),
|
|
_ => None,
|
|
});
|
|
|
|
match merge_entry {
|
|
Some((
|
|
ContextMenuAction::MergeRef {
|
|
repo_id: rid,
|
|
reference,
|
|
},
|
|
disabled,
|
|
)) => {
|
|
assert_eq!(rid, repo_id);
|
|
assert_eq!(reference, branch_name);
|
|
assert!(!disabled);
|
|
}
|
|
_ => panic!("expected Merge into current entry with MergeRef action"),
|
|
}
|
|
|
|
let squash_entry = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry {
|
|
label,
|
|
action,
|
|
disabled,
|
|
..
|
|
} if label.as_ref() == "Squash into current" => Some(((**action).clone(), *disabled)),
|
|
_ => None,
|
|
});
|
|
|
|
match squash_entry {
|
|
Some((
|
|
ContextMenuAction::SquashRef {
|
|
repo_id: rid,
|
|
reference,
|
|
},
|
|
disabled,
|
|
)) => {
|
|
assert_eq!(rid, repo_id);
|
|
assert_eq!(reference, branch_name);
|
|
assert!(!disabled);
|
|
}
|
|
_ => panic!("expected Squash into current entry with SquashRef action"),
|
|
}
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn local_branch_menu_excludes_pull_merge_and_squash_for_current_branch(
|
|
cx: &mut gpui::TestAppContext,
|
|
) {
|
|
let (store, events) = AppStore::new(Arc::new(TestBackend));
|
|
let (view, cx) =
|
|
cx.add_window_view(|window, cx| GitCometView::new(store, events, None, window, cx));
|
|
|
|
let repo_id = RepoId(24);
|
|
let branch_name = "main".to_string();
|
|
let workdir = std::env::temp_dir().join(format!(
|
|
"gitcomet_ui_test_{}_local_branch_menu_current_branch",
|
|
std::process::id()
|
|
));
|
|
|
|
cx.update(|_window, app| {
|
|
view.update(app, |this, cx| {
|
|
let mut repo = RepoState::new_opening(
|
|
repo_id,
|
|
gitcomet_core::domain::RepoSpec {
|
|
workdir: workdir.clone(),
|
|
},
|
|
);
|
|
repo.head_branch = Loadable::Ready(branch_name.clone());
|
|
|
|
let state = Arc::new(AppState {
|
|
repos: vec![repo],
|
|
active_repo: Some(repo_id),
|
|
..Default::default()
|
|
});
|
|
this.state = Arc::clone(&state);
|
|
this._ui_model
|
|
.update(cx, |model, cx| model.set_state(state, cx));
|
|
cx.notify();
|
|
});
|
|
});
|
|
|
|
cx.update(|_window, app| {
|
|
let model = view
|
|
.update(app, |this, cx| {
|
|
this.popover_host.update(cx, |host, cx| {
|
|
host.context_menu_model(
|
|
&PopoverKind::BranchMenu {
|
|
repo_id,
|
|
section: BranchSection::Local,
|
|
name: branch_name.clone(),
|
|
},
|
|
cx,
|
|
)
|
|
})
|
|
})
|
|
.expect("expected branch context menu model");
|
|
|
|
let has_merge = model.items.iter().any(|item| match item {
|
|
ContextMenuItem::Entry { label, .. } => label.as_ref() == "Merge into current",
|
|
_ => false,
|
|
});
|
|
let has_pull = model.items.iter().any(|item| match item {
|
|
ContextMenuItem::Entry { label, .. } => label.as_ref() == "Pull into current",
|
|
_ => false,
|
|
});
|
|
let has_squash = model.items.iter().any(|item| match item {
|
|
ContextMenuItem::Entry { label, .. } => label.as_ref() == "Squash into current",
|
|
_ => false,
|
|
});
|
|
|
|
let delete_disabled = model.items.iter().any(|item| match item {
|
|
ContextMenuItem::Entry {
|
|
label,
|
|
action,
|
|
disabled,
|
|
..
|
|
} if label.as_ref() == "Delete branch" => {
|
|
*disabled
|
|
&& matches!(
|
|
action.as_ref(),
|
|
ContextMenuAction::DeleteBranch { repo_id: rid, name }
|
|
if *rid == repo_id && name == &branch_name
|
|
)
|
|
}
|
|
_ => false,
|
|
});
|
|
|
|
assert!(!has_pull, "expected pull entry to be excluded");
|
|
assert!(!has_merge, "expected merge entry to be excluded");
|
|
assert!(!has_squash, "expected squash entry to be excluded");
|
|
assert!(delete_disabled, "expected delete entry to be disabled");
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn status_file_menu_uses_multi_selection_for_stage(cx: &mut gpui::TestAppContext) {
|
|
let (store, events) = AppStore::new(Arc::new(TestBackend));
|
|
let (view, cx) =
|
|
cx.add_window_view(|window, cx| GitCometView::new(store, events, None, window, cx));
|
|
|
|
let repo_id = RepoId(3);
|
|
let workdir = std::env::temp_dir().join(format!(
|
|
"gitcomet_ui_test_{}_status_menu",
|
|
std::process::id()
|
|
));
|
|
|
|
let a = std::path::PathBuf::from("a.txt");
|
|
let b = std::path::PathBuf::from("b.txt");
|
|
|
|
cx.update(|_window, app| {
|
|
view.update(app, |this, cx| {
|
|
let mut repo = RepoState::new_opening(
|
|
repo_id,
|
|
gitcomet_core::domain::RepoSpec {
|
|
workdir: workdir.clone(),
|
|
},
|
|
);
|
|
repo.status = Loadable::Ready(
|
|
gitcomet_core::domain::RepoStatus {
|
|
staged: vec![],
|
|
unstaged: vec![
|
|
gitcomet_core::domain::FileStatus {
|
|
path: a.clone(),
|
|
kind: gitcomet_core::domain::FileStatusKind::Modified,
|
|
conflict: None,
|
|
},
|
|
gitcomet_core::domain::FileStatus {
|
|
path: b.clone(),
|
|
kind: gitcomet_core::domain::FileStatusKind::Modified,
|
|
conflict: None,
|
|
},
|
|
],
|
|
}
|
|
.into(),
|
|
);
|
|
|
|
this.state = Arc::new(AppState {
|
|
repos: vec![repo],
|
|
active_repo: Some(repo_id),
|
|
..Default::default()
|
|
});
|
|
this.details_pane.update(cx, |pane, cx| {
|
|
pane.status_multi_selection.insert(
|
|
repo_id,
|
|
StatusMultiSelection {
|
|
unstaged: vec![a.clone(), b.clone()],
|
|
unstaged_anchor: Some(a.clone()),
|
|
staged: vec![],
|
|
staged_anchor: None,
|
|
},
|
|
);
|
|
cx.notify();
|
|
});
|
|
cx.notify();
|
|
});
|
|
});
|
|
|
|
cx.update(|_window, app| {
|
|
let model = view
|
|
.update(app, |this, cx| {
|
|
this.popover_host.update(cx, |host, cx| {
|
|
host.context_menu_model(
|
|
&PopoverKind::StatusFileMenu {
|
|
repo_id,
|
|
area: DiffArea::Unstaged,
|
|
path: a.clone(),
|
|
},
|
|
cx,
|
|
)
|
|
})
|
|
})
|
|
.expect("expected status file context menu model");
|
|
|
|
let stage_action = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry { label, action, .. } if label.as_ref() == "Stage (2)" => {
|
|
Some((**action).clone())
|
|
}
|
|
_ => None,
|
|
});
|
|
|
|
match stage_action {
|
|
Some(ContextMenuAction::StageSelectionOrPath {
|
|
repo_id: rid,
|
|
area,
|
|
path,
|
|
}) => {
|
|
assert_eq!(rid, repo_id);
|
|
assert_eq!(area, DiffArea::Unstaged);
|
|
assert_eq!(path, a);
|
|
}
|
|
_ => panic!("expected Stage (2) to stage selected paths"),
|
|
}
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn status_file_menu_uses_multi_selection_for_unstage(cx: &mut gpui::TestAppContext) {
|
|
let (store, events) = AppStore::new(Arc::new(TestBackend));
|
|
let (view, cx) =
|
|
cx.add_window_view(|window, cx| GitCometView::new(store, events, None, window, cx));
|
|
|
|
let repo_id = RepoId(4);
|
|
let workdir = std::env::temp_dir().join(format!(
|
|
"gitcomet_ui_test_{}_status_menu_staged",
|
|
std::process::id()
|
|
));
|
|
|
|
let a = std::path::PathBuf::from("a.txt");
|
|
let b = std::path::PathBuf::from("b.txt");
|
|
|
|
cx.update(|_window, app| {
|
|
view.update(app, |this, cx| {
|
|
let mut repo = RepoState::new_opening(
|
|
repo_id,
|
|
gitcomet_core::domain::RepoSpec {
|
|
workdir: workdir.clone(),
|
|
},
|
|
);
|
|
repo.status = Loadable::Ready(
|
|
gitcomet_core::domain::RepoStatus {
|
|
staged: vec![
|
|
gitcomet_core::domain::FileStatus {
|
|
path: a.clone(),
|
|
kind: gitcomet_core::domain::FileStatusKind::Modified,
|
|
conflict: None,
|
|
},
|
|
gitcomet_core::domain::FileStatus {
|
|
path: b.clone(),
|
|
kind: gitcomet_core::domain::FileStatusKind::Modified,
|
|
conflict: None,
|
|
},
|
|
],
|
|
unstaged: vec![],
|
|
}
|
|
.into(),
|
|
);
|
|
|
|
this.state = Arc::new(AppState {
|
|
repos: vec![repo],
|
|
active_repo: Some(repo_id),
|
|
..Default::default()
|
|
});
|
|
this.details_pane.update(cx, |pane, cx| {
|
|
pane.status_multi_selection.insert(
|
|
repo_id,
|
|
StatusMultiSelection {
|
|
unstaged: vec![],
|
|
unstaged_anchor: None,
|
|
staged: vec![a.clone(), b.clone()],
|
|
staged_anchor: Some(a.clone()),
|
|
},
|
|
);
|
|
cx.notify();
|
|
});
|
|
cx.notify();
|
|
});
|
|
});
|
|
|
|
cx.update(|_window, app| {
|
|
let model = view
|
|
.update(app, |this, cx| {
|
|
this.popover_host.update(cx, |host, cx| {
|
|
host.context_menu_model(
|
|
&PopoverKind::StatusFileMenu {
|
|
repo_id,
|
|
area: DiffArea::Staged,
|
|
path: a.clone(),
|
|
},
|
|
cx,
|
|
)
|
|
})
|
|
})
|
|
.expect("expected status file context menu model");
|
|
|
|
let unstage_action = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry { label, action, .. } if label.as_ref() == "Unstage (2)" => {
|
|
Some((**action).clone())
|
|
}
|
|
_ => None,
|
|
});
|
|
|
|
match unstage_action {
|
|
Some(ContextMenuAction::UnstageSelectionOrPath {
|
|
repo_id: rid,
|
|
area,
|
|
path,
|
|
}) => {
|
|
assert_eq!(rid, repo_id);
|
|
assert_eq!(area, DiffArea::Staged);
|
|
assert_eq!(path, a);
|
|
}
|
|
_ => panic!("expected Unstage (2) to unstage selected paths"),
|
|
}
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn status_file_menu_offers_resolve_actions_for_conflicts(cx: &mut gpui::TestAppContext) {
|
|
let (store, events) = AppStore::new(Arc::new(TestBackend));
|
|
let (view, cx) =
|
|
cx.add_window_view(|window, cx| GitCometView::new(store, events, None, window, cx));
|
|
|
|
let repo_id = RepoId(5);
|
|
let workdir = std::env::temp_dir().join(format!(
|
|
"gitcomet_ui_test_{}_status_menu_conflict",
|
|
std::process::id()
|
|
));
|
|
let path = std::path::PathBuf::from("conflict.txt");
|
|
|
|
cx.update(|_window, app| {
|
|
view.update(app, |this, cx| {
|
|
let mut repo = RepoState::new_opening(
|
|
repo_id,
|
|
gitcomet_core::domain::RepoSpec {
|
|
workdir: workdir.clone(),
|
|
},
|
|
);
|
|
repo.status = Loadable::Ready(
|
|
gitcomet_core::domain::RepoStatus {
|
|
staged: vec![],
|
|
unstaged: vec![gitcomet_core::domain::FileStatus {
|
|
path: path.clone(),
|
|
kind: gitcomet_core::domain::FileStatusKind::Conflicted,
|
|
conflict: None,
|
|
}],
|
|
}
|
|
.into(),
|
|
);
|
|
let state = Arc::new(AppState {
|
|
repos: vec![repo],
|
|
active_repo: Some(repo_id),
|
|
..Default::default()
|
|
});
|
|
this.state = Arc::clone(&state);
|
|
this._ui_model
|
|
.update(cx, |model, cx| model.set_state(state, cx));
|
|
cx.notify();
|
|
});
|
|
});
|
|
|
|
cx.update(|_window, app| {
|
|
let model = view
|
|
.update(app, |this, cx| {
|
|
this.popover_host.update(cx, |host, cx| {
|
|
host.context_menu_model(
|
|
&PopoverKind::StatusFileMenu {
|
|
repo_id,
|
|
area: DiffArea::Unstaged,
|
|
path: path.clone(),
|
|
},
|
|
cx,
|
|
)
|
|
})
|
|
})
|
|
.expect("expected status file context menu model");
|
|
|
|
let has_ours = model.items.iter().any(|item| match item {
|
|
ContextMenuItem::Entry { label, action, .. }
|
|
if label.as_ref() == "Resolve using ours" =>
|
|
{
|
|
matches!(
|
|
action.as_ref(),
|
|
ContextMenuAction::CheckoutConflictSideSelectionOrPath {
|
|
repo_id: rid,
|
|
area: DiffArea::Unstaged,
|
|
path: p,
|
|
side: gitcomet_core::services::ConflictSide::Ours
|
|
} if *rid == repo_id && p.as_path() == path.as_path()
|
|
)
|
|
}
|
|
_ => false,
|
|
});
|
|
let has_theirs = model.items.iter().any(|item| match item {
|
|
ContextMenuItem::Entry { label, action, .. }
|
|
if label.as_ref() == "Resolve using theirs" =>
|
|
{
|
|
matches!(
|
|
action.as_ref(),
|
|
ContextMenuAction::CheckoutConflictSideSelectionOrPath {
|
|
repo_id: rid,
|
|
area: DiffArea::Unstaged,
|
|
path: p,
|
|
side: gitcomet_core::services::ConflictSide::Theirs
|
|
} if *rid == repo_id && p.as_path() == path.as_path()
|
|
)
|
|
}
|
|
_ => false,
|
|
});
|
|
let has_manual = model.items.iter().any(|item| match item {
|
|
ContextMenuItem::Entry { label, action, .. }
|
|
if label.as_ref() == "Resolve manually…" =>
|
|
{
|
|
matches!(
|
|
action.as_ref(),
|
|
ContextMenuAction::SelectDiff {
|
|
repo_id: rid,
|
|
target: DiffTarget::WorkingTree { path: p, area: DiffArea::Unstaged }
|
|
} if *rid == repo_id && p.as_path() == path.as_path()
|
|
)
|
|
}
|
|
_ => false,
|
|
});
|
|
let has_external_mergetool = model.items.iter().any(|item| match item {
|
|
ContextMenuItem::Entry { label, action, .. }
|
|
if label.as_ref() == "Open external mergetool" =>
|
|
{
|
|
matches!(
|
|
action.as_ref(),
|
|
ContextMenuAction::LaunchMergetool {
|
|
repo_id: rid,
|
|
path: p
|
|
} if *rid == repo_id && p.as_path() == path.as_path()
|
|
)
|
|
}
|
|
_ => false,
|
|
});
|
|
|
|
assert!(has_ours);
|
|
assert!(has_theirs);
|
|
assert!(has_manual);
|
|
assert!(has_external_mergetool);
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn status_file_menu_hides_external_mergetool_for_staged_conflicts(cx: &mut gpui::TestAppContext) {
|
|
let (store, events) = AppStore::new(Arc::new(TestBackend));
|
|
let (view, cx) =
|
|
cx.add_window_view(|window, cx| GitCometView::new(store, events, None, window, cx));
|
|
|
|
let repo_id = RepoId(7);
|
|
let workdir = std::env::temp_dir().join(format!(
|
|
"gitcomet_ui_test_{}_status_menu_staged_conflict",
|
|
std::process::id()
|
|
));
|
|
let path = std::path::PathBuf::from("conflict.txt");
|
|
|
|
cx.update(|_window, app| {
|
|
view.update(app, |this, cx| {
|
|
let mut repo = RepoState::new_opening(
|
|
repo_id,
|
|
gitcomet_core::domain::RepoSpec {
|
|
workdir: workdir.clone(),
|
|
},
|
|
);
|
|
repo.status = Loadable::Ready(
|
|
gitcomet_core::domain::RepoStatus {
|
|
staged: vec![gitcomet_core::domain::FileStatus {
|
|
path: path.clone(),
|
|
kind: gitcomet_core::domain::FileStatusKind::Conflicted,
|
|
conflict: None,
|
|
}],
|
|
unstaged: vec![],
|
|
}
|
|
.into(),
|
|
);
|
|
let state = Arc::new(AppState {
|
|
repos: vec![repo],
|
|
active_repo: Some(repo_id),
|
|
..Default::default()
|
|
});
|
|
this.state = Arc::clone(&state);
|
|
this._ui_model
|
|
.update(cx, |model, cx| model.set_state(state, cx));
|
|
cx.notify();
|
|
});
|
|
});
|
|
|
|
cx.update(|_window, app| {
|
|
let model = view
|
|
.update(app, |this, cx| {
|
|
this.popover_host.update(cx, |host, cx| {
|
|
host.context_menu_model(
|
|
&PopoverKind::StatusFileMenu {
|
|
repo_id,
|
|
area: DiffArea::Staged,
|
|
path: path.clone(),
|
|
},
|
|
cx,
|
|
)
|
|
})
|
|
})
|
|
.expect("expected status file context menu model");
|
|
|
|
let has_external_mergetool = model.items.iter().any(|item| match item {
|
|
ContextMenuItem::Entry { label, .. } => {
|
|
label.as_ref().starts_with("Open external mergetool")
|
|
}
|
|
_ => false,
|
|
});
|
|
let has_discard_changes = model.items.iter().any(|item| match item {
|
|
ContextMenuItem::Entry { label, .. } => label.as_ref() == "Discard changes",
|
|
_ => false,
|
|
});
|
|
assert!(!has_external_mergetool);
|
|
assert!(!has_discard_changes);
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn status_file_menu_open_from_details_pane_does_not_double_lease_panic(
|
|
cx: &mut gpui::TestAppContext,
|
|
) {
|
|
let (store, events) = AppStore::new(Arc::new(TestBackend));
|
|
let (view, cx) =
|
|
cx.add_window_view(|window, cx| GitCometView::new(store, events, None, window, cx));
|
|
|
|
let repo_id = RepoId(6);
|
|
let workdir = std::env::temp_dir().join(format!(
|
|
"gitcomet_ui_test_{}_status_menu_reentrant",
|
|
std::process::id()
|
|
));
|
|
let path = std::path::PathBuf::from("conflict.txt");
|
|
|
|
cx.update(|_window, app| {
|
|
view.update(app, |this, cx| {
|
|
let mut repo = RepoState::new_opening(
|
|
repo_id,
|
|
gitcomet_core::domain::RepoSpec {
|
|
workdir: workdir.clone(),
|
|
},
|
|
);
|
|
repo.status = Loadable::Ready(
|
|
gitcomet_core::domain::RepoStatus {
|
|
staged: vec![],
|
|
unstaged: vec![gitcomet_core::domain::FileStatus {
|
|
path: path.clone(),
|
|
kind: gitcomet_core::domain::FileStatusKind::Conflicted,
|
|
conflict: None,
|
|
}],
|
|
}
|
|
.into(),
|
|
);
|
|
this.state = Arc::new(AppState {
|
|
repos: vec![repo],
|
|
active_repo: Some(repo_id),
|
|
..Default::default()
|
|
});
|
|
cx.notify();
|
|
});
|
|
});
|
|
|
|
cx.update(|window, app| {
|
|
let details_pane = view.read(app).details_pane.clone();
|
|
let anchor = point(px(0.0), px(0.0));
|
|
details_pane.update(app, |pane, cx| {
|
|
pane.open_popover_at(
|
|
PopoverKind::StatusFileMenu {
|
|
repo_id,
|
|
area: DiffArea::Unstaged,
|
|
path: path.clone(),
|
|
},
|
|
anchor,
|
|
window,
|
|
cx,
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn stash_menu_has_apply_pop_and_drop_entries(cx: &mut gpui::TestAppContext) {
|
|
let (store, events) = AppStore::new(Arc::new(TestBackend));
|
|
let (view, cx) =
|
|
cx.add_window_view(|window, cx| GitCometView::new(store, events, None, window, cx));
|
|
|
|
let repo_id = RepoId(8);
|
|
let index = 3usize;
|
|
let message = "WIP".to_string();
|
|
|
|
cx.update(|_window, app| {
|
|
let model = view
|
|
.update(app, |this, cx| {
|
|
this.popover_host.update(cx, |host, cx| {
|
|
host.context_menu_model(
|
|
&PopoverKind::StashMenu {
|
|
repo_id,
|
|
index,
|
|
message: message.clone(),
|
|
},
|
|
cx,
|
|
)
|
|
})
|
|
})
|
|
.expect("expected stash context menu model");
|
|
|
|
let apply_action = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry { label, action, .. } if label.as_ref() == "Apply stash" => {
|
|
Some((**action).clone())
|
|
}
|
|
_ => None,
|
|
});
|
|
assert!(matches!(
|
|
apply_action,
|
|
Some(ContextMenuAction::ApplyStash {
|
|
repo_id: rid,
|
|
index: ix
|
|
}) if rid == repo_id && ix == index
|
|
));
|
|
|
|
let pop_action = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry { label, action, .. } if label.as_ref() == "Pop stash" => {
|
|
Some((**action).clone())
|
|
}
|
|
_ => None,
|
|
});
|
|
assert!(matches!(
|
|
pop_action,
|
|
Some(ContextMenuAction::PopStash {
|
|
repo_id: rid,
|
|
index: ix
|
|
}) if rid == repo_id && ix == index
|
|
));
|
|
|
|
let drop_action = model.items.iter().find_map(|item| match item {
|
|
ContextMenuItem::Entry { label, action, .. } if label.as_ref() == "Drop stash…" => {
|
|
Some((**action).clone())
|
|
}
|
|
_ => None,
|
|
});
|
|
assert!(matches!(
|
|
drop_action,
|
|
Some(ContextMenuAction::DropStashConfirm {
|
|
repo_id: rid,
|
|
index: ix,
|
|
message: ref msg
|
|
}) if rid == repo_id && ix == index && msg == &message
|
|
));
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
fn stash_menu_drop_action_opens_drop_confirm_popover(cx: &mut gpui::TestAppContext) {
|
|
let (store, events) = AppStore::new(Arc::new(TestBackend));
|
|
let (view, cx) =
|
|
cx.add_window_view(|window, cx| GitCometView::new(store, events, None, window, cx));
|
|
|
|
let repo_id = RepoId(9);
|
|
let index = 1usize;
|
|
let message = "Drop me".to_string();
|
|
|
|
cx.update(|window, app| {
|
|
view.update(app, |this, cx| {
|
|
this.popover_host.update(cx, |host, cx| {
|
|
host.popover_anchor = Some(PopoverAnchor::Point(point(px(32.0), px(48.0))));
|
|
host.context_menu_activate_action(
|
|
ContextMenuAction::DropStashConfirm {
|
|
repo_id,
|
|
index,
|
|
message: message.clone(),
|
|
},
|
|
window,
|
|
cx,
|
|
);
|
|
|
|
match host.popover.as_ref() {
|
|
Some(PopoverKind::StashDropConfirm {
|
|
repo_id: rid,
|
|
index: ix,
|
|
message: msg,
|
|
}) => {
|
|
assert_eq!(*rid, repo_id);
|
|
assert_eq!(*ix, index);
|
|
assert_eq!(msg, &message);
|
|
}
|
|
_ => panic!("expected stash drop confirm popover to open"),
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|