Show branch starts in commit history

This commit is contained in:
Georg Bauer
2026-07-31 21:30:29 +02:00
parent a3962449a2
commit 16ecf68bf8
6 changed files with 134 additions and 22 deletions

View File

@@ -180,6 +180,7 @@ pub struct CommitRow {
pub sha: String,
pub title: String,
pub detail: String,
pub branch_label: Option<String>,
pub top_lanes: Vec<u32>,
pub bottom_lanes: Vec<u32>,
pub node_lane: Option<u32>,
@@ -516,6 +517,12 @@ pub fn commit_page(
.and_then(|commit| commit.author.as_ref())
.and_then(|author| author.date.as_deref())
.or(commit.created.as_deref());
let mut branch_labels = row.refs.clone();
for label in &row.branch_starts {
if !branch_labels.contains(label) {
branch_labels.push(label.clone());
}
}
Some(CommitRow {
sha: sha.into(),
title: details
@@ -523,16 +530,12 @@ pub fn commit_page(
.map(|message| message.lines().next().unwrap_or(message))
.unwrap_or("Commit")
.into(),
detail: if row.refs.is_empty() {
format!("{author} · {}\n{}", compact_date(date), short_sha(sha))
detail: if !branch_labels.is_empty() {
format!("{author} · {} · {}", compact_date(date), short_sha(sha))
} else {
format!(
"{}\n{author} · {} · {}",
row.refs.join(" · "),
compact_date(date),
short_sha(sha)
)
format!("{author} · {}\n{}", compact_date(date), short_sha(sha))
},
branch_label: (!branch_labels.is_empty()).then(|| branch_labels.join(" · ")),
top_lanes: indices(&row.top_lanes),
bottom_lanes: indices(&row.bottom_lanes),
node_lane: row.node_lane.map(|lane| lane as u32),

View File

@@ -1,5 +1,3 @@
use std::collections::HashMap;
use crate::models;
pub const DEFAULT_PAGE_SIZE: i32 = 30;
@@ -159,6 +157,7 @@ pub struct HistoryCommit {
pub top_connections: Vec<usize>,
pub bottom_connections: Vec<usize>,
pub refs: Vec<String>,
pub branch_starts: Vec<String>,
}
#[derive(Clone, Debug)]
@@ -168,8 +167,6 @@ pub struct HomeData {
pub next_page: Option<i32>,
}
pub type PullRefs = HashMap<String, Vec<String>>;
pub fn api_date(timestamp: i64) -> String {
let (year, month, day) = civil_from_days(timestamp.div_euclid(86_400));
format!("{year:04}-{month:02}-{day:02}T00:00:00Z")

View File

@@ -7,7 +7,7 @@ use tokio::task::JoinSet;
use crate::{
Client, Error, Result,
domain::{DEFAULT_PAGE_SIZE, HistoryCommit, Page, PullRefs, RepositoryId},
domain::{DEFAULT_PAGE_SIZE, HistoryCommit, Page, RepositoryId},
models,
};
use gitea_openapi::apis;
@@ -147,6 +147,7 @@ impl Client {
top_connections: Vec::new(),
bottom_connections: Vec::new(),
refs: Vec::new(),
branch_starts: Vec::new(),
})
.collect(),
})
@@ -219,8 +220,8 @@ impl Client {
.map_err(Error::generated)
}
async fn pull_refs(&self, repository: &RepositoryId) -> Result<PullRefs> {
let mut refs: PullRefs = HashMap::new();
async fn pull_refs(&self, repository: &RepositoryId) -> Result<PullMetadata> {
let mut refs = PullMetadata::default();
for page in 1.. {
let batch = self.repository_pulls(repository, "all", page, 100).await?;
for pull in batch.items {
@@ -230,7 +231,17 @@ impl Client {
continue;
};
if !label.is_empty() {
refs.entry(sha).or_default().push(label);
refs.tips
.entry(sha.clone())
.or_default()
.push(label.clone());
if let Some(base) = pull.merge_base.filter(|base| !base.is_empty()) {
refs.starts.push(PullBranch {
head: sha,
base,
label,
});
}
}
}
if !batch.has_more {
@@ -281,13 +292,25 @@ impl Client {
}
}
#[derive(Default)]
struct PullMetadata {
tips: HashMap<String, Vec<String>>,
starts: Vec<PullBranch>,
}
struct PullBranch {
head: String,
base: String,
label: String,
}
fn build_graph(
histories: Vec<(usize, String, Vec<models::Commit>)>,
mut extra_refs: PullRefs,
extra_refs: PullMetadata,
) -> Vec<HistoryCommit> {
let mut commits = HashMap::new();
let mut ranks: HashMap<String, (usize, usize)> = HashMap::new();
let mut refs: PullRefs = HashMap::new();
let mut refs: HashMap<String, Vec<String>> = HashMap::new();
for (branch_index, branch, history) in histories {
if let Some(sha) = history.iter().find_map(|commit| commit.sha.clone()) {
refs.entry(sha).or_default().push(branch);
@@ -303,7 +326,7 @@ fn build_graph(
commits.entry(sha).or_insert(commit);
}
}
for (sha, labels) in extra_refs.drain() {
for (sha, labels) in extra_refs.tips {
let row = refs.entry(sha).or_default();
for label in labels {
if !row.contains(&label) {
@@ -311,6 +334,7 @@ fn build_graph(
}
}
}
let mut branch_starts = branch_starts(&commits, extra_refs.starts);
let mut children = HashMap::new();
for sha in commits.keys() {
@@ -350,12 +374,46 @@ fn build_graph(
top_connections: Vec::new(),
bottom_connections: Vec::new(),
refs: refs.remove(&sha).unwrap_or_default(),
branch_starts: branch_starts.remove(&sha).unwrap_or_default(),
});
}
layout_graph(&mut ordered);
ordered
}
fn branch_starts(
commits: &HashMap<String, models::Commit>,
branches: Vec<PullBranch>,
) -> HashMap<String, Vec<String>> {
let mut starts: HashMap<String, Vec<String>> = HashMap::new();
for branch in branches {
let Some(sha) = first_commit_after(commits, &branch.head, &branch.base) else {
continue;
};
let labels = starts.entry(sha).or_default();
if !labels.contains(&branch.label) {
labels.push(branch.label);
}
}
starts
}
fn first_commit_after(
commits: &HashMap<String, models::Commit>,
head: &str,
base: &str,
) -> Option<String> {
let mut sha = head;
for _ in 0..commits.len() {
let parent = commit_parents(commits.get(sha)?).next()?;
if parent == base {
return Some(sha.into());
}
sha = parent;
}
None
}
fn layout_graph(commits: &mut [HistoryCommit]) {
let mut lanes: Vec<Option<String>> = Vec::new();
for row in commits {
@@ -476,7 +534,7 @@ mod tests {
vec![commit("right", &["base"]), commit("base", &[])],
),
],
HashMap::new(),
PullMetadata::default(),
);
assert_eq!(rows.len(), 4);
assert!(rows.iter().any(|row| row.refs == ["main"]));
@@ -499,4 +557,41 @@ mod tests {
assert_eq!(base.node_lane, Some(0));
assert_eq!(base.top_connections, [1]);
}
#[test]
fn labels_first_commit_of_pull_branch() {
let rows = build_graph(
vec![(
0,
"main".into(),
vec![
commit("merge", &["main", "head"]),
commit("head", &["first"]),
commit("first", &["base"]),
commit("main", &["base"]),
commit("base", &[]),
],
)],
PullMetadata {
tips: HashMap::from([("head".into(), vec!["feature".into()])]),
starts: vec![PullBranch {
head: "head".into(),
base: "base".into(),
label: "feature".into(),
}],
},
);
let first = rows
.iter()
.find(|row| row.commit.sha.as_deref() == Some("first"))
.unwrap();
assert_eq!(first.branch_starts, ["feature"]);
assert!(
rows.iter()
.find(|row| row.commit.sha.as_deref() == Some("head"))
.unwrap()
.refs
.contains(&"feature".into())
);
}
}