a very good jj gui
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: revset-driven graph visualization backend (TAT-54, TAT-55, TAT-56)

- Add optional revset parameter to get_revisions command
- Use iter_graph() for topological ordering with WC prioritization
- Add edge type classification (direct/indirect/missing)
- Add is_mine field to identify user's own commits
- Add ParentEdge struct with edge type information

+328 -82
+3
Cargo.lock
··· 412 412 checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" 413 413 dependencies = [ 414 414 "iana-time-zone", 415 + "js-sys", 415 416 "num-traits", 416 417 "serde", 418 + "wasm-bindgen", 417 419 "windows-link 0.2.1", 418 420 ] 419 421 ··· 5407 5409 version = "0.1.0" 5408 5410 dependencies = [ 5409 5411 "anyhow", 5412 + "chrono", 5410 5413 "futures", 5411 5414 "hex", 5412 5415 "jj-lib",
+1
apps/desktop/src-tauri/Cargo.toml
··· 33 33 notify = "8.2.0" 34 34 notify-debouncer-mini = "0.7.0" 35 35 tauri-plugin-deep-link = "2" 36 + chrono = "0.4.42" 36 37 37 38 [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] 38 39 tauri-plugin-shell = "2.0"
+2 -2
apps/desktop/src-tauri/src/lib.rs
··· 49 49 } 50 50 51 51 #[tauri::command] 52 - async fn get_revisions(repo_path: String, limit: usize) -> Result<Vec<Revision>, String> { 52 + async fn get_revisions(repo_path: String, limit: usize, revset: Option<String>) -> Result<Vec<Revision>, String> { 53 53 let path = Path::new(&repo_path); 54 - repo::log::fetch_log(path, limit).map_err(|e| format!("Failed to fetch log: {}", e)) 54 + repo::log::fetch_log(path, limit, revset.as_deref()).map_err(|e| format!("Failed to fetch log: {}", e)) 55 55 } 56 56 57 57 #[tauri::command]
+4
apps/desktop/src-tauri/src/repo/jj.rs
··· 168 168 self.workspace.repo_loader() 169 169 } 170 170 171 + pub fn user_settings(&self) -> &UserSettings { 172 + &self.user_settings 173 + } 174 + 171 175 pub fn new_revision(&mut self, parent_change_ids: Vec<String>) -> Result<()> { 172 176 let repo = self.workspace.repo_loader().load_at_head()?; 173 177 let mut tx = repo.start_transaction();
+76 -20
apps/desktop/src-tauri/src/repo/log.rs
··· 1 1 use anyhow::{Context, Result}; 2 2 use jj_lib::backend::CommitId; 3 - use jj_lib::commit::Commit; 3 + use jj_lib::git; 4 + use jj_lib::graph::{GraphEdge, GraphEdgeType}; 4 5 use jj_lib::object_id::ObjectId; 5 6 use jj_lib::repo::Repo; 6 - use jj_lib::revset::RevsetExpression; 7 + use jj_lib::revset::{ 8 + parse, RevsetAliasesMap, RevsetDiagnostics, RevsetExpression, RevsetExtensions, 9 + RevsetParseContext, SymbolResolver, SymbolResolverExtension, 10 + }; 11 + use std::collections::HashMap; 7 12 use std::path::Path; 8 13 9 14 use super::jj::JjRepo; 10 15 11 16 #[derive(Clone, Debug, serde::Serialize)] 17 + pub struct ParentEdge { 18 + pub parent_id: String, 19 + pub edge_type: String, 20 + } 21 + 22 + #[derive(Clone, Debug, serde::Serialize)] 12 23 pub struct Revision { 13 24 pub commit_id: String, 14 25 pub change_id: String, 15 26 pub change_id_short: String, 16 27 pub parent_ids: Vec<String>, 28 + pub parent_edges: Vec<ParentEdge>, 17 29 pub description: String, 18 30 pub author: String, 19 31 pub timestamp: String, 20 32 pub is_working_copy: bool, 21 33 pub is_immutable: bool, 34 + pub is_mine: bool, 22 35 pub bookmarks: Vec<String>, 23 36 } 24 37 25 - pub fn fetch_log(repo_path: &Path, limit: usize) -> Result<Vec<Revision>> { 38 + pub fn fetch_log(repo_path: &Path, limit: usize, revset: Option<&str>) -> Result<Vec<Revision>> { 26 39 let jj_repo = JjRepo::open(repo_path)?; 27 40 let repo = jj_repo.repo_loader().load_at_head()?; 41 + let user_email = jj_repo.user_settings().user_email(); 28 42 29 - // Show all commits reachable from heads (includes WC descendants) 30 43 let wc_id = repo 31 44 .view() 32 45 .wc_commit_ids() 33 46 .values() 34 47 .next() 35 48 .context("No working copy")?; 36 - let revset_expression = RevsetExpression::visible_heads().ancestors(); 37 49 38 - let revset = revset_expression 39 - .evaluate(repo.as_ref()) 40 - .context("Failed to evaluate revset")?; 50 + let revset_expression = if let Some(revset_str) = revset { 51 + // Parse and evaluate custom revset 52 + let context = RevsetParseContext { 53 + aliases_map: &RevsetAliasesMap::default(), 54 + local_variables: HashMap::new(), 55 + user_email: "", 56 + date_pattern_context: chrono::Utc::now().fixed_offset().into(), 57 + default_ignored_remote: Some(git::REMOTE_NAME_FOR_LOCAL_GIT_REPO), 58 + extensions: &RevsetExtensions::default(), 59 + workspace: None, 60 + }; 41 61 42 - let commits: Vec<Commit> = revset 43 - .iter() 62 + let mut diagnostics = RevsetDiagnostics::new(); 63 + let expression = parse(&mut diagnostics, revset_str, &context) 64 + .context("Failed to parse revset")?; 65 + 66 + let symbol_resolver = SymbolResolver::new(repo.as_ref(), &([] as [&Box<dyn SymbolResolverExtension>; 0])); 67 + let resolved = expression.resolve_user_expression(repo.as_ref(), &symbol_resolver) 68 + .context("Failed to resolve revset")?; 69 + 70 + resolved.evaluate(repo.as_ref()) 71 + .context("Failed to evaluate revset")? 72 + } else { 73 + // Default revset: all commits reachable from visible heads 74 + // TODO: Use more sophisticated default: present(@) | ancestors(immutable_heads().., 2) | present(trunk()) 75 + RevsetExpression::visible_heads() 76 + .ancestors() 77 + .evaluate(repo.as_ref()) 78 + .context("Failed to evaluate default revset")? 79 + }; 80 + 81 + // Use iter_graph() to get commits with edge information 82 + let graph_nodes: Vec<(CommitId, Vec<GraphEdge<CommitId>>)> = revset_expression 83 + .iter_graph() 44 84 .take(limit) 45 - .map(|commit_id_result| { 46 - let commit_id = commit_id_result?; 47 - repo.store() 48 - .get_commit(&commit_id) 49 - .context("Failed to get commit") 85 + .map(|result| { 86 + result.map_err(|e| anyhow::anyhow!("Graph iteration error: {}", e)) 50 87 }) 51 88 .collect::<Result<Vec<_>>>()?; 52 89 ··· 56 93 57 94 let mut revisions = Vec::new(); 58 95 59 - for commit in commits { 60 - let commit_id = commit.id(); 96 + for (commit_id, edges) in graph_nodes { 97 + let commit = repo.store().get_commit(&commit_id)?; 61 98 let change_id = commit.change_id(); 62 - let is_working_copy = wc_id == commit_id; 63 - let is_immutable = immutable_ids.contains(commit_id); 99 + let is_working_copy = wc_id == &commit_id; 100 + let is_immutable = immutable_ids.contains(&commit_id); 64 101 65 102 let description = commit.description().to_string(); 66 103 let first_line = description ··· 71 108 72 109 let author = commit.author(); 73 110 let author_name = author.name.clone(); 111 + let author_email = author.email.clone(); 112 + let is_mine = author_email == user_email; 74 113 75 114 // Use committer timestamp (when commit was created/modified) for relative time display 76 115 let committer = commit.committer(); 77 116 let timestamp = format_timestamp(&committer.timestamp, change_id); 78 117 79 - let bookmarks = get_bookmarks_for_commit(repo.as_ref(), commit_id); 118 + let bookmarks = get_bookmarks_for_commit(repo.as_ref(), &commit_id); 80 119 120 + // Keep parent_ids for backward compatibility 81 121 let parent_ids: Vec<String> = commit 82 122 .parent_ids() 83 123 .iter() 84 124 .map(|id| hex::encode(&id.to_bytes()[..6])) 85 125 .collect(); 86 126 127 + // Build parent_edges from graph edges with type information 128 + let parent_edges: Vec<ParentEdge> = edges 129 + .iter() 130 + .map(|edge| ParentEdge { 131 + parent_id: hex::encode(&edge.target.to_bytes()[..6]), 132 + edge_type: match edge.edge_type { 133 + GraphEdgeType::Direct => "direct", 134 + GraphEdgeType::Indirect => "indirect", 135 + GraphEdgeType::Missing => "missing", 136 + } 137 + .to_string(), 138 + }) 139 + .collect(); 140 + 87 141 let full_change_id = format_change_id(change_id); 88 142 let prefix_len = repo 89 143 .shortest_unique_change_id_prefix_len(change_id) ··· 95 149 change_id: full_change_id, 96 150 change_id_short, 97 151 parent_ids, 152 + parent_edges, 98 153 description: first_line, 99 154 author: author_name, 100 155 timestamp, 101 156 is_working_copy, 102 157 is_immutable, 158 + is_mine, 103 159 bookmarks, 104 160 }); 105 161 }
+222 -55
apps/desktop/src/components/RevisionGraph.tsx
··· 27 27 "hsl(340 85% 60%)", // pink 28 28 ]; 29 29 30 + type GraphEdgeType = "direct" | "indirect"; 31 + 32 + interface ParentConnection { 33 + parentRow: number; 34 + parentLane: number; 35 + edgeType: GraphEdgeType; 36 + } 37 + 38 + type GraphNodeType = "revision" | "elided"; 39 + 40 + interface ElidedInfo { 41 + id: string; 42 + childCommitId: string; 43 + parentCommitId: string | null; 44 + } 45 + 30 46 interface GraphNode { 31 - revision: Revision; 47 + type: GraphNodeType; 48 + revision: Revision | null; 49 + elidedInfo: ElidedInfo | null; 32 50 row: number; 33 51 lane: number; 34 - parentConnections: Array<{ parentRow: number; parentLane: number }>; 52 + parentConnections: ParentConnection[]; 53 + } 54 + 55 + interface GraphRow { 56 + type: GraphNodeType; 57 + revision: Revision | null; 58 + elidedInfo: ElidedInfo | null; 35 59 } 36 60 37 61 interface GraphData { 38 62 nodes: GraphNode[]; 39 63 laneCount: number; 40 - orderedRevisions: Revision[]; 64 + rows: GraphRow[]; 41 65 } 42 66 43 67 export function reorderForGraph(revisions: Revision[]): Revision[] { ··· 94 118 } 95 119 96 120 function buildGraph(revisions: Revision[]): GraphData { 97 - if (revisions.length === 0) return { nodes: [], laneCount: 1, orderedRevisions: [] }; 121 + if (revisions.length === 0) return { nodes: [], laneCount: 1, rows: [] }; 98 122 99 123 // Reorder: heads first (children before parents), working copy branch prioritized 100 124 const orderedRevisions = reorderForGraph(revisions); 101 125 126 + // Build set of visible commit IDs for edge type detection 127 + const visibleCommitIds = new Set(orderedRevisions.map((r) => r.commit_id)); 128 + 129 + // First pass: build rows array with elided placeholders inserted 130 + const rows: GraphRow[] = []; 131 + const elidedIds = new Set<string>(); 132 + 133 + for (let i = 0; i < orderedRevisions.length; i++) { 134 + const rev = orderedRevisions[i]; 135 + rows.push({ type: "revision", revision: rev, elidedInfo: null }); 136 + 137 + // Check if any parent is not visible (indirect edge) - insert elided placeholder 138 + for (const parentId of rev.parent_ids) { 139 + if (!visibleCommitIds.has(parentId)) { 140 + const elidedId = `elided-${rev.commit_id}-${parentId}`; 141 + if (!elidedIds.has(elidedId)) { 142 + elidedIds.add(elidedId); 143 + rows.push({ 144 + type: "elided", 145 + revision: null, 146 + elidedInfo: { 147 + id: elidedId, 148 + childCommitId: rev.commit_id, 149 + parentCommitId: parentId, 150 + }, 151 + }); 152 + } 153 + } 154 + } 155 + } 156 + 157 + // Build row index maps 102 158 const commitToRow = new Map<string, number>(); 159 + const elidedToRow = new Map<string, number>(); 160 + rows.forEach((row, idx) => { 161 + if (row.type === "revision" && row.revision) { 162 + commitToRow.set(row.revision.commit_id, idx); 163 + } else if (row.type === "elided" && row.elidedInfo) { 164 + elidedToRow.set(row.elidedInfo.id, idx); 165 + } 166 + }); 167 + 103 168 const commitToLane = new Map<string, number>(); 169 + const elidedToLane = new Map<string, number>(); 104 170 const nodes: GraphNode[] = []; 105 - 106 - orderedRevisions.forEach((rev, idx) => commitToRow.set(rev.commit_id, idx)); 107 - 108 171 const activeLanes: (string | null)[] = [null]; 109 172 110 - function claimLane(commitId: string, preferredLane?: number): number { 173 + function claimLane(id: string, preferredLane?: number): number { 111 174 if ( 112 175 preferredLane !== undefined && 113 176 preferredLane < activeLanes.length && 114 177 activeLanes[preferredLane] === null 115 178 ) { 116 - activeLanes[preferredLane] = commitId; 179 + activeLanes[preferredLane] = id; 117 180 return preferredLane; 118 181 } 119 182 for (let i = 0; i < activeLanes.length; i++) { 120 183 if (activeLanes[i] === null) { 121 - activeLanes[i] = commitId; 184 + activeLanes[i] = id; 122 185 return i; 123 186 } 124 187 } 125 188 if (activeLanes.length < MAX_LANES) { 126 - activeLanes.push(commitId); 189 + activeLanes.push(id); 127 190 return activeLanes.length - 1; 128 191 } 129 192 return MAX_LANES - 1; 130 193 } 131 194 132 - for (let row = 0; row < orderedRevisions.length; row++) { 133 - const revision = orderedRevisions[row]; 195 + for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) { 196 + const row = rows[rowIdx]; 197 + 198 + if (row.type === "revision" && row.revision) { 199 + const revision = row.revision; 200 + 201 + let lane = commitToLane.get(revision.commit_id); 202 + if (lane === undefined) { 203 + const preferLane = rowIdx === 0 ? 0 : undefined; 204 + lane = claimLane(revision.commit_id, preferLane); 205 + commitToLane.set(revision.commit_id, lane); 206 + } else { 207 + activeLanes[lane] = revision.commit_id; 208 + } 134 209 135 - let lane = commitToLane.get(revision.commit_id); 136 - if (lane === undefined) { 137 - // First branch gets lane 0 (working copy's branch due to our ordering) 138 - const preferLane = row === 0 ? 0 : undefined; 139 - lane = claimLane(revision.commit_id, preferLane); 140 - commitToLane.set(revision.commit_id, lane); 141 - } else { 142 - activeLanes[lane] = revision.commit_id; 143 - } 210 + const parentConnections: ParentConnection[] = []; 144 211 145 - const parentConnections: GraphNode["parentConnections"] = []; 146 - const parentRows = revision.parent_ids 147 - .map((pid) => ({ id: pid, row: commitToRow.get(pid) })) 148 - .filter((p): p is { id: string; row: number } => p.row !== undefined); 212 + for (let i = 0; i < revision.parent_ids.length; i++) { 213 + const parentId = revision.parent_ids[i]; 214 + const isVisible = visibleCommitIds.has(parentId); 149 215 150 - for (let i = 0; i < parentRows.length; i++) { 151 - const { id: parentId, row: parentRow } = parentRows[i]; 216 + if (isVisible) { 217 + // Direct edge to visible parent 218 + const parentRow = commitToRow.get(parentId); 219 + if (parentRow !== undefined) { 220 + let parentLane = commitToLane.get(parentId); 221 + if (parentLane === undefined) { 222 + parentLane = i === 0 ? lane : claimLane(parentId); 223 + commitToLane.set(parentId, parentLane); 224 + } 225 + parentConnections.push({ parentRow, parentLane, edgeType: "direct" }); 226 + } 227 + } else { 228 + // Indirect edge - connect to elided placeholder 229 + const elidedId = `elided-${revision.commit_id}-${parentId}`; 230 + const elidedRow = elidedToRow.get(elidedId); 231 + if (elidedRow !== undefined) { 232 + let elidedLane = elidedToLane.get(elidedId); 233 + if (elidedLane === undefined) { 234 + elidedLane = lane; // Elided node stays in same lane as child 235 + elidedToLane.set(elidedId, elidedLane); 236 + } 237 + parentConnections.push({ parentRow: elidedRow, parentLane: elidedLane, edgeType: "indirect" }); 238 + } 239 + } 240 + } 152 241 153 - let parentLane = commitToLane.get(parentId); 154 - if (parentLane === undefined) { 155 - parentLane = i === 0 ? lane : claimLane(parentId); 156 - commitToLane.set(parentId, parentLane); 242 + if (revision.parent_ids.length === 0 && lane < activeLanes.length) { 243 + activeLanes[lane] = null; 157 244 } 158 245 159 - parentConnections.push({ parentRow, parentLane }); 160 - } 246 + nodes.push({ 247 + type: "revision", 248 + revision, 249 + elidedInfo: null, 250 + row: rowIdx, 251 + lane, 252 + parentConnections, 253 + }); 254 + } else if (row.type === "elided" && row.elidedInfo) { 255 + const elidedInfo = row.elidedInfo; 256 + let lane = elidedToLane.get(elidedInfo.id); 257 + if (lane === undefined) { 258 + // Get lane from child 259 + const childLane = commitToLane.get(elidedInfo.childCommitId); 260 + lane = childLane ?? claimLane(elidedInfo.id); 261 + elidedToLane.set(elidedInfo.id, lane); 262 + } 161 263 162 - if (parentRows.length === 0 && lane < activeLanes.length) { 163 - activeLanes[lane] = null; 264 + nodes.push({ 265 + type: "elided", 266 + revision: null, 267 + elidedInfo, 268 + row: rowIdx, 269 + lane, 270 + parentConnections: [], // Elided nodes don't have parent connections in the graph 271 + }); 164 272 } 165 - 166 - nodes.push({ revision, row, lane, parentConnections }); 167 273 } 168 274 169 - return { nodes, laneCount: Math.min(activeLanes.length, MAX_LANES), orderedRevisions }; 275 + return { nodes, laneCount: Math.min(activeLanes.length, MAX_LANES), rows }; 170 276 } 171 277 172 278 function laneToX(lane: number): number { ··· 186 292 <title>Revision graph</title> 187 293 {/* Edges */} 188 294 {nodes.map((node) => { 295 + if (node.type === "elided") return null; 296 + 189 297 const y = node.row * ROW_HEIGHT + ROW_HEIGHT / 2; 190 298 const x = laneToX(node.lane); 191 299 const color = laneColor(node.lane); 300 + const nodeKey = node.revision?.change_id ?? node.elidedInfo?.id ?? `node-${node.row}`; 192 301 193 302 return ( 194 - <g key={`edges-${node.revision.change_id}`}> 303 + <g key={`edges-${nodeKey}`}> 195 304 {node.parentConnections.map((conn, idx) => { 196 305 const parentY = conn.parentRow * ROW_HEIGHT + ROW_HEIGHT / 2; 197 306 const parentX = laneToX(conn.parentLane); 198 307 const edgeColor = laneColor(conn.parentLane); 308 + const isDashed = conn.edgeType === "indirect"; 199 309 200 310 if (node.lane === conn.parentLane) { 201 311 return ( ··· 207 317 y2={parentY - NODE_RADIUS} 208 318 stroke={color} 209 319 strokeWidth={2} 320 + strokeDasharray={isDashed ? "4 4" : undefined} 210 321 /> 211 322 ); 212 323 } ··· 222 333 stroke={edgeColor} 223 334 strokeWidth={2} 224 335 strokeOpacity={0.8} 336 + strokeDasharray={isDashed ? "4 4" : undefined} 225 337 /> 226 338 ); 227 339 })} ··· 233 345 {nodes.map((node) => { 234 346 const y = node.row * ROW_HEIGHT + ROW_HEIGHT / 2; 235 347 const x = laneToX(node.lane); 236 - const isWorkingCopy = node.revision.is_working_copy; 237 348 const color = laneColor(node.lane); 238 349 350 + // Elided placeholder node - render ~ symbol 351 + if (node.type === "elided") { 352 + return ( 353 + <g key={node.elidedInfo?.id ?? `elided-${node.row}`}> 354 + <text 355 + x={x} 356 + y={y} 357 + textAnchor="middle" 358 + dominantBaseline="central" 359 + fill={color} 360 + fontWeight="bold" 361 + fontSize="14" 362 + opacity={0.7} 363 + > 364 + ~ 365 + </text> 366 + </g> 367 + ); 368 + } 369 + 370 + const isWorkingCopy = node.revision?.is_working_copy ?? false; 371 + const isImmutable = node.revision?.is_immutable ?? false; 372 + 239 373 if (isWorkingCopy) { 240 374 return ( 241 - <g key={node.revision.change_id}> 375 + <g key={node.revision?.change_id}> 242 376 <circle cx={x} cy={y} r={NODE_RADIUS + 3} fill={color} fillOpacity={0.2} /> 243 377 <text 244 378 x={x} ··· 255 389 ); 256 390 } 257 391 258 - return <circle key={node.revision.change_id} cx={x} cy={y} r={NODE_RADIUS} fill={color} />; 392 + // Immutable commits get a diamond shape (◆) 393 + if (isImmutable) { 394 + return ( 395 + <g key={node.revision?.change_id}> 396 + <rect 397 + x={x - NODE_RADIUS} 398 + y={y - NODE_RADIUS} 399 + width={NODE_RADIUS * 2} 400 + height={NODE_RADIUS * 2} 401 + fill={color} 402 + transform={`rotate(45 ${x} ${y})`} 403 + /> 404 + </g> 405 + ); 406 + } 407 + 408 + return <circle key={node.revision?.change_id} cx={x} cy={y} r={NODE_RADIUS} fill={color} />; 259 409 })} 260 410 </svg> 261 411 ); ··· 309 459 ); 310 460 }); 311 461 462 + const ElidedRow = memo(function ElidedRow(_props: { elidedInfo: ElidedInfo }) { 463 + return ( 464 + <div 465 + style={{ height: ROW_HEIGHT }} 466 + className="flex items-center px-2 text-muted-foreground text-xs opacity-60" 467 + > 468 + <span className="font-mono">▸ commits elided</span> 469 + </div> 470 + ); 471 + }); 472 + 312 473 export function RevisionGraph({ 313 474 revisions, 314 475 selectedRevision, ··· 316 477 isLoading, 317 478 flash, 318 479 }: RevisionGraphProps) { 319 - const { nodes, laneCount, orderedRevisions } = useMemo(() => buildGraph(revisions), [revisions]); 480 + const { nodes, laneCount, rows } = useMemo(() => buildGraph(revisions), [revisions]); 320 481 321 482 const revisionMap = useMemo(() => new Map(revisions.map((r) => [r.change_id, r])), [revisions]); 322 483 ··· 341 502 <div className="flex"> 342 503 <GraphColumn nodes={nodes} laneCount={laneCount} /> 343 504 <div className="flex-1 min-w-0"> 344 - {orderedRevisions.map((revision) => { 345 - const isFlashing = flash?.changeId === revision.change_id; 346 - return ( 347 - <RevisionRow 348 - key={revision.change_id} 349 - revision={revision} 350 - isSelected={selectedRevision?.change_id === revision.change_id} 351 - onSelect={handleSelect} 352 - isFlashing={isFlashing} 353 - /> 354 - ); 505 + {rows.map((row) => { 506 + if (row.type === "revision" && row.revision) { 507 + const isFlashing = flash?.changeId === row.revision.change_id; 508 + return ( 509 + <RevisionRow 510 + key={row.revision.change_id} 511 + revision={row.revision} 512 + isSelected={selectedRevision?.change_id === row.revision.change_id} 513 + onSelect={handleSelect} 514 + isFlashing={isFlashing} 515 + /> 516 + ); 517 + } 518 + if (row.type === "elided" && row.elidedInfo) { 519 + return <ElidedRow key={row.elidedInfo.id} elidedInfo={row.elidedInfo} />; 520 + } 521 + return null; 355 522 })} 356 523 </div> 357 524 </div>
+11
apps/desktop/src/schemas.ts
··· 1 1 import { Schema } from "effect"; 2 2 3 + export const GraphEdgeType = Schema.Literal("direct", "indirect", "missing"); 4 + export type GraphEdgeType = typeof GraphEdgeType.Type; 5 + 6 + export const ParentEdge = Schema.Struct({ 7 + parent_id: Schema.String, 8 + edge_type: GraphEdgeType, 9 + }); 10 + export type ParentEdge = typeof ParentEdge.Type; 11 + 3 12 export const Revision = Schema.Struct({ 4 13 commit_id: Schema.String, 5 14 change_id: Schema.String, 6 15 change_id_short: Schema.String, 7 16 parent_ids: Schema.Array(Schema.String), 17 + parent_edges: Schema.Array(ParentEdge), 8 18 description: Schema.String, 9 19 author: Schema.String, 10 20 timestamp: Schema.String, 11 21 is_working_copy: Schema.Boolean, 12 22 is_immutable: Schema.Boolean, 23 + is_mine: Schema.Boolean, 13 24 bookmarks: Schema.Array(Schema.String), 14 25 }); 15 26 export type Revision = typeof Revision.Type;
+9 -5
apps/desktop/src/tauri-commands.ts
··· 1 1 import { invoke } from "@tauri-apps/api/core"; 2 2 3 3 export type { 4 - Revision, 5 4 ChangedFile, 6 - WorkingCopyStatus, 7 - DiffLine, 8 5 DiffHunk, 6 + DiffLine, 9 7 FileDiff, 10 8 Project, 9 + Revision, 10 + WorkingCopyStatus, 11 11 } from "./schemas"; 12 12 13 13 import type { FileDiff, Project, Revision, WorkingCopyStatus } from "./schemas"; ··· 16 16 return invoke<string | null>("find_repository", { startPath }); 17 17 } 18 18 19 - export async function getRevisions(repoPath: string, limit: number): Promise<Revision[]> { 20 - return invoke<Revision[]>("get_revisions", { repoPath, limit }); 19 + export async function getRevisions( 20 + repoPath: string, 21 + limit: number, 22 + revset?: string, 23 + ): Promise<Revision[]> { 24 + return invoke<Revision[]>("get_revisions", { repoPath, limit, revset }); 21 25 } 22 26 23 27 export async function getStatus(repoPath: string): Promise<WorkingCopyStatus> {