|
1 | 1 | use crate::head_info::function::workspace_data_of_default_workspace_branch;
|
2 |
| -use crate::ui::{CommitState, PushStatus}; |
| 2 | +use crate::ui::{CommitState, PushStatus, UpstreamCommit}; |
3 | 3 | use crate::{state_handle, ui};
|
4 | 4 | use anyhow::{Context, bail};
|
5 | 5 | use but_core::RefMetadata;
|
6 | 6 | use gitbutler_command_context::CommandContext;
|
7 | 7 | use gitbutler_error::error::Code;
|
8 |
| -use gitbutler_oxidize::OidExt; |
| 8 | +use gitbutler_oxidize::{ObjectIdExt, OidExt}; |
| 9 | +use gix::reference::Category; |
9 | 10 | use gix::remote::Direction;
|
| 11 | +use itertools::Itertools; |
10 | 12 | use std::collections::HashSet;
|
11 | 13 | use std::path::Path;
|
12 | 14 |
|
@@ -58,64 +60,20 @@ pub fn branch_details(
|
58 | 60 | bail!("Failed to find merge base");
|
59 | 61 | };
|
60 | 62 |
|
61 |
| - let mut revwalk = repository.revwalk()?; |
62 |
| - revwalk.push(branch_oid)?; |
63 |
| - revwalk.hide(default_target.sha)?; |
64 |
| - revwalk.simplify_first_parent()?; |
65 |
| - |
66 |
| - let commits = revwalk |
67 |
| - .filter_map(|oid| repository.find_commit(oid.ok()?).ok()) |
68 |
| - .collect::<Vec<_>>(); |
69 |
| - |
70 |
| - let upstream_commits = if let Some(upstream_oid) = upstream_oid { |
71 |
| - let mut revwalk = repository.revwalk()?; |
72 |
| - revwalk.push(upstream_oid)?; |
73 |
| - revwalk.hide(branch_oid)?; |
74 |
| - revwalk.hide(default_target.sha)?; |
75 |
| - revwalk.simplify_first_parent()?; |
76 |
| - revwalk |
77 |
| - .filter_map(|oid| repository.find_commit(oid.ok()?).ok()) |
78 |
| - .collect::<Vec<_>>() |
79 |
| - } else { |
80 |
| - vec![] |
81 |
| - }; |
82 |
| - |
83 | 63 | let mut authors = HashSet::new();
|
84 |
| - |
85 |
| - let commits = commits |
86 |
| - .into_iter() |
87 |
| - .map(|commit| { |
88 |
| - let author: ui::Author = commit.author().into(); |
89 |
| - let commiter: ui::Author = commit.committer().into(); |
90 |
| - authors.insert(author.clone()); |
91 |
| - authors.insert(commiter); |
92 |
| - ui::Commit { |
93 |
| - id: commit.id().to_gix(), |
94 |
| - parent_ids: commit.parent_ids().map(|id| id.to_gix()).collect(), |
95 |
| - message: commit.message().unwrap_or_default().into(), |
96 |
| - has_conflicts: false, |
97 |
| - state: CommitState::LocalAndRemote(commit.id().to_gix()), |
98 |
| - created_at: u128::try_from(commit.time().seconds()).unwrap_or(0) * 1000, |
99 |
| - author, |
100 |
| - } |
101 |
| - }) |
102 |
| - .collect::<Vec<_>>(); |
103 |
| - |
104 |
| - let upstream_commits = upstream_commits |
105 |
| - .into_iter() |
106 |
| - .map(|commit| { |
107 |
| - let author: ui::Author = commit.author().into(); |
108 |
| - let commiter: ui::Author = commit.committer().into(); |
109 |
| - authors.insert(author.clone()); |
110 |
| - authors.insert(commiter); |
111 |
| - ui::UpstreamCommit { |
112 |
| - id: commit.id().to_gix(), |
113 |
| - message: commit.message().unwrap_or_default().into(), |
114 |
| - created_at: u128::try_from(commit.time().seconds()).unwrap_or(0) * 1000, |
115 |
| - author, |
116 |
| - } |
| 64 | + let commits = local_commits(repository, default_target.sha, branch_oid, &mut authors)?; |
| 65 | + let upstream_commits = upstream_oid |
| 66 | + .map(|upstream_oid| { |
| 67 | + upstream_commits( |
| 68 | + repository, |
| 69 | + upstream_oid, |
| 70 | + default_target.sha, |
| 71 | + branch_oid, |
| 72 | + &mut authors, |
| 73 | + ) |
117 | 74 | })
|
118 |
| - .collect::<Vec<_>>(); |
| 75 | + .transpose()? |
| 76 | + .unwrap_or_default(); |
119 | 77 |
|
120 | 78 | Ok(ui::BranchDetails {
|
121 | 79 | name: branch_name.into(),
|
@@ -145,76 +103,169 @@ pub fn branch_details(
|
145 | 103 | /// This branch is assumed to not be in the workspace, but it will still be assumed to want to integrate with the workspace target reference if set.
|
146 | 104 | ///
|
147 | 105 | /// ### Implementation
|
148 |
| -#[allow(unused_variables)] |
| 106 | +/// |
| 107 | +/// Note that the following fields aren't computed, but set to a bogus value - this would need to change once the UI makes use of them. |
| 108 | +/// They are set to values which should be clearly wrong. |
| 109 | +/// |
| 110 | +/// - `push_status` |
| 111 | +/// - `is_conflicted` |
149 | 112 | pub fn branch_details_v3(
|
150 | 113 | repo: &gix::Repository,
|
151 | 114 | name: &gix::refs::FullNameRef,
|
152 | 115 | meta: &impl RefMetadata,
|
153 | 116 | ) -> anyhow::Result<ui::BranchDetails> {
|
154 |
| - let integration_ref_name = workspace_data_of_default_workspace_branch(meta)? |
| 117 | + let integration_branch_name = workspace_data_of_default_workspace_branch(meta)? |
155 | 118 | .context(
|
156 | 119 | "TODO: cannot run in non-workspace mode yet.\
|
157 | 120 | It would need a way to deal with limiting the commit traversal",
|
158 | 121 | )?
|
159 | 122 | .target_ref
|
160 | 123 | .context("TODO: a target to integrate with is currently needed for a workspace commit")?;
|
161 |
| - let mut integration_ref = repo |
162 |
| - .find_reference(&integration_ref_name) |
| 124 | + let mut integration_branch = repo |
| 125 | + .find_reference(&integration_branch_name) |
163 | 126 | .context("The branch to integrate with must be present")?;
|
164 |
| - let integration_ref_target_id = integration_ref.peel_to_id_in_place()?; |
| 127 | + let integration_branch_id = integration_branch.peel_to_id_in_place()?; |
165 | 128 |
|
166 | 129 | let mut branch = repo.find_reference(name)?;
|
167 |
| - let branch_target_id = branch.peel_to_id_in_place()?; |
| 130 | + let branch_id = branch.peel_to_id_in_place()?; |
| 131 | + |
| 132 | + let cache = repo.commit_graph_if_enabled()?; |
| 133 | + let mut graph = repo.revision_graph(cache.as_ref()); |
168 | 134 |
|
169 | 135 | let mut remote_tracking_branch = repo
|
170 | 136 | .branch_remote_tracking_ref_name(name, Direction::Fetch)
|
171 | 137 | .transpose()?
|
172 | 138 | .and_then(|remote_tracking_ref| repo.find_reference(remote_tracking_ref.as_ref()).ok());
|
173 |
| - let remote_tracking_target_id = remote_tracking_branch |
174 |
| - .as_mut() |
175 |
| - .map(|remote_ref| remote_ref.peel_to_id_in_place()) |
176 |
| - .transpose()?; |
177 |
| - let push_status = remote_tracking_target_id |
178 |
| - .map(|remote_target_id| { |
179 |
| - if remote_target_id == branch_target_id { |
180 |
| - PushStatus::NothingToPush |
181 |
| - } else { |
182 |
| - PushStatus::UnpushedCommits |
183 |
| - } |
184 |
| - }) |
185 |
| - .unwrap_or(PushStatus::CompletelyUnpushed); |
186 | 139 |
|
187 | 140 | let meta = meta.branch(name)?;
|
188 | 141 | let meta: &but_core::ref_metadata::Branch = &meta;
|
189 | 142 |
|
190 | 143 | let base_commit = {
|
191 |
| - let cache = repo.commit_graph_if_enabled()?; |
192 |
| - let mut graph = repo.revision_graph(cache.as_ref()); |
193 | 144 | let merge_bases = repo.merge_bases_many_with_graph(
|
194 |
| - branch_target_id, |
195 |
| - &[integration_ref_target_id.detach()], |
| 145 | + branch_id, |
| 146 | + &[integration_branch_id.detach()], |
196 | 147 | &mut graph,
|
197 | 148 | )?;
|
198 | 149 | // TODO: have a test that shows why this must/should be last. Then maybe make it easy to do
|
199 | 150 | // the right thing whenever the mergebase with the integration branch is needed.
|
200 |
| - merge_bases.last().map(|id| id.detach()) |
| 151 | + merge_bases |
| 152 | + .last() |
| 153 | + .map(|id| id.detach()) |
| 154 | + .with_context(|| format!("No merge-base found between {name} and the integration branch {integration_branch_name}", name = name.as_bstr()))? |
| 155 | + }; |
| 156 | + |
| 157 | + let mut authors = HashSet::new(); |
| 158 | + let (commits, upstream_commits) = { |
| 159 | + let repo = git2::Repository::open(repo.path())?; |
| 160 | + |
| 161 | + let commits = local_commits( |
| 162 | + &repo, |
| 163 | + integration_branch_id.to_git2(), |
| 164 | + branch_id.to_git2(), |
| 165 | + &mut authors, |
| 166 | + )?; |
| 167 | + |
| 168 | + let upstream_commits = if let Some(remote_tracking_branch) = remote_tracking_branch.as_mut() |
| 169 | + { |
| 170 | + let remote_id = remote_tracking_branch.peel_to_id_in_place()?; |
| 171 | + upstream_commits( |
| 172 | + &repo, |
| 173 | + remote_id.to_git2(), |
| 174 | + integration_branch_id.to_git2(), |
| 175 | + branch_id.to_git2(), |
| 176 | + &mut authors, |
| 177 | + )? |
| 178 | + } else { |
| 179 | + Vec::new() |
| 180 | + }; |
| 181 | + (commits, upstream_commits) |
201 | 182 | };
|
202 | 183 |
|
203 |
| - todo!() |
204 |
| - // Ok(ui::BranchDetails { |
205 |
| - // name: name.as_bstr().into(), |
206 |
| - // remote_tracking_branch: remote_tracking_branch.map(|b| b.name().as_bstr().to_owned()), |
207 |
| - // description: meta.description.clone(), |
208 |
| - // pr_number: meta.review.pull_request, |
209 |
| - // review_id: meta.review.review_id.clone(), |
210 |
| - // base_commit: todo!(), |
211 |
| - // push_status, |
212 |
| - // last_updated_at: todo!(), |
213 |
| - // authors: todo!(), |
214 |
| - // is_conflicted: todo!(), |
215 |
| - // commits: todo!(), |
216 |
| - // upstream_commits: todo!(), |
217 |
| - // tip: branch_target_id.detach(), |
218 |
| - // is_remote_head: name.category() == Some(Category::RemoteBranch), |
219 |
| - // }) |
| 184 | + Ok(ui::BranchDetails { |
| 185 | + name: name.as_bstr().into(), |
| 186 | + remote_tracking_branch: remote_tracking_branch.map(|b| b.name().as_bstr().to_owned()), |
| 187 | + description: meta.description.clone(), |
| 188 | + pr_number: meta.review.pull_request, |
| 189 | + review_id: meta.review.review_id.clone(), |
| 190 | + base_commit, |
| 191 | + last_updated_at: meta.ref_info.updated_at.map(|d| d.seconds as u128 * 1_000), |
| 192 | + authors: authors |
| 193 | + .into_iter() |
| 194 | + .sorted_by(|a, b| a.name.cmp(&b.name).then_with(|| a.email.cmp(&b.email))) |
| 195 | + .collect(), |
| 196 | + commits, |
| 197 | + upstream_commits, |
| 198 | + tip: branch_id.detach(), |
| 199 | + is_remote_head: name.category() == Some(Category::RemoteBranch), |
| 200 | + |
| 201 | + // Should be unitialized, but this type is re-used in StackDetails where these are set so we can't indicate it. |
| 202 | + push_status: PushStatus::Integrated, |
| 203 | + is_conflicted: true, |
| 204 | + }) |
| 205 | +} |
| 206 | + |
| 207 | +/// Traverse all commits that are reachable from the first parent of `upstream_id`, but not in `integration_branch_id` nor in `branch_id`. |
| 208 | +/// While at it, collect the commiter and author of each commit into `authors`. |
| 209 | +fn upstream_commits( |
| 210 | + repository: &git2::Repository, |
| 211 | + upstream_id: git2::Oid, |
| 212 | + integration_branch_id: git2::Oid, |
| 213 | + branch_id: git2::Oid, |
| 214 | + authors: &mut HashSet<ui::Author>, |
| 215 | +) -> anyhow::Result<Vec<UpstreamCommit>> { |
| 216 | + let mut revwalk = repository.revwalk()?; |
| 217 | + revwalk.push(upstream_id)?; |
| 218 | + revwalk.hide(branch_id)?; |
| 219 | + revwalk.hide(integration_branch_id)?; |
| 220 | + revwalk.simplify_first_parent()?; |
| 221 | + Ok(revwalk |
| 222 | + .filter_map(Result::ok) |
| 223 | + .filter_map(|oid| repository.find_commit(oid).ok()) |
| 224 | + .map(|commit| { |
| 225 | + let author: ui::Author = commit.author().into(); |
| 226 | + let commiter: ui::Author = commit.committer().into(); |
| 227 | + authors.insert(author.clone()); |
| 228 | + authors.insert(commiter); |
| 229 | + ui::UpstreamCommit { |
| 230 | + id: commit.id().to_gix(), |
| 231 | + message: commit.message().unwrap_or_default().into(), |
| 232 | + created_at: u128::try_from(commit.time().seconds()).unwrap_or(0) * 1000, |
| 233 | + author, |
| 234 | + } |
| 235 | + }) |
| 236 | + .collect()) |
| 237 | +} |
| 238 | + |
| 239 | +/// Traverse all commits that are reachable from the first parent of `branch_id`, but not in `integration_branch`, and store all |
| 240 | +/// commit authors and committers in `authors` while at it. |
| 241 | +fn local_commits( |
| 242 | + repository: &git2::Repository, |
| 243 | + integration_branch_id: git2::Oid, |
| 244 | + branch_id: git2::Oid, |
| 245 | + authors: &mut HashSet<ui::Author>, |
| 246 | +) -> anyhow::Result<Vec<ui::Commit>> { |
| 247 | + let mut revwalk = repository.revwalk()?; |
| 248 | + revwalk.push(branch_id)?; |
| 249 | + revwalk.hide(integration_branch_id)?; |
| 250 | + revwalk.simplify_first_parent()?; |
| 251 | + |
| 252 | + Ok(revwalk |
| 253 | + .filter_map(Result::ok) |
| 254 | + .filter_map(|oid| repository.find_commit(oid).ok()) |
| 255 | + .map(|commit| { |
| 256 | + let author: ui::Author = commit.author().into(); |
| 257 | + let commiter: ui::Author = commit.committer().into(); |
| 258 | + authors.insert(author.clone()); |
| 259 | + authors.insert(commiter); |
| 260 | + ui::Commit { |
| 261 | + id: commit.id().to_gix(), |
| 262 | + parent_ids: commit.parent_ids().map(|id| id.to_gix()).collect(), |
| 263 | + message: commit.message().unwrap_or_default().into(), |
| 264 | + has_conflicts: false, |
| 265 | + state: CommitState::LocalAndRemote(commit.id().to_gix()), |
| 266 | + created_at: u128::try_from(commit.time().seconds()).unwrap_or(0) * 1000, |
| 267 | + author, |
| 268 | + } |
| 269 | + }) |
| 270 | + .collect()) |
220 | 271 | }
|
0 commit comments