a very good jj gui
0
fork

Configure Feed

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

feat(backend): add is_trunk field to detect trunk ancestors

+102 -28
+8
apps/desktop/src-tauri/src/repo/jj.rs
··· 172 172 &self.user_settings 173 173 } 174 174 175 + pub fn workspace_name(&self) -> &jj_lib::ref_name::WorkspaceName { 176 + self.workspace.workspace_name() 177 + } 178 + 179 + pub fn workspace_root(&self) -> &Path { 180 + self.workspace.workspace_root() 181 + } 182 + 175 183 pub fn new_revision(&mut self, parent_change_ids: Vec<String>) -> Result<()> { 176 184 let repo = self.workspace.repo_loader().load_at_head()?; 177 185 let mut tx = repo.start_transaction();
+92 -28
apps/desktop/src-tauri/src/repo/log.rs
··· 4 4 use jj_lib::graph::{GraphEdge, GraphEdgeType}; 5 5 use jj_lib::object_id::ObjectId; 6 6 use jj_lib::repo::Repo; 7 + use jj_lib::repo_path::RepoPathUiConverter; 7 8 use jj_lib::revset::{ 8 9 parse, RevsetAliasesMap, RevsetDiagnostics, RevsetExpression, RevsetExtensions, 9 - RevsetParseContext, SymbolResolver, SymbolResolverExtension, 10 + RevsetParseContext, RevsetWorkspaceContext, SymbolResolver, SymbolResolverExtension, 10 11 }; 11 12 use std::collections::HashMap; 12 13 use std::path::Path; ··· 32 33 pub is_working_copy: bool, 33 34 pub is_immutable: bool, 34 35 pub is_mine: bool, 36 + pub is_trunk: bool, 35 37 pub bookmarks: Vec<String>, 36 38 } 37 39 38 - pub fn fetch_log(repo_path: &Path, limit: usize, revset: Option<&str>) -> Result<Vec<Revision>> { 40 + pub fn fetch_log(repo_path: &Path, limit: usize, revset: Option<&str>, preset: Option<&str>) -> Result<Vec<Revision>> { 39 41 let jj_repo = JjRepo::open(repo_path)?; 40 42 let repo = jj_repo.repo_loader().load_at_head()?; 41 43 let user_email = jj_repo.user_settings().user_email(); ··· 47 49 .next() 48 50 .context("No working copy")?; 49 51 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 - }; 52 + // Determine which revset to use 53 + let revset_str = if let Some(custom_revset) = revset { 54 + custom_revset 55 + } else if let Some(preset_name) = preset { 56 + match preset_name { 57 + "my_work" => "mine() | present(@)", 58 + "active" => "present(@) | ancestors(immutable_heads().., 2) | present(trunk())", 59 + "full_history" => "ancestors(visible_heads())", 60 + _ => "present(@) | ancestors(immutable_heads().., 2) | present(trunk())", // default to active 61 + } 62 + } else { 63 + // Default to "active" preset 64 + "present(@) | ancestors(immutable_heads().., 2) | present(trunk())" 65 + }; 61 66 62 - let mut diagnostics = RevsetDiagnostics::new(); 63 - let expression = parse(&mut diagnostics, revset_str, &context) 64 - .context("Failed to parse revset")?; 67 + // Set up aliases needed for presets 68 + let mut aliases_map = RevsetAliasesMap::new(); 65 69 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")?; 70 + // trunk() - jj-cli style using remote_bookmarks with fallback to root 71 + aliases_map.insert( 72 + "trunk()", 73 + r#"latest( 74 + remote_bookmarks(exact:"main", exact:"origin") | 75 + remote_bookmarks(exact:"master", exact:"origin") | 76 + remote_bookmarks(exact:"trunk", exact:"origin") | 77 + root() 78 + )"#, 79 + ).ok(); 69 80 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")? 81 + // builtin_immutable_heads() - trunk + tags + untracked remote bookmarks 82 + aliases_map.insert("builtin_immutable_heads()", "present(trunk()) | tags() | untracked_remote_bookmarks()").ok(); 83 + 84 + // immutable_heads() - defaults to builtin 85 + aliases_map.insert("immutable_heads()", "builtin_immutable_heads()").ok(); 86 + 87 + // mine() - commits authored by current user 88 + let mine_revset = format!(r#"author_email(exact-i:"{}")"#, user_email); 89 + aliases_map.insert("mine()", &mine_revset).ok(); 90 + 91 + // Create workspace context for @ resolution 92 + let path_converter = RepoPathUiConverter::Fs { 93 + cwd: repo_path.to_path_buf(), 94 + base: repo_path.to_path_buf(), 95 + }; 96 + let workspace_name = jj_repo.workspace_name(); 97 + let workspace_ctx = RevsetWorkspaceContext { 98 + path_converter: &path_converter, 99 + workspace_name, 79 100 }; 80 101 102 + let context = RevsetParseContext { 103 + aliases_map: &aliases_map, 104 + local_variables: HashMap::new(), 105 + user_email: jj_repo.user_settings().user_email(), 106 + date_pattern_context: chrono::Utc::now().fixed_offset().into(), 107 + default_ignored_remote: Some(git::REMOTE_NAME_FOR_LOCAL_GIT_REPO), 108 + extensions: &RevsetExtensions::default(), 109 + workspace: Some(workspace_ctx), 110 + }; 111 + 112 + let mut diagnostics = RevsetDiagnostics::new(); 113 + let expression = parse(&mut diagnostics, revset_str, &context) 114 + .context("Failed to parse revset")?; 115 + 116 + let symbol_resolver = SymbolResolver::new(repo.as_ref(), &([] as [&Box<dyn SymbolResolverExtension>; 0])); 117 + let resolved = expression.resolve_user_expression(repo.as_ref(), &symbol_resolver) 118 + .context("Failed to resolve revset")?; 119 + 120 + let revset_expression = resolved.evaluate(repo.as_ref()) 121 + .context("Failed to evaluate revset")?; 122 + 81 123 // Use iter_graph() to get commits with edge information 82 124 let graph_nodes: Vec<(CommitId, Vec<GraphEdge<CommitId>>)> = revset_expression 83 125 .iter_graph() ··· 90 132 let immutable_expression = RevsetExpression::root(); 91 133 let immutable_revset = immutable_expression.evaluate(repo.as_ref())?; 92 134 let immutable_ids: Vec<CommitId> = immutable_revset.iter().collect::<Result<Vec<_>, _>>()?; 135 + 136 + // Evaluate ::trunk() to identify trunk ancestors 137 + let trunk_ancestor_ids: std::collections::HashSet<CommitId> = { 138 + let mut trunk_diagnostics = RevsetDiagnostics::new(); 139 + match parse(&mut trunk_diagnostics, "::trunk()", &context) { 140 + Ok(trunk_expr) => { 141 + match trunk_expr.resolve_user_expression(repo.as_ref(), &symbol_resolver) { 142 + Ok(resolved) => { 143 + match resolved.evaluate(repo.as_ref()) { 144 + Ok(revset) => revset.iter().filter_map(|r| r.ok()).collect(), 145 + Err(_) => std::collections::HashSet::new(), 146 + } 147 + } 148 + Err(_) => std::collections::HashSet::new(), 149 + } 150 + } 151 + Err(_) => std::collections::HashSet::new(), 152 + } 153 + }; 93 154 94 155 let mut revisions = Vec::new(); 95 156 ··· 144 205 .unwrap_or(full_change_id.len()); 145 206 let change_id_short = full_change_id[..prefix_len].to_string(); 146 207 208 + let is_trunk = trunk_ancestor_ids.contains(&commit_id); 209 + 147 210 revisions.push(Revision { 148 211 commit_id: hex::encode(&commit_id.to_bytes()[..6]), 149 212 change_id: full_change_id, ··· 156 219 is_working_copy, 157 220 is_immutable, 158 221 is_mine, 222 + is_trunk, 159 223 bookmarks, 160 224 }); 161 225 }
+2
apps/desktop/src/schemas.ts
··· 21 21 is_working_copy: Schema.Boolean, 22 22 is_immutable: Schema.Boolean, 23 23 is_mine: Schema.Boolean, 24 + is_trunk: Schema.Boolean, 24 25 bookmarks: Schema.Array(Schema.String), 25 26 }); 26 27 export type Revision = typeof Revision.Type; ··· 72 73 path: Schema.String, 73 74 name: Schema.String, 74 75 last_opened_at: Schema.Number, 76 + revset_preset: Schema.NullOr(Schema.String), 75 77 }); 76 78 export type Project = typeof Project.Type;