diff --git a/crates/radicle-tauri/src/commands/repo.rs b/crates/radicle-tauri/src/commands/repo.rs index 7527382..054037b 100644 --- a/crates/radicle-tauri/src/commands/repo.rs +++ b/crates/radicle-tauri/src/commands/repo.rs @@ -42,9 +42,8 @@ pub async fn diff_stats( pub async fn list_commits( ctx: tauri::State<'_, AppState>, rid: RepoId, - parent: Option, - skip: Option, - take: Option, -) -> Result>, Error> { - ctx.list_commits(rid, parent, skip, take) + base: String, + head: String, +) -> Result, Error> { + ctx.list_commits(rid, base, head) } diff --git a/crates/radicle-types/src/traits/repo.rs b/crates/radicle-types/src/traits/repo.rs index e45e8c9..fe280ce 100644 --- a/crates/radicle-types/src/traits/repo.rs +++ b/crates/radicle-types/src/traits/repo.rs @@ -210,158 +210,26 @@ pub trait Repo: Profile { fn list_commits( &self, rid: identity::RepoId, - parent: Option, - skip: Option, - take: Option, - ) -> Result>, Error> { + base: String, + head: String, + ) -> Result, Error> { let profile = self.profile(); - let cursor = skip.unwrap_or(0); - let take = take.unwrap_or(20); let repo = profile.storage.repository(rid)?; - let sha = match parent { - Some(commit) => commit, - None => repo.head()?.1.to_string(), - }; - let repo = surf::Repository::open(repo.path())?; - let history = repo.history(&sha)?; - - let mut commits = history + let history = repo.history(&head)?; + + let commits = history + .take_while(|c| { + if let Ok(c) = c { + c.id.to_string() != base + } else { + false + } + }) .filter_map(|c| c.map(Into::into).ok()) - .skip(cursor) - .take(take + 1); // Take one extra item to check if there's more. - - let paginated_commits: Vec<_> = commits.by_ref().take(take).collect(); - let more = commits.next().is_some(); - - Ok::<_, Error>(cobs::PaginatedQuery { - cursor, - more, - content: paginated_commits.to_vec(), - }) - } -} - -#[cfg(test)] -#[allow(clippy::unwrap_used)] -mod test { - use std::str::FromStr; - use std::vec; - - use radicle::crypto::test::signer::MockSigner; - use radicle::{git, test}; - use radicle_surf::Author; - - use crate::cobs; - use crate::repo; - use crate::traits::repo::Repo; - use crate::AppState; - - #[test] - fn list_commits_pagination() { - let signer = MockSigner::from_seed([0xff; 32]); - let tempdir = tempfile::tempdir().unwrap(); - let profile = crate::test::profile(tempdir.path(), [0xff; 32]); - let (rid, _, _, _) = - test::fixtures::project(tempdir.path().join("original"), &profile.storage, &signer) - .unwrap(); - let state = AppState { profile }; - let commits = Repo::list_commits(&state, rid, None, None, Some(1)).unwrap(); - - assert_eq!( - commits, - cobs::PaginatedQuery { - cursor: 0, - more: true, - content: vec![repo::Commit { - id: git::Oid::from_str("f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354").unwrap(), - author: Author { - name: "anonymous".to_string(), - email: "anonymous@radicle.xyz".to_string(), - time: radicle::git::raw::Time::new(1514817556, 0).into(), - }, - committer: Author { - name: "anonymous".to_string(), - email: "anonymous@radicle.xyz".to_string(), - time: radicle::git::raw::Time::new(1514817556, 0).into(), - }, - message: "Second commit".to_string(), - summary: "Second commit".to_string(), - parents: vec![ - git::Oid::from_str("08c788dd1be6315de09e3fe09b5b1b7a2b8711d9").unwrap() - ], - }], - } - ); - - let commits = Repo::list_commits(&state, rid, None, Some(1), None).unwrap(); - - assert_eq!( - commits, - cobs::PaginatedQuery { - cursor: 1, - more: false, - content: vec![repo::Commit { - id: git::Oid::from_str("08c788dd1be6315de09e3fe09b5b1b7a2b8711d9").unwrap(), - author: Author { - name: "anonymous".to_string(), - email: "anonymous@radicle.xyz".to_string(), - time: radicle::git::raw::Time::new(1514817556, 0).into(), - }, - committer: Author { - name: "anonymous".to_string(), - email: "anonymous@radicle.xyz".to_string(), - time: radicle::git::raw::Time::new(1514817556, 0).into(), - }, - message: "Initial commit".to_string(), - summary: "Initial commit".to_string(), - parents: vec![], - }], - } - ); - } + .collect(); - #[test] - fn list_commits_with_head() { - let signer = MockSigner::from_seed([0xff; 32]); - let tempdir = tempfile::tempdir().unwrap(); - let profile = crate::test::profile(tempdir.path(), [0xff; 32]); - let (rid, _, _, _) = - test::fixtures::project(tempdir.path().join("original"), &profile.storage, &signer) - .unwrap(); - let state = AppState { profile }; - let commits = Repo::list_commits( - &state, - rid, - Some("08c788dd1be6315de09e3fe09b5b1b7a2b8711d9".to_string()), - None, - None, - ) - .unwrap(); - - assert_eq!( - commits, - cobs::PaginatedQuery { - cursor: 0, - more: false, - content: vec![repo::Commit { - id: git::Oid::from_str("08c788dd1be6315de09e3fe09b5b1b7a2b8711d9").unwrap(), - author: Author { - name: "anonymous".to_string(), - email: "anonymous@radicle.xyz".to_string(), - time: radicle::git::raw::Time::new(1514817556, 0).into(), - }, - committer: Author { - name: "anonymous".to_string(), - email: "anonymous@radicle.xyz".to_string(), - time: radicle::git::raw::Time::new(1514817556, 0).into(), - }, - message: "Initial commit".to_string(), - summary: "Initial commit".to_string(), - parents: vec![], - }], - } - ); + Ok(commits) } } diff --git a/package-lock.json b/package-lock.json index 334af3f..79f38a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@tauri-apps/cli": "^2.1.0", "@tsconfig/svelte": "^5.0.4", "@types/lodash": "^4.17.13", + "@types/md5": "^2.3.5", "@types/node": "^22.10.2", "@types/wait-on": "^5.3.4", "@wooorm/starry-night": "^3.5.0", @@ -46,6 +47,7 @@ "marked-footnote": "^1.2.4", "marked-katex-extension": "^5.1.3", "marked-linkify-it": "^3.1.12", + "md5": "^2.3.0", "prettier": "^3.4.2", "prettier-plugin-svelte": "^3.3.2", "svelte": "^5.14.0", @@ -1373,6 +1375,13 @@ "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==", "dev": true }, + "node_modules/@types/md5": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.5.tgz", + "integrity": "sha512-/i42wjYNgE6wf0j2bcTX6kuowmdL/6PE4IVitMpm2eYKBUuYCprdcWVK+xEF0gcV6ufMCRhtxmReGfc6hIK7Jw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.10.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", @@ -1972,6 +1981,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -2056,6 +2075,16 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2867,6 +2896,13 @@ "node": ">=0.8.19" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true, + "license": "MIT" + }, "node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -3197,6 +3233,18 @@ "marked": ">=4 <16" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", diff --git a/package.json b/package.json index 65458c7..fa365db 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@tauri-apps/cli": "^2.1.0", "@tsconfig/svelte": "^5.0.4", "@types/lodash": "^4.17.13", + "@types/md5": "^2.3.5", "@types/node": "^22.10.2", "@types/wait-on": "^5.3.4", "@wooorm/starry-night": "^3.5.0", @@ -61,6 +62,7 @@ "marked-footnote": "^1.2.4", "marked-katex-extension": "^5.1.3", "marked-linkify-it": "^3.1.12", + "md5": "^2.3.0", "prettier": "^3.4.2", "prettier-plugin-svelte": "^3.3.2", "svelte": "^5.14.0", diff --git a/src/components/CobCommitTeaser.svelte b/src/components/CobCommitTeaser.svelte new file mode 100644 index 0000000..4734224 --- /dev/null +++ b/src/components/CobCommitTeaser.svelte @@ -0,0 +1,84 @@ + + + + +
+
+
+
+ +
+ {#if commit.message.trim() !== commit.summary.trim()} +
+ { + commitMessageVisible = !commitMessageVisible; + }}> + + +
+ {/if} +
+ {#if commitMessageVisible} +
+
{commit.message.replace(commit.summary, "").trim()}
+
+ {/if} +
+
+
+ + + +
+
+
diff --git a/src/components/CommitsContainer.svelte b/src/components/CommitsContainer.svelte new file mode 100644 index 0000000..a78b201 --- /dev/null +++ b/src/components/CommitsContainer.svelte @@ -0,0 +1,69 @@ + + + + + +
+
+ { + expanded = !expanded; + }}> + + + {@render leftHeader()} +
+
+ + {#if expanded} +
+
+ {@render children()} +
+ {/if} +
diff --git a/src/components/CompactCommitAuthorship.svelte b/src/components/CompactCommitAuthorship.svelte new file mode 100644 index 0000000..f2cb2de --- /dev/null +++ b/src/components/CompactCommitAuthorship.svelte @@ -0,0 +1,96 @@ + + + + +
+ +
+ {#if commit.author.email === commit.committer.email} + avatar + {:else} + avatar + avatar + {/if} +
+ +
+ {#if commit.author.email === commit.committer.email} +
+
Author
+ avatar + {commit.author.name} +
+ {:else} +
+
Author
+ avatar + {commit.author.name} +
+
+
Committer
+ avatar + {commit.committer.name} +
+ {/if} +
+
+ +
+ {utils.formatTimestamp(commit.committer.time * 1000)} +
+
diff --git a/src/components/File.svelte b/src/components/File.svelte index fb9918a..6d67332 100644 --- a/src/components/File.svelte +++ b/src/components/File.svelte @@ -1,7 +1,5 @@ + + + +
+
setVisible(true)} + on:mouseleave={() => setVisible(false)}> + + + {#if visible} +
+
+ +
+
+ {/if} +
+
diff --git a/src/components/Icon.svelte b/src/components/Icon.svelte index 5752d5b..330be02 100644 --- a/src/components/Icon.svelte +++ b/src/components/Icon.svelte @@ -29,6 +29,7 @@ | "dashboard" | "delegate" | "diff" + | "ellipsis" | "expand" | "expand-panel" | "eye" @@ -432,6 +433,10 @@ + {:else if name === "ellipsis"} + + + {:else if name === "expand"} diff --git a/src/components/Revision.svelte b/src/components/Revision.svelte index 3ea37da..00f99fa 100644 --- a/src/components/Revision.svelte +++ b/src/components/Revision.svelte @@ -1,5 +1,6 @@
@@ -372,7 +410,36 @@
{/if} +
+ {#await loadCommits(rid, revision.base, revision.head) then commits} +
+ + {#snippet leftHeader()} +
Commits
+ {/snippet} + {#snippet children()} +
+
+ +
base
+
+
+ {#each commits.reverse() as commit} +
+
+ +
+ {/each} +
+
+ {/snippet} +
+
+ {/await} + {#await loadHighlightedDiff(rid, revision.base, revision.head)} Loading… {:then diff} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 5319c9b..24bf347 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -6,6 +6,7 @@ import type { Patch } from "@bindings/cob/patch/Patch"; import bs58 from "bs58"; import twemojiModule from "twemoji"; +import md5 from "md5"; import NodeId from "@app/components/NodeId.svelte"; @@ -211,3 +212,11 @@ export function parseNodeId( return undefined; } + +// Get the gravatar URL of an email. +export function gravatarURL(email: string): string { + const address = email.trim().toLowerCase(); + const hash = md5(address); + + return `https://www.gravatar.com/avatar/${hash}`; +}