Show branch starts in commit history
This commit is contained in:
@@ -214,6 +214,9 @@ xcrun simctl launch booted de.rfc1437.gotcha
|
||||
- [ ] Commit graph lanes align with their rows while scrolling; branch-out and
|
||||
merge-back connections use smooth, rounded curves that meet the correct
|
||||
lane and commit node without angular horizontal bars or overlaps.
|
||||
- [ ] In **All** history, branch names use the accent color both at branch tips
|
||||
and at the first commit made on a pull-request branch, including a merged
|
||||
branch whose tip was deleted.
|
||||
- [ ] Open a commit and verify Changed Files paths and statuses.
|
||||
- [ ] Open a changed file and verify the diff title, old/new line numbers,
|
||||
monospaced text, addition/removal/hunk colors, vertical scrolling, and
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1509,6 +1509,7 @@ public struct CommitRow: Equatable, Hashable {
|
||||
public var sha: String
|
||||
public var title: String
|
||||
public var detail: String
|
||||
public var branchLabel: String?
|
||||
public var topLanes: [UInt32]
|
||||
public var bottomLanes: [UInt32]
|
||||
public var nodeLane: UInt32?
|
||||
@@ -1517,10 +1518,11 @@ public struct CommitRow: Equatable, Hashable {
|
||||
|
||||
// Default memberwise initializers are never public by default, so we
|
||||
// declare one manually.
|
||||
public init(sha: String, title: String, detail: String, topLanes: [UInt32], bottomLanes: [UInt32], nodeLane: UInt32?, topConnections: [UInt32], bottomConnections: [UInt32]) {
|
||||
public init(sha: String, title: String, detail: String, branchLabel: String?, topLanes: [UInt32], bottomLanes: [UInt32], nodeLane: UInt32?, topConnections: [UInt32], bottomConnections: [UInt32]) {
|
||||
self.sha = sha
|
||||
self.title = title
|
||||
self.detail = detail
|
||||
self.branchLabel = branchLabel
|
||||
self.topLanes = topLanes
|
||||
self.bottomLanes = bottomLanes
|
||||
self.nodeLane = nodeLane
|
||||
@@ -1547,6 +1549,7 @@ public struct FfiConverterTypeCommitRow: FfiConverterRustBuffer {
|
||||
sha: FfiConverterString.read(from: &buf),
|
||||
title: FfiConverterString.read(from: &buf),
|
||||
detail: FfiConverterString.read(from: &buf),
|
||||
branchLabel: FfiConverterOptionString.read(from: &buf),
|
||||
topLanes: FfiConverterSequenceUInt32.read(from: &buf),
|
||||
bottomLanes: FfiConverterSequenceUInt32.read(from: &buf),
|
||||
nodeLane: FfiConverterOptionUInt32.read(from: &buf),
|
||||
@@ -1559,6 +1562,7 @@ public struct FfiConverterTypeCommitRow: FfiConverterRustBuffer {
|
||||
FfiConverterString.write(value.sha, into: &buf)
|
||||
FfiConverterString.write(value.title, into: &buf)
|
||||
FfiConverterString.write(value.detail, into: &buf)
|
||||
FfiConverterOptionString.write(value.branchLabel, into: &buf)
|
||||
FfiConverterSequenceUInt32.write(value.topLanes, into: &buf)
|
||||
FfiConverterSequenceUInt32.write(value.bottomLanes, into: &buf)
|
||||
FfiConverterOptionUInt32.write(value.nodeLane, into: &buf)
|
||||
|
||||
@@ -1309,7 +1309,17 @@ final class CommitCell: UITableViewCell {
|
||||
? 0
|
||||
: CGFloat(laneCount) * Self.laneSpacing + 10
|
||||
content.text = row.title
|
||||
content.secondaryText = row.detail
|
||||
if let branch = row.branchLabel {
|
||||
let text = NSMutableAttributedString(string: "\(branch)\n\(row.detail)")
|
||||
text.addAttribute(
|
||||
.foregroundColor,
|
||||
value: UIColor.tintColor,
|
||||
range: NSRange(location: 0, length: (branch as NSString).length)
|
||||
)
|
||||
content.secondaryAttributedText = text
|
||||
} else {
|
||||
content.secondaryText = row.detail
|
||||
}
|
||||
content.secondaryTextProperties.numberOfLines = 2
|
||||
contentConfiguration = content
|
||||
setNeedsDisplay()
|
||||
|
||||
Reference in New Issue
Block a user