1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
use crate::Result;
use gix_error::{ErrorExt, ResultExt};
use gix_ref::{
Category, FullName,
transaction::{PreviousValue, RefEdit},
};
/// Delete local branches.
pub mod delete {
use std::path::PathBuf;
use gix_ref::FullName;
/// A branch-deletion rejection because the branch is checked out in a worktree.
#[derive(Debug)]
pub struct CheckedOutError {
/// The local branch whose checkout prevents deletion.
pub name: FullName,
/// The worktree directories in which the branch is checked out.
pub worktree_dirs: Vec<PathBuf>,
}
impl std::fmt::Display for CheckedOutError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"The local branch {:?} is checked out in {:?}",
self.name, self.worktree_dirs
)
}
}
impl std::error::Error for CheckedOutError {}
/// A configuration-cleanup failure after all requested references were made absent.
#[derive(Debug)]
pub struct CleanupError {
/// Every requested reference name, including names which were already missing before the call.
///
/// All of these references and their reflogs are guaranteed to be absent. Their `branch.<name>` configuration
/// sections may remain.
pub references: Vec<FullName>,
/// The branches actually deleted, as would have been returned on success.
///
/// Names are sorted and deduplicated; branches missing when locked for deletion are excluded.
pub deleted: Vec<FullName>,
}
impl std::fmt::Display for CleanupError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"References {:?} are absent, but local branch configuration cleanup failed",
self.references
)
}
}
impl std::error::Error for CleanupError {}
}
impl crate::Repository {
/// Delete all local branches in `names` and remove their `branch.<name>` sections from the local configuration.
///
/// All names must be local branch references such as `refs/heads/topic`. The operation fails before making changes if
/// any name belongs to another reference category or is checked out in any worktree. Missing branches are accepted so any
/// associated local configuration is still removed. **It deliberately performs no merged-state check**.
///
/// A checked-out branch rejection contains [`delete::CheckedOutError`] in its error chain, identifying the branch and
/// the worktree directories that prevented deletion.
///
/// On success, every requested reference and its reflog is absent, and every matching `branch.<name>` section has been
/// removed from the local configuration. Return the sorted, deduplicated names of branches that existed when locked for
/// deletion. Missing branches are omitted from the returned vector, but their configuration is still removed.
///
/// Reference deletion and configuration cleanup cannot be one atomic transaction. Once reference deletion succeeds, a
/// configuration write or commit failure contains [`delete::CleanupError`] in its error chain. Its `references` field
/// contains every requested name—including names which were missing initially—and guarantees only that their references and
/// reflogs are absent. Its `deleted` field contains the branches actually deleted, just as in the success case.
/// The remaining error chain identifies the failed cleanup phase.
pub fn delete_local_branches(&mut self, names: impl IntoIterator<Item = FullName>) -> Result<Vec<FullName>> {
let mut names: Vec<_> = names.into_iter().collect();
names.sort();
names.dedup();
if names.is_empty() {
return Ok(names);
}
for name in &names {
if name.category_and_short_name().map(|(category, _)| category) != Some(Category::LocalBranch) {
return Err(gix_error::message!("{name:?} is not a local branch").raise().into());
}
}
let checked_out = self.checked_out_branches()?;
for name in &names {
if let Some(worktree_dirs) = checked_out.get(name) {
return Err(delete::CheckedOutError {
name: name.clone(),
worktree_dirs: worktree_dirs.clone(),
}
.raise()
.into());
}
}
let edits: Vec<_> = names
.iter()
.map(|name| RefEdit::delete(name.clone(), PreviousValue::Any))
.collect();
let config_path = self.common_dir().join("config");
let mut config_lock =
gix_lock::File::acquire_to_update_resource(&config_path, gix_lock::acquire::Fail::Immediately, None)
.or_raise(|| gix_error::message("Could not acquire the local configuration lock"))?;
let mut config = match gix_config::File::from_path_no_includes(config_path.clone(), gix_config::Source::Local) {
Ok(config) => Some(config),
Err(err) if err.is_not_found() => None,
Err(err) => {
return Err(err
.raise(gix_error::message("Could not read the local configuration"))
.into());
}
};
let removed_config = config
.as_mut()
.is_some_and(|config| remove_branch_config(config, &names, |_| true));
let deleted: Vec<_> = self
.edit_references(edits)
.or_raise(|| gix_error::message("Could not delete local branches"))?
.into_iter()
.filter_map(|edit| edit.change.previous_value().is_some().then_some(edit.name))
.collect();
if removed_config {
let config = config.expect("configuration was present when sections were removed");
config
.write_to(&mut config_lock)
.or_raise(|| gix_error::message("Could not write the updated local configuration"))
.or_raise(|| delete::CleanupError {
references: names.clone(),
deleted: deleted.clone(),
})?;
config_lock
.commit()
.map_err(|err| err.error)
.or_raise(|| gix_error::message("Could not commit the updated local configuration"))
.or_raise(|| delete::CleanupError {
references: names.clone(),
deleted: deleted.clone(),
})?;
remove_branch_config(
gix_features::threading::OwnShared::make_mut(&mut self.config.resolved),
&names,
|meta| {
meta.source == gix_config::Source::Local
&& meta.level == 0
&& meta.path.as_deref() == Some(config_path.as_path())
},
);
}
Ok(deleted)
}
}
fn remove_branch_config(
config: &mut gix_config::File,
names: &[FullName],
mut filter: impl FnMut(&gix_config::file::Metadata) -> bool,
) -> bool {
let section_ids: Vec<_> = config
.sections_and_ids_by_name("branch")
.into_iter()
.flatten()
.filter_map(|(section, id)| {
if !filter(section.meta()) {
return None;
}
let subsection = section.header().subsection_name()?;
names
.iter()
.any(|name| {
name.category_and_short_name()
.is_some_and(|(_, short)| short == subsection)
})
.then_some(id)
})
.collect();
let removed = !section_ids.is_empty();
for id in section_ids {
config.remove_section_by_id(id);
}
removed
}