git: Fully implement "all staged" checkbox (#23079)

Also includes some improvements to the "stage/unstage all" actions and
buttons.

Release Notes:

- N/A
This commit is contained in:
Cole Miller 2025-01-13 15:13:14 -05:00 committed by GitHub
parent 2179be1855
commit bd3c7d6cbf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -69,7 +69,7 @@ pub struct GitListEntry {
display_name: String, display_name: String,
repo_path: RepoPath, repo_path: RepoPath,
status: GitStatusPair, status: GitStatusPair,
toggle_state: ToggleState, is_staged: Option<bool>,
} }
pub struct GitPanel { pub struct GitPanel {
@ -91,18 +91,11 @@ pub struct GitPanel {
/// At this point it doesn't matter what repository the entry belongs to, /// At this point it doesn't matter what repository the entry belongs to,
/// as only one repositories' entries are visible in the list at a time. /// as only one repositories' entries are visible in the list at a time.
visible_entries: Vec<GitListEntry>, visible_entries: Vec<GitListEntry>,
all_staged: Option<bool>,
width: Option<Pixels>, width: Option<Pixels>,
reveal_in_editor: Task<()>, reveal_in_editor: Task<()>,
} }
fn status_to_toggle_state(status: &GitStatusPair) -> ToggleState {
match status.is_staged() {
Some(true) => ToggleState::Selected,
Some(false) => ToggleState::Unselected,
None => ToggleState::Indeterminate,
}
}
impl GitPanel { impl GitPanel {
pub fn load( pub fn load(
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
@ -314,6 +307,7 @@ impl GitPanel {
fs, fs,
pending_serialization: Task::ready(None), pending_serialization: Task::ready(None),
visible_entries: Vec::new(), visible_entries: Vec::new(),
all_staged: None,
current_modifiers: cx.modifiers(), current_modifiers: cx.modifiers(),
width: Some(px(360.)), width: Some(px(360.)),
scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()), scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()),
@ -602,10 +596,26 @@ impl GitPanel {
} }
fn stage_all(&mut self, _: &StageAll, cx: &mut ViewContext<Self>) { fn stage_all(&mut self, _: &StageAll, cx: &mut ViewContext<Self>) {
self.git_state.update(cx, |state, _| state.stage_all()); let to_stage = self
.visible_entries
.iter_mut()
.filter_map(|entry| {
let is_unstaged = !entry.is_staged.unwrap_or(false);
entry.is_staged = Some(true);
is_unstaged.then(|| entry.repo_path.clone())
})
.collect();
self.all_staged = Some(true);
self.git_state
.update(cx, |state, _| state.stage_entries(to_stage));
} }
fn unstage_all(&mut self, _: &UnstageAll, cx: &mut ViewContext<Self>) { fn unstage_all(&mut self, _: &UnstageAll, cx: &mut ViewContext<Self>) {
// This should only be called when all entries are staged.
for entry in &mut self.visible_entries {
entry.is_staged = Some(false);
}
self.all_staged = Some(false);
self.git_state.update(cx, |state, _| { self.git_state.update(cx, |state, _| {
state.unstage_all(); state.unstage_all();
}); });
@ -639,11 +649,6 @@ impl GitPanel {
println!("Commit all changes triggered"); println!("Commit all changes triggered");
} }
fn all_staged(&self) -> bool {
// TODO: Implement all_staged
true
}
fn no_entries(&self) -> bool { fn no_entries(&self) -> bool {
self.visible_entries.is_empty() self.visible_entries.is_empty()
} }
@ -678,7 +683,7 @@ impl GitPanel {
status, status,
depth: 0, depth: 0,
display_name: filename, display_name: filename,
toggle_state: entry.toggle_state, is_staged: entry.is_staged,
}; };
callback(ix, details, cx); callback(ix, details, cx);
@ -705,10 +710,19 @@ impl GitPanel {
let path_set = HashSet::from_iter(repo.status().map(|entry| entry.repo_path)); let path_set = HashSet::from_iter(repo.status().map(|entry| entry.repo_path));
// Second pass - create entries with proper depth calculation // Second pass - create entries with proper depth calculation
for entry in repo.status() { let mut all_staged = None;
for (ix, entry) in repo.status().enumerate() {
let (depth, difference) = let (depth, difference) =
Self::calculate_depth_and_difference(&entry.repo_path, &path_set); Self::calculate_depth_and_difference(&entry.repo_path, &path_set);
let toggle_state = status_to_toggle_state(&entry.status); let is_staged = entry.status.is_staged();
all_staged = if ix == 0 {
is_staged
} else {
match (all_staged, is_staged) {
(None, _) | (_, None) => None,
(Some(a), Some(b)) => (a == b).then_some(a),
}
};
let display_name = if difference > 1 { let display_name = if difference > 1 {
// Show partial path for deeply nested files // Show partial path for deeply nested files
@ -734,11 +748,12 @@ impl GitPanel {
display_name, display_name,
repo_path: entry.repo_path, repo_path: entry.repo_path,
status: entry.status, status: entry.status,
toggle_state, is_staged,
}; };
self.visible_entries.push(entry); self.visible_entries.push(entry);
} }
self.all_staged = all_staged;
// Sort entries by path to maintain consistent order // Sort entries by path to maintain consistent order
self.visible_entries self.visible_entries
@ -805,7 +820,11 @@ impl GitPanel {
.child( .child(
h_flex() h_flex()
.gap_2() .gap_2()
.child(Checkbox::new("all-changes", true.into()).disabled(true)) .child(Checkbox::new(
"all-changes",
self.all_staged
.map_or(ToggleState::Indeterminate, ToggleState::from),
))
.child(div().text_buffer(cx).text_ui_sm(cx).child(changes_string)), .child(div().text_buffer(cx).text_ui_sm(cx).child(changes_string)),
) )
.child(div().flex_grow()) .child(div().flex_grow())
@ -814,27 +833,50 @@ impl GitPanel {
.gap_2() .gap_2()
.child( .child(
IconButton::new("discard-changes", IconName::Undo) IconButton::new("discard-changes", IconName::Undo)
.tooltip(move |cx| { .tooltip({
let focus_handle = focus_handle.clone(); let focus_handle = focus_handle.clone();
move |cx| {
Tooltip::for_action_in( Tooltip::for_action_in(
"Discard all changes", "Discard all changes",
&RevertAll, &RevertAll,
&focus_handle, &focus_handle,
cx, cx,
) )
}
}) })
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.disabled(true), .disabled(true),
) )
.child(if self.all_staged() { .child(if self.all_staged.unwrap_or(false) {
self.panel_button("unstage-all", "Unstage All").on_click( self.panel_button("unstage-all", "Unstage All")
cx.listener(move |_, _, cx| cx.dispatch_action(Box::new(RevertAll))), .tooltip({
) let focus_handle = focus_handle.clone();
move |cx| {
Tooltip::for_action_in(
"Unstage all changes",
&UnstageAll,
&focus_handle,
cx,
)
}
})
.on_click(
cx.listener(move |this, _, cx| this.unstage_all(&UnstageAll, cx)),
)
} else { } else {
self.panel_button("stage-all", "Stage All").on_click( self.panel_button("stage-all", "Stage All")
cx.listener(move |_, _, cx| cx.dispatch_action(Box::new(StageAll))), .tooltip({
) let focus_handle = focus_handle.clone();
move |cx| {
Tooltip::for_action_in(
"Stage all changes",
&StageAll,
&focus_handle,
cx,
)
}
})
.on_click(cx.listener(move |this, _, cx| this.stage_all(&StageAll, cx)))
}), }),
) )
} }
@ -1049,30 +1091,39 @@ impl GitPanel {
entry = entry entry = entry
.child( .child(
Checkbox::new(checkbox_id, entry_details.toggle_state) Checkbox::new(
.fill() checkbox_id,
.elevation(ElevationIndex::Surface) entry_details
.on_click({ .is_staged
let handle = handle.clone(); .map_or(ToggleState::Indeterminate, ToggleState::from),
let repo_path = repo_path.clone(); )
move |toggle, cx| { .fill()
let Some(this) = handle.upgrade() else { .elevation(ElevationIndex::Surface)
return; .on_click({
}; let handle = handle.clone();
this.update(cx, |this, _| { let repo_path = repo_path.clone();
this.visible_entries[ix].toggle_state = *toggle; move |toggle, cx| {
}); let Some(this) = handle.upgrade() else {
state.update(cx, { return;
let repo_path = repo_path.clone(); };
move |state, _| match toggle { this.update(cx, |this, _| {
ToggleState::Selected | ToggleState::Indeterminate => { this.visible_entries[ix].is_staged = match *toggle {
state.stage_entry(repo_path); ToggleState::Selected => Some(true),
} ToggleState::Unselected => Some(false),
ToggleState::Unselected => state.unstage_entry(repo_path), ToggleState::Indeterminate => None,
}
});
state.update(cx, {
let repo_path = repo_path.clone();
move |state, _| match toggle {
ToggleState::Selected | ToggleState::Indeterminate => {
state.stage_entry(repo_path);
} }
}); ToggleState::Unselected => state.unstage_entry(repo_path),
} }
}), });
}
}),
) )
.child(git_status_icon(status)) .child(git_status_icon(status))
.child( .child(