1use std::collections::HashMap;
4use std::path::PathBuf;
5
6use time::OffsetDateTime;
7
8use crate::config::ConfigSet;
9use crate::error::{Error, Result};
10use crate::objects::{read_commit_subject_pretty, ObjectId, ObjectKind};
11use crate::parallel::{try_par_map, ParallelHashError, ParallelOp};
12use crate::refs::listing::{
13 query_refs_peeling, PeelTips, RefKind, RefNamespace, RefQuery, RefQueryError, RefQueryOptions,
14 SortKey,
15};
16use crate::repo::Repository;
17use crate::rev_parse::abbreviate_ref_name;
18use crate::state::{resolve_head_for_repository, HeadState};
19use crate::worktree::list_worktrees;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum BranchScope {
24 #[default]
26 Local,
27 Remote,
29 All,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub enum BranchDetail {
36 #[default]
38 Names,
39 Full,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Default)]
45pub struct BranchListQuery {
46 pub scope: BranchScope,
48 pub refs: RefQuery,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct UpstreamStatus {
55 pub full_ref: String,
57 pub short: String,
59 pub remote: String,
61 pub merge: String,
63 pub gone: bool,
65 pub ahead: Option<usize>,
67 pub behind: Option<usize>,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct BranchSummary {
74 pub name: String,
76 pub full_ref: String,
78 pub is_remote: bool,
80 pub current: bool,
82 pub oid: ObjectId,
84 pub subject: Option<String>,
86 pub committer_time: Option<OffsetDateTime>,
88 pub upstream: Option<UpstreamStatus>,
90 pub worktree: Option<PathBuf>,
92}
93
94pub fn upstream_status(repo: &Repository, branch: &str) -> Result<Option<UpstreamStatus>> {
102 let tip = repo
103 .references()
104 .resolve_if_exists(&format!("refs/heads/{branch}"))?;
105 let config = repo.config()?;
106 build_upstream_status(repo, &config, branch, tip, BranchDetail::Full)
107}
108
109pub fn list_branches(
119 repo: &Repository,
120 query: &BranchListQuery,
121 detail: BranchDetail,
122) -> Result<Vec<BranchSummary>> {
123 if detail == BranchDetail::Names && matches!(query.refs.sort.key, SortKey::Date) {
124 return Err(Error::BranchListNamesDateSort);
125 }
126 let config = repo.config()?;
127 let listings = query_refs_peeling(
128 repo,
129 &ref_query_for_branch_list(query),
130 &ref_query_options(&config),
131 PeelTips::WhenFiltering,
132 )
133 .map_err(ref_query_error)?;
134
135 let current = match resolve_head_for_repository(repo)? {
136 HeadState::Branch { short_name, .. } => Some(short_name),
137 _ => None,
138 };
139
140 let worktrees = branch_worktree_paths(repo)?;
141
142 let commit_meta = if detail == BranchDetail::Full {
143 let oids: Vec<ObjectId> = listings.iter().map(|l| l.oid).collect();
144 Some(load_commit_metadata(repo, &oids)?)
145 } else {
146 None
147 };
148
149 let mut out = Vec::with_capacity(listings.len());
150 for (idx, listing) in listings.into_iter().enumerate() {
151 let is_remote = listing.kind == RefKind::Remote;
152 let current = !is_remote && current.as_deref() == Some(listing.short_name.as_str());
153 let (subject, committer_time) = commit_meta
154 .as_ref()
155 .and_then(|m| m.get(idx))
156 .cloned()
157 .unwrap_or((None, None));
158
159 let upstream = if is_remote {
160 None
161 } else {
162 build_upstream_status(
163 repo,
164 &config,
165 &listing.short_name,
166 Some(listing.oid),
167 detail,
168 )?
169 };
170
171 let worktree = if is_remote {
172 None
173 } else {
174 worktrees.get(&listing.short_name).cloned()
175 };
176
177 out.push(BranchSummary {
178 name: branch_display_name(query.scope, is_remote, &listing.short_name),
179 full_ref: listing.full_name,
180 is_remote,
181 current,
182 oid: listing.oid,
183 subject,
184 committer_time,
185 upstream,
186 worktree,
187 });
188 }
189 Ok(out)
190}
191
192fn branch_display_name(scope: BranchScope, is_remote: bool, short_name: &str) -> String {
193 if is_remote && scope == BranchScope::All {
194 format!("remotes/{short_name}")
195 } else {
196 short_name.to_owned()
197 }
198}
199
200fn ref_query_for_branch_list(query: &BranchListQuery) -> RefQuery {
201 let mut refs = query.refs.clone();
202 refs.namespaces = match query.scope {
203 BranchScope::Local => vec![RefNamespace::Heads],
204 BranchScope::Remote => vec![RefNamespace::Remotes],
205 BranchScope::All => vec![RefNamespace::Heads, RefNamespace::Remotes],
206 };
207 refs
208}
209
210fn ref_query_options(config: &ConfigSet) -> RefQueryOptions {
211 RefQueryOptions {
212 versionsort_suffixes: config
213 .get_all("versionsort.suffix")
214 .iter()
215 .map(|s| s.to_owned())
216 .collect(),
217 ignore_case: config
218 .get_bool("core.ignorecase")
219 .and_then(|r| r.ok())
220 .unwrap_or(false),
221 }
222}
223
224fn ref_query_error(err: RefQueryError) -> Error {
225 match err {
226 RefQueryError::Store(e) => e,
227 RefQueryError::MissingObjectDatabase => {
228 Error::Message("creator-date sort requires commit objects".into())
229 }
230 RefQueryError::MissingObject(oid) => Error::ObjectNotFound(oid.to_string()),
231 RefQueryError::NotCommit(oid) => Error::Message(format!("not a commit: {oid}")),
232 }
233}
234
235fn branch_worktree_paths(repo: &Repository) -> Result<HashMap<String, PathBuf>> {
236 let mut map = HashMap::new();
237 for entry in list_worktrees(repo)? {
238 if let HeadState::Branch { short_name, .. } = &entry.head {
239 map.entry(short_name.clone())
240 .or_insert_with(|| entry.path.clone());
241 }
242 }
243 Ok(map)
244}
245
246fn build_upstream_status(
247 repo: &Repository,
248 config: &ConfigSet,
249 branch_short: &str,
250 branch_tip: Option<ObjectId>,
251 detail: BranchDetail,
252) -> Result<Option<UpstreamStatus>> {
253 let remote_key = format!("branch.{branch_short}.remote");
254 let merge_key = format!("branch.{branch_short}.merge");
255 let Some(full_ref) = crate::branch_tracking::branch_upstream_full_ref(config, branch_short)
256 else {
257 return Ok(None);
258 };
259 let remote = config.get(&remote_key).unwrap_or_else(|| ".".to_owned());
260 let merge = config
261 .get(&merge_key)
262 .unwrap_or_else(|| format!("refs/heads/{branch_short}"));
263 let resolved = repo.references().resolve_if_exists(&full_ref)?;
264 let gone = resolved.is_none();
265 let short = abbreviate_ref_name(&full_ref);
266 let (ahead, behind) = if detail == BranchDetail::Full && !gone {
267 if let (Some(tip), Some(up_oid)) = (branch_tip, resolved) {
268 let cmp = crate::branch_tracking::TipComparison::compute(repo, tip, up_oid, 0)?;
269 (Some(cmp.ahead), Some(cmp.behind))
270 } else {
271 (None, None)
272 }
273 } else {
274 (None, None)
275 };
276 Ok(Some(UpstreamStatus {
277 full_ref,
278 short,
279 remote,
280 merge,
281 gone,
282 ahead,
283 behind,
284 }))
285}
286
287fn load_commit_metadata(
288 repo: &Repository,
289 oids: &[ObjectId],
290) -> Result<Vec<(Option<String>, Option<OffsetDateTime>)>> {
291 if let [oid] = oids {
292 return Ok(vec![read_commit_meta(repo, oid)?]);
293 }
294 if oids.is_empty() {
295 return Ok(Vec::new());
296 }
297 let config = repo.config()?;
298 let par_opts = repo
299 .thread_policy()
300 .resolve(ParallelOp::DiffContent, &config);
301 let total_bytes = oids.len().saturating_mul(512);
302 let rows = try_par_map(oids, par_opts, total_bytes, |oid| {
303 read_commit_meta(repo, oid)
304 })
305 .map_err(|e| match e {
306 ParallelHashError::Task(err) => err,
307 })?;
308 Ok(rows)
309}
310
311fn read_commit_meta(
312 repo: &Repository,
313 oid: &ObjectId,
314) -> Result<(Option<String>, Option<OffsetDateTime>)> {
315 let obj = repo.odb().read(oid)?;
316 if obj.kind != ObjectKind::Commit {
317 return Ok((None, None));
318 }
319 let subject = read_commit_subject_pretty(&obj.data).ok();
320 let committer_time = crate::objects::CommitRef::parse(&obj.data)
321 .ok()
322 .and_then(|cref| {
323 OffsetDateTime::from_unix_timestamp(cref.committer_unix_seconds_for_ordering()).ok()
324 });
325 Ok((subject, committer_time))
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn branch_display_name_matches_git_branch_a_for_remotes() {
334 assert_eq!(
335 branch_display_name(BranchScope::All, true, "origin/main"),
336 "remotes/origin/main"
337 );
338 assert_eq!(
339 branch_display_name(BranchScope::Remote, true, "origin/main"),
340 "origin/main"
341 );
342 assert_eq!(branch_display_name(BranchScope::All, false, "main"), "main");
343 }
344}