Show commit details above changed files
This commit is contained in:
@@ -217,7 +217,11 @@ xcrun simctl launch booted de.rfc1437.gotcha
|
|||||||
- [ ] In **All** history, branch names use the accent color both at branch tips
|
- [ ] 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
|
and at the first commit made on a pull-request branch, including a merged
|
||||||
branch whose tip was deleted.
|
branch whose tip was deleted.
|
||||||
- [ ] Open a commit and verify Changed Files paths and statuses.
|
- [ ] Open a commit and verify its header shows the full title and description,
|
||||||
|
full hash, author, committer, date, known branch/ref, and signature status
|
||||||
|
above the Changed Files paths and statuses. Unsigned commits say
|
||||||
|
**Unsigned**. Check that long values wrap and remain readable at the
|
||||||
|
largest Dynamic Type accessibility size.
|
||||||
- [ ] 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
|
||||||
source-style horizontal scrolling for long lines without word wrapping,
|
source-style horizontal scrolling for long lines without word wrapping,
|
||||||
|
|||||||
@@ -316,14 +316,14 @@ pub async fn load_pull(
|
|||||||
.map_err(message)
|
.map_err(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn load_commit_files(
|
pub async fn load_commit(
|
||||||
server: &Server,
|
server: &Server,
|
||||||
owner: &str,
|
owner: &str,
|
||||||
repository: &str,
|
repository: &str,
|
||||||
sha: &str,
|
sha: &str,
|
||||||
) -> Result<Vec<models::CommitAffectedFiles>, String> {
|
) -> Result<models::Commit, String> {
|
||||||
client(server)?
|
client(server)?
|
||||||
.commit_files(&scope(owner, repository)?, sha)
|
.commit(&scope(owner, repository)?, sha)
|
||||||
.await
|
.await
|
||||||
.map_err(message)
|
.map_err(message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -726,14 +726,16 @@ impl GotchaCore {
|
|||||||
Ok(repository_file_page(&path, data))
|
Ok(repository_file_page(&path, data))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn commit_files(
|
pub async fn commit_details(
|
||||||
&self,
|
&self,
|
||||||
owner: String,
|
owner: String,
|
||||||
repository: String,
|
repository: String,
|
||||||
sha: String,
|
sha: String,
|
||||||
) -> Result<Vec<FileRow>, GotchaError> {
|
branch: Option<String>,
|
||||||
Ok(commit_file_rows(
|
) -> Result<CommitDetailsPage, GotchaError> {
|
||||||
load_commit_files(&self.server()?, &owner, &repository, &sha).await?,
|
Ok(commit_details_page(
|
||||||
|
load_commit(&self.server()?, &owner, &repository, &sha).await?,
|
||||||
|
branch,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -196,6 +196,20 @@ pub struct CommitPage {
|
|||||||
pub has_more: bool,
|
pub has_more: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, uniffi::Record)]
|
||||||
|
pub struct CommitMetadataRow {
|
||||||
|
pub label: String,
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, uniffi::Record)]
|
||||||
|
pub struct CommitDetailsPage {
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
pub metadata: Vec<CommitMetadataRow>,
|
||||||
|
pub files: Vec<FileRow>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, uniffi::Record)]
|
#[derive(Clone, uniffi::Record)]
|
||||||
pub struct FileRow {
|
pub struct FileRow {
|
||||||
pub path: String,
|
pub path: String,
|
||||||
@@ -716,6 +730,97 @@ pub fn commit_file_rows(files: Vec<models::CommitAffectedFiles>) -> Vec<FileRow>
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn commit_details_page(commit: models::Commit, branch: Option<String>) -> CommitDetailsPage {
|
||||||
|
let details = commit.commit.as_deref();
|
||||||
|
let message = details
|
||||||
|
.and_then(|details| details.message.as_deref())
|
||||||
|
.unwrap_or("Commit");
|
||||||
|
let mut lines = message.lines();
|
||||||
|
let title = lines.next().unwrap_or("Commit").to_string();
|
||||||
|
let description = lines.collect::<Vec<_>>().join("\n").trim().to_string();
|
||||||
|
let author = details
|
||||||
|
.and_then(|details| details.author.as_deref())
|
||||||
|
.and_then(commit_user)
|
||||||
|
.or_else(|| {
|
||||||
|
commit
|
||||||
|
.author
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|author| author.login.clone())
|
||||||
|
});
|
||||||
|
let committer = details
|
||||||
|
.and_then(|details| details.committer.as_deref())
|
||||||
|
.and_then(commit_user)
|
||||||
|
.or_else(|| {
|
||||||
|
commit
|
||||||
|
.committer
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|committer| committer.login.clone())
|
||||||
|
});
|
||||||
|
let date = details
|
||||||
|
.and_then(|details| details.committer.as_deref())
|
||||||
|
.and_then(|committer| committer.date.as_deref())
|
||||||
|
.or_else(|| {
|
||||||
|
details
|
||||||
|
.and_then(|details| details.author.as_deref())
|
||||||
|
.and_then(|author| author.date.as_deref())
|
||||||
|
})
|
||||||
|
.or(commit.created.as_deref());
|
||||||
|
let files = commit.files.unwrap_or_default();
|
||||||
|
let mut metadata = Vec::new();
|
||||||
|
if let Some(author) = author {
|
||||||
|
metadata.push(metadata_row("Author", author));
|
||||||
|
}
|
||||||
|
if let Some(committer) = committer {
|
||||||
|
metadata.push(metadata_row("Committer", committer));
|
||||||
|
}
|
||||||
|
if date.is_some() {
|
||||||
|
metadata.push(metadata_row("Committed", compact_date(date)));
|
||||||
|
}
|
||||||
|
if let Some(branch) = branch.filter(|branch| !branch.is_empty()) {
|
||||||
|
metadata.push(metadata_row("Branch", branch));
|
||||||
|
}
|
||||||
|
metadata.push(metadata_row(
|
||||||
|
"Commit",
|
||||||
|
commit.sha.clone().unwrap_or_else(|| "unknown".into()),
|
||||||
|
));
|
||||||
|
let verification = details.and_then(|details| details.verification.as_deref());
|
||||||
|
metadata.push(metadata_row(
|
||||||
|
"Signature",
|
||||||
|
match verification.filter(|verification| {
|
||||||
|
verification
|
||||||
|
.signature
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|signature| !signature.is_empty())
|
||||||
|
}) {
|
||||||
|
Some(verification) if verification.verified.unwrap_or(false) => "Verified".into(),
|
||||||
|
Some(_) => "Unverified".into(),
|
||||||
|
None => "Unsigned".into(),
|
||||||
|
},
|
||||||
|
));
|
||||||
|
CommitDetailsPage {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
metadata,
|
||||||
|
files: commit_file_rows(files),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn commit_user(user: &models::CommitUser) -> Option<String> {
|
||||||
|
match (user.name.as_deref(), user.email.as_deref()) {
|
||||||
|
(Some(name), Some(email)) => Some(format!("{name} <{email}>")),
|
||||||
|
(Some(name), None) => Some(name.into()),
|
||||||
|
(None, Some(email)) => Some(email.into()),
|
||||||
|
(None, None) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata_row(label: &str, value: String) -> CommitMetadataRow {
|
||||||
|
CommitMetadataRow {
|
||||||
|
label: label.into(),
|
||||||
|
value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn repository_content_rows(
|
pub fn repository_content_rows(
|
||||||
contents: Vec<models::ContentsResponse>,
|
contents: Vec<models::ContentsResponse>,
|
||||||
) -> Vec<RepositoryContentRow> {
|
) -> Vec<RepositoryContentRow> {
|
||||||
@@ -1215,6 +1320,66 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn presents_complete_commit_details() {
|
||||||
|
let page = commit_details_page(
|
||||||
|
models::Commit {
|
||||||
|
sha: Some("0123456789abcdef".into()),
|
||||||
|
commit: Some(Box::new(models::RepoCommit {
|
||||||
|
message: Some("Add details\n\nExplain the change.".into()),
|
||||||
|
author: Some(Box::new(models::CommitUser {
|
||||||
|
name: Some("Ada".into()),
|
||||||
|
email: Some("ada@example.com".into()),
|
||||||
|
date: Some("2026-08-01T10:00:00Z".into()),
|
||||||
|
})),
|
||||||
|
committer: Some(Box::new(models::CommitUser {
|
||||||
|
name: Some("Grace".into()),
|
||||||
|
date: Some("2026-08-02T11:00:00Z".into()),
|
||||||
|
..Default::default()
|
||||||
|
})),
|
||||||
|
verification: Some(Box::new(models::PayloadCommitVerification {
|
||||||
|
verified: Some(true),
|
||||||
|
signature: Some("signed".into()),
|
||||||
|
..Default::default()
|
||||||
|
})),
|
||||||
|
..Default::default()
|
||||||
|
})),
|
||||||
|
files: Some(vec![models::CommitAffectedFiles {
|
||||||
|
filename: Some("README.md".into()),
|
||||||
|
status: Some("modified".into()),
|
||||||
|
}]),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Some("main".into()),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(page.title, "Add details");
|
||||||
|
assert_eq!(page.description, "Explain the change.");
|
||||||
|
assert_eq!(page.files[0].path, "README.md");
|
||||||
|
assert_eq!(
|
||||||
|
page.metadata
|
||||||
|
.iter()
|
||||||
|
.map(|row| (row.label.as_str(), row.value.as_str()))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
[
|
||||||
|
("Author", "Ada <ada@example.com>"),
|
||||||
|
("Committer", "Grace"),
|
||||||
|
("Committed", "2026-08-02"),
|
||||||
|
("Branch", "main"),
|
||||||
|
("Commit", "0123456789abcdef"),
|
||||||
|
("Signature", "Verified"),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
commit_details_page(models::Commit::default(), None)
|
||||||
|
.metadata
|
||||||
|
.last()
|
||||||
|
.unwrap()
|
||||||
|
.value,
|
||||||
|
"Unsigned"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn maps_heat_levels_and_labels() {
|
fn maps_heat_levels_and_labels() {
|
||||||
let heatmap = vec![
|
let heatmap = vec![
|
||||||
|
|||||||
@@ -189,22 +189,17 @@ impl Client {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn commit_files(
|
pub async fn commit(&self, repository: &RepositoryId, sha: &str) -> Result<models::Commit> {
|
||||||
&self,
|
|
||||||
repository: &RepositoryId,
|
|
||||||
sha: &str,
|
|
||||||
) -> Result<Vec<models::CommitAffectedFiles>> {
|
|
||||||
apis::repository_api::repo_get_single_commit(
|
apis::repository_api::repo_get_single_commit(
|
||||||
&self.configuration(),
|
&self.configuration(),
|
||||||
&repository.owner,
|
&repository.owner,
|
||||||
&repository.repository,
|
&repository.repository,
|
||||||
sha,
|
sha,
|
||||||
|
Some(false),
|
||||||
Some(true),
|
Some(true),
|
||||||
None,
|
|
||||||
Some(true),
|
Some(true),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map(|commit| commit.files.unwrap_or_default())
|
|
||||||
.map_err(Error::generated)
|
.map_err(Error::generated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -613,9 +613,9 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
|
|||||||
|
|
||||||
func clearPullFilters() throws
|
func clearPullFilters() throws
|
||||||
|
|
||||||
func commitDiff(owner: String, repository: String, sha: String, path: String) async throws -> DiffPage
|
func commitDetails(owner: String, repository: String, sha: String, branch: String?) async throws -> CommitDetailsPage
|
||||||
|
|
||||||
func commitFiles(owner: String, repository: String, sha: String) async throws -> [FileRow]
|
func commitDiff(owner: String, repository: String, sha: String, path: String) async throws -> DiffPage
|
||||||
|
|
||||||
func commits(owner: String, repository: String, branch: String?, path: String, pages: UInt32) async throws -> CommitPage
|
func commits(owner: String, repository: String, branch: String?, path: String, pages: UInt32) async throws -> CommitPage
|
||||||
|
|
||||||
@@ -793,6 +793,22 @@ open func clearPullFilters()throws {try rustCallWithError(FfiConverterTypeGotc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open func commitDetails(owner: String, repository: String, sha: String, branch: String?)async throws -> CommitDetailsPage {
|
||||||
|
return
|
||||||
|
try await uniffiRustCallAsync(
|
||||||
|
rustFutureFunc: {
|
||||||
|
uniffi_gotcha_core_fn_method_gotchacore_commit_details(
|
||||||
|
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterString.lower(sha),FfiConverterOptionString.lower(branch)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
|
||||||
|
completeFunc: ffi_gotcha_core_rust_future_complete_rust_buffer,
|
||||||
|
freeFunc: ffi_gotcha_core_rust_future_free_rust_buffer,
|
||||||
|
liftFunc: FfiConverterTypeCommitDetailsPage_lift,
|
||||||
|
errorHandler: FfiConverterTypeGotchaError_lift
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
open func commitDiff(owner: String, repository: String, sha: String, path: String)async throws -> DiffPage {
|
open func commitDiff(owner: String, repository: String, sha: String, path: String)async throws -> DiffPage {
|
||||||
return
|
return
|
||||||
try await uniffiRustCallAsync(
|
try await uniffiRustCallAsync(
|
||||||
@@ -809,22 +825,6 @@ open func commitDiff(owner: String, repository: String, sha: String, path: Strin
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
open func commitFiles(owner: String, repository: String, sha: String)async throws -> [FileRow] {
|
|
||||||
return
|
|
||||||
try await uniffiRustCallAsync(
|
|
||||||
rustFutureFunc: {
|
|
||||||
uniffi_gotcha_core_fn_method_gotchacore_commit_files(
|
|
||||||
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterString.lower(sha)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
|
|
||||||
completeFunc: ffi_gotcha_core_rust_future_complete_rust_buffer,
|
|
||||||
freeFunc: ffi_gotcha_core_rust_future_free_rust_buffer,
|
|
||||||
liftFunc: FfiConverterSequenceTypeFileRow.lift,
|
|
||||||
errorHandler: FfiConverterTypeGotchaError_lift
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
open func commits(owner: String, repository: String, branch: String?, path: String, pages: UInt32)async throws -> CommitPage {
|
open func commits(owner: String, repository: String, branch: String?, path: String, pages: UInt32)async throws -> CommitPage {
|
||||||
return
|
return
|
||||||
try await uniffiRustCallAsync(
|
try await uniffiRustCallAsync(
|
||||||
@@ -1443,6 +1443,122 @@ public func FfiConverterTypeCommentRow_lower(_ value: CommentRow) -> RustBuffer
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public struct CommitDetailsPage: Equatable, Hashable {
|
||||||
|
public var title: String
|
||||||
|
public var description: String
|
||||||
|
public var metadata: [CommitMetadataRow]
|
||||||
|
public var files: [FileRow]
|
||||||
|
|
||||||
|
// Default memberwise initializers are never public by default, so we
|
||||||
|
// declare one manually.
|
||||||
|
public init(title: String, description: String, metadata: [CommitMetadataRow], files: [FileRow]) {
|
||||||
|
self.title = title
|
||||||
|
self.description = description
|
||||||
|
self.metadata = metadata
|
||||||
|
self.files = files
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension CommitDetailsPage: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeCommitDetailsPage: FfiConverterRustBuffer {
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> CommitDetailsPage {
|
||||||
|
return
|
||||||
|
try CommitDetailsPage(
|
||||||
|
title: FfiConverterString.read(from: &buf),
|
||||||
|
description: FfiConverterString.read(from: &buf),
|
||||||
|
metadata: FfiConverterSequenceTypeCommitMetadataRow.read(from: &buf),
|
||||||
|
files: FfiConverterSequenceTypeFileRow.read(from: &buf)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: CommitDetailsPage, into buf: inout [UInt8]) {
|
||||||
|
FfiConverterString.write(value.title, into: &buf)
|
||||||
|
FfiConverterString.write(value.description, into: &buf)
|
||||||
|
FfiConverterSequenceTypeCommitMetadataRow.write(value.metadata, into: &buf)
|
||||||
|
FfiConverterSequenceTypeFileRow.write(value.files, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeCommitDetailsPage_lift(_ buf: RustBuffer) throws -> CommitDetailsPage {
|
||||||
|
return try FfiConverterTypeCommitDetailsPage.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeCommitDetailsPage_lower(_ value: CommitDetailsPage) -> RustBuffer {
|
||||||
|
return FfiConverterTypeCommitDetailsPage.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public struct CommitMetadataRow: Equatable, Hashable {
|
||||||
|
public var label: String
|
||||||
|
public var value: String
|
||||||
|
|
||||||
|
// Default memberwise initializers are never public by default, so we
|
||||||
|
// declare one manually.
|
||||||
|
public init(label: String, value: String) {
|
||||||
|
self.label = label
|
||||||
|
self.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension CommitMetadataRow: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeCommitMetadataRow: FfiConverterRustBuffer {
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> CommitMetadataRow {
|
||||||
|
return
|
||||||
|
try CommitMetadataRow(
|
||||||
|
label: FfiConverterString.read(from: &buf),
|
||||||
|
value: FfiConverterString.read(from: &buf)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: CommitMetadataRow, into buf: inout [UInt8]) {
|
||||||
|
FfiConverterString.write(value.label, into: &buf)
|
||||||
|
FfiConverterString.write(value.value, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeCommitMetadataRow_lift(_ buf: RustBuffer) throws -> CommitMetadataRow {
|
||||||
|
return try FfiConverterTypeCommitMetadataRow.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeCommitMetadataRow_lower(_ value: CommitMetadataRow) -> RustBuffer {
|
||||||
|
return FfiConverterTypeCommitMetadataRow.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public struct CommitPage: Equatable, Hashable {
|
public struct CommitPage: Equatable, Hashable {
|
||||||
public var branches: [String]
|
public var branches: [String]
|
||||||
public var commits: [CommitRow]
|
public var commits: [CommitRow]
|
||||||
@@ -3744,6 +3860,31 @@ fileprivate struct FfiConverterSequenceTypeCommentRow: FfiConverterRustBuffer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
fileprivate struct FfiConverterSequenceTypeCommitMetadataRow: FfiConverterRustBuffer {
|
||||||
|
typealias SwiftType = [CommitMetadataRow]
|
||||||
|
|
||||||
|
public static func write(_ value: [CommitMetadataRow], into buf: inout [UInt8]) {
|
||||||
|
let len = Int32(value.count)
|
||||||
|
writeInt(&buf, len)
|
||||||
|
for item in value {
|
||||||
|
FfiConverterTypeCommitMetadataRow.write(item, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [CommitMetadataRow] {
|
||||||
|
let len: Int32 = try readInt(&buf)
|
||||||
|
var seq = [CommitMetadataRow]()
|
||||||
|
seq.reserveCapacity(Int(len))
|
||||||
|
for _ in 0 ..< len {
|
||||||
|
seq.append(try FfiConverterTypeCommitMetadataRow.read(from: &buf))
|
||||||
|
}
|
||||||
|
return seq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if swift(>=5.8)
|
#if swift(>=5.8)
|
||||||
@_documentation(visibility: private)
|
@_documentation(visibility: private)
|
||||||
#endif
|
#endif
|
||||||
@@ -4147,10 +4288,10 @@ private let initializationResult: InitializationResult = {
|
|||||||
if (uniffi_gotcha_core_checksum_method_gotchacore_clear_pull_filters() != 36676) {
|
if (uniffi_gotcha_core_checksum_method_gotchacore_clear_pull_filters() != 36676) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
if (uniffi_gotcha_core_checksum_method_gotchacore_commit_diff() != 56887) {
|
if (uniffi_gotcha_core_checksum_method_gotchacore_commit_details() != 19611) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
if (uniffi_gotcha_core_checksum_method_gotchacore_commit_files() != 43926) {
|
if (uniffi_gotcha_core_checksum_method_gotchacore_commit_diff() != 56887) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
if (uniffi_gotcha_core_checksum_method_gotchacore_commits() != 60062) {
|
if (uniffi_gotcha_core_checksum_method_gotchacore_commits() != 60062) {
|
||||||
|
|||||||
@@ -284,16 +284,16 @@ void uniffi_gotcha_core_fn_method_gotchacore_clear_issue_filters(uint64_t ptr, R
|
|||||||
void uniffi_gotcha_core_fn_method_gotchacore_clear_pull_filters(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
void uniffi_gotcha_core_fn_method_gotchacore_clear_pull_filters(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMIT_DETAILS
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMIT_DETAILS
|
||||||
|
uint64_t uniffi_gotcha_core_fn_method_gotchacore_commit_details(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer sha, RustBuffer branch
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMIT_DIFF
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMIT_DIFF
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMIT_DIFF
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMIT_DIFF
|
||||||
uint64_t uniffi_gotcha_core_fn_method_gotchacore_commit_diff(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer sha, RustBuffer path
|
uint64_t uniffi_gotcha_core_fn_method_gotchacore_commit_diff(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer sha, RustBuffer path
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMIT_FILES
|
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMIT_FILES
|
|
||||||
uint64_t uniffi_gotcha_core_fn_method_gotchacore_commit_files(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer sha
|
|
||||||
);
|
|
||||||
#endif
|
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMITS
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMITS
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMITS
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMITS
|
||||||
uint64_t uniffi_gotcha_core_fn_method_gotchacore_commits(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer branch, RustBuffer path, uint32_t pages
|
uint64_t uniffi_gotcha_core_fn_method_gotchacore_commits(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer branch, RustBuffer path, uint32_t pages
|
||||||
@@ -739,15 +739,15 @@ uint16_t uniffi_gotcha_core_checksum_method_gotchacore_clear_pull_filters(void
|
|||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_COMMIT_DIFF
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_COMMIT_DETAILS
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_COMMIT_DIFF
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_COMMIT_DETAILS
|
||||||
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_commit_diff(void
|
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_commit_details(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_COMMIT_FILES
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_COMMIT_DIFF
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_COMMIT_FILES
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_COMMIT_DIFF
|
||||||
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_commit_files(void
|
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_commit_diff(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1229,7 +1229,8 @@ final class CommitsViewController: RefreshingTableViewController {
|
|||||||
context: context,
|
context: context,
|
||||||
owner: owner,
|
owner: owner,
|
||||||
repository: repository,
|
repository: repository,
|
||||||
sha: row.sha
|
sha: row.sha,
|
||||||
|
branch: row.branchLabel ?? branch
|
||||||
),
|
),
|
||||||
animated: true
|
animated: true
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -398,19 +398,96 @@ private final class FileListView: UITableView, UITableViewDataSource, UITableVie
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private final class CommitHeaderView: UIView {
|
||||||
|
private let stack = UIStackView()
|
||||||
|
private let titleLabel = UILabel()
|
||||||
|
private let descriptionLabel = UILabel()
|
||||||
|
private let metadataStack = UIStackView()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
backgroundColor = .systemBackground
|
||||||
|
directionalLayoutMargins = .init(top: 20, leading: 20, bottom: 20, trailing: 20)
|
||||||
|
|
||||||
|
titleLabel.font = .preferredFont(forTextStyle: .title2)
|
||||||
|
titleLabel.adjustsFontForContentSizeCategory = true
|
||||||
|
titleLabel.numberOfLines = 0
|
||||||
|
titleLabel.accessibilityTraits = .header
|
||||||
|
descriptionLabel.font = .preferredFont(forTextStyle: .body)
|
||||||
|
descriptionLabel.adjustsFontForContentSizeCategory = true
|
||||||
|
descriptionLabel.numberOfLines = 0
|
||||||
|
|
||||||
|
metadataStack.axis = .vertical
|
||||||
|
metadataStack.spacing = 10
|
||||||
|
stack.axis = .vertical
|
||||||
|
stack.spacing = 12
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stack.addArrangedSubview(titleLabel)
|
||||||
|
stack.addArrangedSubview(descriptionLabel)
|
||||||
|
stack.addArrangedSubview(metadataStack)
|
||||||
|
addSubview(stack)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
stack.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
|
||||||
|
stack.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
|
||||||
|
stack.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
|
||||||
|
stack.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||||
|
|
||||||
|
func configure(_ page: CommitDetailsPage) {
|
||||||
|
titleLabel.text = page.title
|
||||||
|
descriptionLabel.text = page.description
|
||||||
|
descriptionLabel.isHidden = page.description.isEmpty
|
||||||
|
metadataStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||||
|
for metadata in page.metadata {
|
||||||
|
let isCommit = metadata.label == "Commit"
|
||||||
|
let label = UILabel()
|
||||||
|
label.text = metadata.label.uppercased()
|
||||||
|
label.font = .preferredFont(forTextStyle: .caption2)
|
||||||
|
label.adjustsFontForContentSizeCategory = true
|
||||||
|
label.textColor = .secondaryLabel
|
||||||
|
|
||||||
|
let value = UILabel()
|
||||||
|
value.text = metadata.value
|
||||||
|
value.font = isCommit
|
||||||
|
? UIFontMetrics(forTextStyle: .subheadline).scaledFont(
|
||||||
|
for: .monospacedSystemFont(ofSize: 15, weight: .regular)
|
||||||
|
)
|
||||||
|
: .preferredFont(forTextStyle: .subheadline)
|
||||||
|
value.adjustsFontForContentSizeCategory = true
|
||||||
|
value.numberOfLines = 0
|
||||||
|
value.lineBreakMode = isCommit ? .byCharWrapping : .byWordWrapping
|
||||||
|
|
||||||
|
let row = UIStackView(arrangedSubviews: [label, value])
|
||||||
|
row.axis = .vertical
|
||||||
|
row.spacing = 2
|
||||||
|
row.isAccessibilityElement = true
|
||||||
|
row.accessibilityLabel = "\(metadata.label): \(metadata.value)"
|
||||||
|
metadataStack.addArrangedSubview(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class FilesViewController: RefreshingTableViewController {
|
final class FilesViewController: RefreshingTableViewController {
|
||||||
private let context: AppContext
|
private let context: AppContext
|
||||||
private let owner: String
|
private let owner: String
|
||||||
private let repository: String
|
private let repository: String
|
||||||
private let sha: String
|
private let sha: String
|
||||||
private var rows: [FileRow] = []
|
private let branch: String?
|
||||||
|
private let commitHeader = CommitHeaderView()
|
||||||
|
private var page: CommitDetailsPage?
|
||||||
|
|
||||||
init(context: AppContext, owner: String, repository: String, sha: String) {
|
init(context: AppContext, owner: String, repository: String, sha: String, branch: String? = nil) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.owner = owner
|
self.owner = owner
|
||||||
self.repository = repository
|
self.repository = repository
|
||||||
self.sha = sha
|
self.sha = sha
|
||||||
|
self.branch = branch
|
||||||
super.init()
|
super.init()
|
||||||
title = "Changed Files"
|
title = "Changed Files"
|
||||||
}
|
}
|
||||||
@@ -423,18 +500,35 @@ final class FilesViewController: RefreshingTableViewController {
|
|||||||
loadContent(refreshing: false)
|
loadContent(refreshing: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewDidLayoutSubviews() {
|
||||||
|
super.viewDidLayoutSubviews()
|
||||||
|
guard let header = tableView.tableHeaderView else { return }
|
||||||
|
let size = header.systemLayoutSizeFitting(
|
||||||
|
CGSize(width: tableView.bounds.width, height: 0),
|
||||||
|
withHorizontalFittingPriority: .required,
|
||||||
|
verticalFittingPriority: .fittingSizeLevel
|
||||||
|
)
|
||||||
|
guard header.frame.height != size.height else { return }
|
||||||
|
header.frame.size.height = size.height
|
||||||
|
tableView.tableHeaderView = header
|
||||||
|
}
|
||||||
|
|
||||||
override func loadContent(refreshing: Bool) {
|
override func loadContent(refreshing: Bool) {
|
||||||
beginLoading(refreshing: refreshing)
|
beginLoading(refreshing: refreshing)
|
||||||
loadingTask?.cancel()
|
loadingTask?.cancel()
|
||||||
loadingTask = Task {
|
loadingTask = Task {
|
||||||
do {
|
do {
|
||||||
rows = try await context.core.commitFiles(
|
let page = try await context.core.commitDetails(
|
||||||
owner: owner,
|
owner: owner,
|
||||||
repository: repository,
|
repository: repository,
|
||||||
sha: sha
|
sha: sha,
|
||||||
|
branch: branch
|
||||||
)
|
)
|
||||||
|
self.page = page
|
||||||
|
commitHeader.configure(page)
|
||||||
|
tableView.tableHeaderView = commitHeader
|
||||||
tableView.reloadData()
|
tableView.reloadData()
|
||||||
tableView.backgroundView = rows.isEmpty
|
tableView.backgroundView = page.files.isEmpty
|
||||||
? EmptyBackgroundView(title: "No changed files", detail: "This commit does not contain file changes.")
|
? EmptyBackgroundView(title: "No changed files", detail: "This commit does not contain file changes.")
|
||||||
: nil
|
: nil
|
||||||
} catch {
|
} catch {
|
||||||
@@ -445,7 +539,7 @@ final class FilesViewController: RefreshingTableViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
rows.count
|
page?.files.count ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(
|
override func tableView(
|
||||||
@@ -454,7 +548,7 @@ final class FilesViewController: RefreshingTableViewController {
|
|||||||
) -> UITableViewCell {
|
) -> UITableViewCell {
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: "file")
|
let cell = tableView.dequeueReusableCell(withIdentifier: "file")
|
||||||
?? UITableViewCell(style: .subtitle, reuseIdentifier: "file")
|
?? UITableViewCell(style: .subtitle, reuseIdentifier: "file")
|
||||||
let row = rows[indexPath.row]
|
let row = page!.files[indexPath.row]
|
||||||
configureTextCell(cell, title: row.path, detail: row.status)
|
configureTextCell(cell, title: row.path, detail: row.status)
|
||||||
cell.accessoryType = .disclosureIndicator
|
cell.accessoryType = .disclosureIndicator
|
||||||
return cell
|
return cell
|
||||||
@@ -462,7 +556,7 @@ final class FilesViewController: RefreshingTableViewController {
|
|||||||
|
|
||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
tableView.deselectRow(at: indexPath, animated: true)
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
let row = rows[indexPath.row]
|
guard let row = page?.files[indexPath.row] else { return }
|
||||||
navigationController?.pushViewController(
|
navigationController?.pushViewController(
|
||||||
DiffViewController(
|
DiffViewController(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -633,7 +727,8 @@ final class RepositoryDirectoryViewController: RefreshingTableViewController {
|
|||||||
context: context,
|
context: context,
|
||||||
owner: owner,
|
owner: owner,
|
||||||
repository: repository,
|
repository: repository,
|
||||||
sha: commit.sha
|
sha: commit.sha,
|
||||||
|
branch: commit.branchLabel
|
||||||
),
|
),
|
||||||
animated: true
|
animated: true
|
||||||
)
|
)
|
||||||
@@ -780,7 +875,8 @@ final class RepositoryHistoryViewController: RefreshingTableViewController {
|
|||||||
context: context,
|
context: context,
|
||||||
owner: owner,
|
owner: owner,
|
||||||
repository: repository,
|
repository: repository,
|
||||||
sha: commit.sha
|
sha: commit.sha,
|
||||||
|
branch: commit.branchLabel
|
||||||
),
|
),
|
||||||
animated: true
|
animated: true
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user