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,
repo_path: RepoPath,
status: GitStatusPair,
toggle_state: ToggleState,
is_staged: Option<bool>,
}
pub struct GitPanel {
@ -91,18 +91,11 @@ pub struct GitPanel {
/// 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.
visible_entries: Vec<GitListEntry>,
all_staged: Option<bool>,
width: Option<Pixels>,
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 {
pub fn load(
workspace: WeakView<Workspace>,
@ -314,6 +307,7 @@ impl GitPanel {
fs,
pending_serialization: Task::ready(None),
visible_entries: Vec::new(),
all_staged: None,
current_modifiers: cx.modifiers(),
width: Some(px(360.)),
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>) {
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>) {
// 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, _| {
state.unstage_all();
});
@ -639,11 +649,6 @@ impl GitPanel {
println!("Commit all changes triggered");
}
fn all_staged(&self) -> bool {
// TODO: Implement all_staged
true
}
fn no_entries(&self) -> bool {
self.visible_entries.is_empty()
}
@ -678,7 +683,7 @@ impl GitPanel {
status,
depth: 0,
display_name: filename,
toggle_state: entry.toggle_state,
is_staged: entry.is_staged,
};
callback(ix, details, cx);
@ -705,10 +710,19 @@ impl GitPanel {
let path_set = HashSet::from_iter(repo.status().map(|entry| entry.repo_path));
// 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) =
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 {
// Show partial path for deeply nested files
@ -734,11 +748,12 @@ impl GitPanel {
display_name,
repo_path: entry.repo_path,
status: entry.status,
toggle_state,
is_staged,
};
self.visible_entries.push(entry);
}
self.all_staged = all_staged;
// Sort entries by path to maintain consistent order
self.visible_entries
@ -805,7 +820,11 @@ impl GitPanel {
.child(
h_flex()
.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().flex_grow())
@ -814,27 +833,50 @@ impl GitPanel {
.gap_2()
.child(
IconButton::new("discard-changes", IconName::Undo)
.tooltip(move |cx| {
.tooltip({
let focus_handle = focus_handle.clone();
Tooltip::for_action_in(
"Discard all changes",
&RevertAll,
&focus_handle,
cx,
)
move |cx| {
Tooltip::for_action_in(
"Discard all changes",
&RevertAll,
&focus_handle,
cx,
)
}
})
.icon_size(IconSize::Small)
.disabled(true),
)
.child(if self.all_staged() {
self.panel_button("unstage-all", "Unstage All").on_click(
cx.listener(move |_, _, cx| cx.dispatch_action(Box::new(RevertAll))),
)
.child(if self.all_staged.unwrap_or(false) {
self.panel_button("unstage-all", "Unstage All")
.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 {
self.panel_button("stage-all", "Stage All").on_click(
cx.listener(move |_, _, cx| cx.dispatch_action(Box::new(StageAll))),
)
self.panel_button("stage-all", "Stage All")
.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
.child(
Checkbox::new(checkbox_id, entry_details.toggle_state)
.fill()
.elevation(ElevationIndex::Surface)
.on_click({
let handle = handle.clone();
let repo_path = repo_path.clone();
move |toggle, cx| {
let Some(this) = handle.upgrade() else {
return;
};
this.update(cx, |this, _| {
this.visible_entries[ix].toggle_state = *toggle;
});
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),
Checkbox::new(
checkbox_id,
entry_details
.is_staged
.map_or(ToggleState::Indeterminate, ToggleState::from),
)
.fill()
.elevation(ElevationIndex::Surface)
.on_click({
let handle = handle.clone();
let repo_path = repo_path.clone();
move |toggle, cx| {
let Some(this) = handle.upgrade() else {
return;
};
this.update(cx, |this, _| {
this.visible_entries[ix].is_staged = match *toggle {
ToggleState::Selected => Some(true),
ToggleState::Unselected => Some(false),
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(