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
|
- [ ] Commit graph lanes align with their rows while scrolling; branch-out and
|
||||||
merge-back connections use smooth, rounded curves that meet the correct
|
merge-back connections use smooth, rounded curves that meet the correct
|
||||||
lane and commit node without angular horizontal bars or overlaps.
|
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 commit and verify Changed Files paths and statuses.
|
||||||
- [ ] Open a changed file and verify the diff title, old/new line numbers,
|
- [ ] Open a changed file and verify the diff title, old/new line numbers,
|
||||||
monospaced text, addition/removal/hunk colors, vertical scrolling, and
|
monospaced text, addition/removal/hunk colors, vertical scrolling, and
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ pub struct CommitRow {
|
|||||||
pub sha: String,
|
pub sha: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub detail: String,
|
pub detail: String,
|
||||||
|
pub branch_label: Option<String>,
|
||||||
pub top_lanes: Vec<u32>,
|
pub top_lanes: Vec<u32>,
|
||||||
pub bottom_lanes: Vec<u32>,
|
pub bottom_lanes: Vec<u32>,
|
||||||
pub node_lane: Option<u32>,
|
pub node_lane: Option<u32>,
|
||||||
@@ -516,6 +517,12 @@ pub fn commit_page(
|
|||||||
.and_then(|commit| commit.author.as_ref())
|
.and_then(|commit| commit.author.as_ref())
|
||||||
.and_then(|author| author.date.as_deref())
|
.and_then(|author| author.date.as_deref())
|
||||||
.or(commit.created.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 {
|
Some(CommitRow {
|
||||||
sha: sha.into(),
|
sha: sha.into(),
|
||||||
title: details
|
title: details
|
||||||
@@ -523,16 +530,12 @@ pub fn commit_page(
|
|||||||
.map(|message| message.lines().next().unwrap_or(message))
|
.map(|message| message.lines().next().unwrap_or(message))
|
||||||
.unwrap_or("Commit")
|
.unwrap_or("Commit")
|
||||||
.into(),
|
.into(),
|
||||||
detail: if row.refs.is_empty() {
|
detail: if !branch_labels.is_empty() {
|
||||||
format!("{author} · {}\n{}", compact_date(date), short_sha(sha))
|
format!("{author} · {} · {}", compact_date(date), short_sha(sha))
|
||||||
} else {
|
} else {
|
||||||
format!(
|
format!("{author} · {}\n{}", compact_date(date), short_sha(sha))
|
||||||
"{}\n{author} · {} · {}",
|
|
||||||
row.refs.join(" · "),
|
|
||||||
compact_date(date),
|
|
||||||
short_sha(sha)
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
|
branch_label: (!branch_labels.is_empty()).then(|| branch_labels.join(" · ")),
|
||||||
top_lanes: indices(&row.top_lanes),
|
top_lanes: indices(&row.top_lanes),
|
||||||
bottom_lanes: indices(&row.bottom_lanes),
|
bottom_lanes: indices(&row.bottom_lanes),
|
||||||
node_lane: row.node_lane.map(|lane| lane as u32),
|
node_lane: row.node_lane.map(|lane| lane as u32),
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use crate::models;
|
use crate::models;
|
||||||
|
|
||||||
pub const DEFAULT_PAGE_SIZE: i32 = 30;
|
pub const DEFAULT_PAGE_SIZE: i32 = 30;
|
||||||
@@ -159,6 +157,7 @@ pub struct HistoryCommit {
|
|||||||
pub top_connections: Vec<usize>,
|
pub top_connections: Vec<usize>,
|
||||||
pub bottom_connections: Vec<usize>,
|
pub bottom_connections: Vec<usize>,
|
||||||
pub refs: Vec<String>,
|
pub refs: Vec<String>,
|
||||||
|
pub branch_starts: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -168,8 +167,6 @@ pub struct HomeData {
|
|||||||
pub next_page: Option<i32>,
|
pub next_page: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type PullRefs = HashMap<String, Vec<String>>;
|
|
||||||
|
|
||||||
pub fn api_date(timestamp: i64) -> String {
|
pub fn api_date(timestamp: i64) -> String {
|
||||||
let (year, month, day) = civil_from_days(timestamp.div_euclid(86_400));
|
let (year, month, day) = civil_from_days(timestamp.div_euclid(86_400));
|
||||||
format!("{year:04}-{month:02}-{day:02}T00:00:00Z")
|
format!("{year:04}-{month:02}-{day:02}T00:00:00Z")
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use tokio::task::JoinSet;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Client, Error, Result,
|
Client, Error, Result,
|
||||||
domain::{DEFAULT_PAGE_SIZE, HistoryCommit, Page, PullRefs, RepositoryId},
|
domain::{DEFAULT_PAGE_SIZE, HistoryCommit, Page, RepositoryId},
|
||||||
models,
|
models,
|
||||||
};
|
};
|
||||||
use gitea_openapi::apis;
|
use gitea_openapi::apis;
|
||||||
@@ -147,6 +147,7 @@ impl Client {
|
|||||||
top_connections: Vec::new(),
|
top_connections: Vec::new(),
|
||||||
bottom_connections: Vec::new(),
|
bottom_connections: Vec::new(),
|
||||||
refs: Vec::new(),
|
refs: Vec::new(),
|
||||||
|
branch_starts: Vec::new(),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
})
|
})
|
||||||
@@ -219,8 +220,8 @@ impl Client {
|
|||||||
.map_err(Error::generated)
|
.map_err(Error::generated)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn pull_refs(&self, repository: &RepositoryId) -> Result<PullRefs> {
|
async fn pull_refs(&self, repository: &RepositoryId) -> Result<PullMetadata> {
|
||||||
let mut refs: PullRefs = HashMap::new();
|
let mut refs = PullMetadata::default();
|
||||||
for page in 1.. {
|
for page in 1.. {
|
||||||
let batch = self.repository_pulls(repository, "all", page, 100).await?;
|
let batch = self.repository_pulls(repository, "all", page, 100).await?;
|
||||||
for pull in batch.items {
|
for pull in batch.items {
|
||||||
@@ -230,7 +231,17 @@ impl Client {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if !label.is_empty() {
|
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 {
|
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(
|
fn build_graph(
|
||||||
histories: Vec<(usize, String, Vec<models::Commit>)>,
|
histories: Vec<(usize, String, Vec<models::Commit>)>,
|
||||||
mut extra_refs: PullRefs,
|
extra_refs: PullMetadata,
|
||||||
) -> Vec<HistoryCommit> {
|
) -> Vec<HistoryCommit> {
|
||||||
let mut commits = HashMap::new();
|
let mut commits = HashMap::new();
|
||||||
let mut ranks: HashMap<String, (usize, usize)> = 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 {
|
for (branch_index, branch, history) in histories {
|
||||||
if let Some(sha) = history.iter().find_map(|commit| commit.sha.clone()) {
|
if let Some(sha) = history.iter().find_map(|commit| commit.sha.clone()) {
|
||||||
refs.entry(sha).or_default().push(branch);
|
refs.entry(sha).or_default().push(branch);
|
||||||
@@ -303,7 +326,7 @@ fn build_graph(
|
|||||||
commits.entry(sha).or_insert(commit);
|
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();
|
let row = refs.entry(sha).or_default();
|
||||||
for label in labels {
|
for label in labels {
|
||||||
if !row.contains(&label) {
|
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();
|
let mut children = HashMap::new();
|
||||||
for sha in commits.keys() {
|
for sha in commits.keys() {
|
||||||
@@ -350,12 +374,46 @@ fn build_graph(
|
|||||||
top_connections: Vec::new(),
|
top_connections: Vec::new(),
|
||||||
bottom_connections: Vec::new(),
|
bottom_connections: Vec::new(),
|
||||||
refs: refs.remove(&sha).unwrap_or_default(),
|
refs: refs.remove(&sha).unwrap_or_default(),
|
||||||
|
branch_starts: branch_starts.remove(&sha).unwrap_or_default(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
layout_graph(&mut ordered);
|
layout_graph(&mut ordered);
|
||||||
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]) {
|
fn layout_graph(commits: &mut [HistoryCommit]) {
|
||||||
let mut lanes: Vec<Option<String>> = Vec::new();
|
let mut lanes: Vec<Option<String>> = Vec::new();
|
||||||
for row in commits {
|
for row in commits {
|
||||||
@@ -476,7 +534,7 @@ mod tests {
|
|||||||
vec![commit("right", &["base"]), commit("base", &[])],
|
vec![commit("right", &["base"]), commit("base", &[])],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
HashMap::new(),
|
PullMetadata::default(),
|
||||||
);
|
);
|
||||||
assert_eq!(rows.len(), 4);
|
assert_eq!(rows.len(), 4);
|
||||||
assert!(rows.iter().any(|row| row.refs == ["main"]));
|
assert!(rows.iter().any(|row| row.refs == ["main"]));
|
||||||
@@ -499,4 +557,41 @@ mod tests {
|
|||||||
assert_eq!(base.node_lane, Some(0));
|
assert_eq!(base.node_lane, Some(0));
|
||||||
assert_eq!(base.top_connections, [1]);
|
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 sha: String
|
||||||
public var title: String
|
public var title: String
|
||||||
public var detail: String
|
public var detail: String
|
||||||
|
public var branchLabel: String?
|
||||||
public var topLanes: [UInt32]
|
public var topLanes: [UInt32]
|
||||||
public var bottomLanes: [UInt32]
|
public var bottomLanes: [UInt32]
|
||||||
public var nodeLane: UInt32?
|
public var nodeLane: UInt32?
|
||||||
@@ -1517,10 +1518,11 @@ public struct CommitRow: Equatable, Hashable {
|
|||||||
|
|
||||||
// Default memberwise initializers are never public by default, so we
|
// Default memberwise initializers are never public by default, so we
|
||||||
// declare one manually.
|
// 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.sha = sha
|
||||||
self.title = title
|
self.title = title
|
||||||
self.detail = detail
|
self.detail = detail
|
||||||
|
self.branchLabel = branchLabel
|
||||||
self.topLanes = topLanes
|
self.topLanes = topLanes
|
||||||
self.bottomLanes = bottomLanes
|
self.bottomLanes = bottomLanes
|
||||||
self.nodeLane = nodeLane
|
self.nodeLane = nodeLane
|
||||||
@@ -1547,6 +1549,7 @@ public struct FfiConverterTypeCommitRow: FfiConverterRustBuffer {
|
|||||||
sha: FfiConverterString.read(from: &buf),
|
sha: FfiConverterString.read(from: &buf),
|
||||||
title: FfiConverterString.read(from: &buf),
|
title: FfiConverterString.read(from: &buf),
|
||||||
detail: FfiConverterString.read(from: &buf),
|
detail: FfiConverterString.read(from: &buf),
|
||||||
|
branchLabel: FfiConverterOptionString.read(from: &buf),
|
||||||
topLanes: FfiConverterSequenceUInt32.read(from: &buf),
|
topLanes: FfiConverterSequenceUInt32.read(from: &buf),
|
||||||
bottomLanes: FfiConverterSequenceUInt32.read(from: &buf),
|
bottomLanes: FfiConverterSequenceUInt32.read(from: &buf),
|
||||||
nodeLane: FfiConverterOptionUInt32.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.sha, into: &buf)
|
||||||
FfiConverterString.write(value.title, into: &buf)
|
FfiConverterString.write(value.title, into: &buf)
|
||||||
FfiConverterString.write(value.detail, into: &buf)
|
FfiConverterString.write(value.detail, into: &buf)
|
||||||
|
FfiConverterOptionString.write(value.branchLabel, into: &buf)
|
||||||
FfiConverterSequenceUInt32.write(value.topLanes, into: &buf)
|
FfiConverterSequenceUInt32.write(value.topLanes, into: &buf)
|
||||||
FfiConverterSequenceUInt32.write(value.bottomLanes, into: &buf)
|
FfiConverterSequenceUInt32.write(value.bottomLanes, into: &buf)
|
||||||
FfiConverterOptionUInt32.write(value.nodeLane, into: &buf)
|
FfiConverterOptionUInt32.write(value.nodeLane, into: &buf)
|
||||||
|
|||||||
@@ -1309,7 +1309,17 @@ final class CommitCell: UITableViewCell {
|
|||||||
? 0
|
? 0
|
||||||
: CGFloat(laneCount) * Self.laneSpacing + 10
|
: CGFloat(laneCount) * Self.laneSpacing + 10
|
||||||
content.text = row.title
|
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
|
content.secondaryTextProperties.numberOfLines = 2
|
||||||
contentConfiguration = content
|
contentConfiguration = content
|
||||||
setNeedsDisplay()
|
setNeedsDisplay()
|
||||||
|
|||||||
Reference in New Issue
Block a user