a very good jj gui
0
fork

Configure Feed

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

feat: add divergent detection and revset resolution

+148 -1
+148 -1
apps/desktop/src-tauri/src/repo/log.rs
··· 34 34 pub is_immutable: bool, 35 35 pub is_mine: bool, 36 36 pub is_trunk: bool, 37 + pub is_divergent: bool, 38 + pub divergent_index: Option<usize>, 37 39 pub bookmarks: Vec<String>, 38 40 } 39 41 ··· 152 154 } 153 155 }; 154 156 157 + // First pass: count occurrences of each change_id to detect divergence 158 + let mut change_id_counts: HashMap<String, usize> = HashMap::new(); 159 + let mut commit_change_ids: Vec<String> = Vec::new(); 160 + 161 + for (commit_id, _) in &graph_nodes { 162 + let commit = repo.store().get_commit(commit_id)?; 163 + let change_id_str = format_change_id(commit.change_id()); 164 + *change_id_counts.entry(change_id_str.clone()).or_insert(0) += 1; 165 + commit_change_ids.push(change_id_str); 166 + } 167 + 168 + // Track which index we're at for each divergent change_id 169 + let mut divergent_indices: HashMap<String, usize> = HashMap::new(); 170 + 155 171 let mut revisions = Vec::new(); 156 172 157 173 for (commit_id, edges) in graph_nodes { ··· 198 214 let prefix_len = repo 199 215 .shortest_unique_change_id_prefix_len(change_id) 200 216 .unwrap_or(full_change_id.len()); 201 - let change_id_short = full_change_id[..prefix_len].to_string(); 217 + 218 + // Check if this change_id is divergent (appears multiple times) 219 + let count = change_id_counts.get(&full_change_id).copied().unwrap_or(1); 220 + let is_divergent = count > 1; 221 + let divergent_index = if is_divergent { 222 + let idx = divergent_indices.entry(full_change_id.clone()).or_insert(0); 223 + let current_idx = *idx; 224 + *idx += 1; 225 + Some(current_idx) 226 + } else { 227 + None 228 + }; 229 + 230 + // Add /N suffix for divergent changes 231 + let change_id_short = if let Some(div_idx) = divergent_index { 232 + format!("{}/{}", &full_change_id[..prefix_len], div_idx) 233 + } else { 234 + full_change_id[..prefix_len].to_string() 235 + }; 202 236 203 237 let is_trunk = trunk_ancestor_ids.contains(&commit_id); 204 238 ··· 215 249 is_immutable, 216 250 is_mine, 217 251 is_trunk, 252 + is_divergent, 253 + divergent_index, 218 254 bookmarks, 219 255 }); 220 256 } ··· 281 317 282 318 bookmarks 283 319 } 320 + 321 + /// Result of resolving a revset expression 322 + #[derive(Clone, Debug, serde::Serialize)] 323 + pub struct RevsetResult { 324 + pub change_ids: Vec<String>, 325 + pub error: Option<String>, 326 + } 327 + 328 + /// Resolve a revset expression and return matching change IDs 329 + pub fn resolve_revset(repo_path: &Path, revset_str: &str) -> Result<RevsetResult> { 330 + let jj_repo = JjRepo::open(repo_path)?; 331 + let repo = jj_repo.repo_loader().load_at_head()?; 332 + 333 + // Set up aliases (same as fetch_log) 334 + let mut aliases_map = RevsetAliasesMap::new(); 335 + let user_email = jj_repo.user_settings().user_email(); 336 + 337 + aliases_map.insert( 338 + "trunk()", 339 + r#"latest( 340 + remote_bookmarks(exact:"main", exact:"origin") | 341 + remote_bookmarks(exact:"master", exact:"origin") | 342 + remote_bookmarks(exact:"trunk", exact:"origin") | 343 + root() 344 + )"#, 345 + ).ok(); 346 + 347 + aliases_map.insert("builtin_immutable_heads()", "present(trunk()) | tags() | untracked_remote_bookmarks()").ok(); 348 + aliases_map.insert("immutable_heads()", "builtin_immutable_heads()").ok(); 349 + 350 + let mine_revset = format!(r#"author_email(exact-i:"{}")"#, user_email); 351 + aliases_map.insert("mine()", &mine_revset).ok(); 352 + 353 + let path_converter = RepoPathUiConverter::Fs { 354 + cwd: repo_path.to_path_buf(), 355 + base: repo_path.to_path_buf(), 356 + }; 357 + let workspace_name = jj_repo.workspace_name(); 358 + let workspace_ctx = RevsetWorkspaceContext { 359 + path_converter: &path_converter, 360 + workspace_name, 361 + }; 362 + 363 + let context = RevsetParseContext { 364 + aliases_map: &aliases_map, 365 + local_variables: HashMap::new(), 366 + user_email: jj_repo.user_settings().user_email(), 367 + date_pattern_context: chrono::Utc::now().fixed_offset().into(), 368 + default_ignored_remote: Some(git::REMOTE_NAME_FOR_LOCAL_GIT_REPO), 369 + extensions: &RevsetExtensions::default(), 370 + workspace: Some(workspace_ctx), 371 + }; 372 + 373 + let mut diagnostics = RevsetDiagnostics::new(); 374 + 375 + // Parse the revset expression 376 + let expression = match parse(&mut diagnostics, revset_str, &context) { 377 + Ok(expr) => expr, 378 + Err(e) => { 379 + return Ok(RevsetResult { 380 + change_ids: vec![], 381 + error: Some(format!("Parse error: {}", e)), 382 + }); 383 + } 384 + }; 385 + 386 + // Resolve symbols 387 + let symbol_resolver = SymbolResolver::new(repo.as_ref(), &([] as [&Box<dyn SymbolResolverExtension>; 0])); 388 + let resolved = match expression.resolve_user_expression(repo.as_ref(), &symbol_resolver) { 389 + Ok(r) => r, 390 + Err(e) => { 391 + return Ok(RevsetResult { 392 + change_ids: vec![], 393 + error: Some(format!("Resolve error: {}", e)), 394 + }); 395 + } 396 + }; 397 + 398 + // Evaluate the revset 399 + let revset = match resolved.evaluate(repo.as_ref()) { 400 + Ok(r) => r, 401 + Err(e) => { 402 + return Ok(RevsetResult { 403 + change_ids: vec![], 404 + error: Some(format!("Evaluation error: {}", e)), 405 + }); 406 + } 407 + }; 408 + 409 + // Collect matching change IDs 410 + let mut change_ids = Vec::new(); 411 + for commit_id_result in revset.iter() { 412 + match commit_id_result { 413 + Ok(commit_id) => { 414 + let commit = repo.store().get_commit(&commit_id)?; 415 + change_ids.push(format_change_id(commit.change_id())); 416 + } 417 + Err(e) => { 418 + return Ok(RevsetResult { 419 + change_ids: vec![], 420 + error: Some(format!("Iteration error: {}", e)), 421 + }); 422 + } 423 + } 424 + } 425 + 426 + Ok(RevsetResult { 427 + change_ids, 428 + error: None, 429 + }) 430 + }