max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
6 files changed,
+427 insertions,
-222 deletions
| @@ -1,52 +1,86 @@ | |||
| 1 | 1 | //! Commit history, log, detail, diff, and blame operations. | |
| 2 | 2 | ||
| 3 | - | use std::path::Path; | |
| 4 | - | ||
| 5 | - | use git2::{Diff, DiffOptions, Oid, Repository, Sort}; | |
| 3 | + | use gix::{ | |
| 4 | + | ObjectId, Repository, | |
| 5 | + | bstr::ByteSlice, | |
| 6 | + | diff::blob::{ | |
| 7 | + | Algorithm, InternedInput, UnifiedDiff, | |
| 8 | + | unified_diff::{ConsumeHunk, ContextSize, DiffLineKind, HunkHeader}, | |
| 9 | + | }, | |
| 10 | + | revision::walk::Sorting, | |
| 11 | + | traverse::commit::simple::CommitTimeOrder, | |
| 12 | + | }; | |
| 6 | 13 | ||
| 7 | 14 | use super::{ | |
| 8 | 15 | BlameLine, CommitDetail, CommitInfo, DiffFile, DiffHunk, DiffLine, DiffStatus, GitError, | |
| 9 | 16 | ParentRef, | |
| 10 | 17 | }; | |
| 11 | 18 | ||
| 19 | + | /// Newest-commit-first history order, matching what the log pages have always | |
| 20 | + | /// shown (libgit2's `Sort::TIME`). | |
| 21 | + | const NEWEST_FIRST: Sorting = Sorting::ByCommitTime(CommitTimeOrder::NewestFirst); | |
| 22 | + | ||
| 23 | + | /// Shorten an object id for display, as the log and commit pages render it. | |
| 24 | + | fn short(oid: &str) -> String { | |
| 25 | + | oid[..7.min(oid.len())].to_string() | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | /// Build the log entry for a commit. Author identity comes from the author | |
| 29 | + | /// signature, the displayed timestamp from the commit (committer) time, which | |
| 30 | + | /// is what the log has always shown. | |
| 31 | + | fn commit_info(commit: &gix::Commit<'_>) -> CommitInfo { | |
| 32 | + | let oid = commit.id().to_string(); | |
| 33 | + | let short_oid = short(&oid); | |
| 34 | + | ||
| 35 | + | let summary = commit | |
| 36 | + | .message_raw() | |
| 37 | + | .ok() | |
| 38 | + | .and_then(|m| m.lines().next().map(|l| l.to_str_lossy().into_owned())) | |
| 39 | + | .unwrap_or_default(); | |
| 40 | + | ||
| 41 | + | let author = commit.author().ok(); | |
| 42 | + | let author_name = author.map_or_else( | |
| 43 | + | || "Unknown".to_string(), | |
| 44 | + | |a| a.name.to_str_lossy().into_owned(), | |
| 45 | + | ); | |
| 46 | + | let author_email = author | |
| 47 | + | .map(|a| a.email.to_str_lossy().into_owned()) | |
| 48 | + | .unwrap_or_default(); | |
| 49 | + | ||
| 50 | + | let seconds = commit.time().map(|t| t.seconds).unwrap_or_default(); | |
| 51 | + | let time = chrono::DateTime::from_timestamp(seconds, 0).unwrap_or_default(); | |
| 52 | + | let time_formatted = time.format("%Y-%m-%d %H:%M").to_string(); | |
| 53 | + | ||
| 54 | + | CommitInfo { | |
| 55 | + | oid, | |
| 56 | + | short_oid, | |
| 57 | + | summary, | |
| 58 | + | author_name, | |
| 59 | + | author_email, | |
| 60 | + | time, | |
| 61 | + | time_formatted, | |
| 62 | + | } | |
| 63 | + | } | |
| 64 | + | ||
| 12 | 65 | /// Walk commit history with pagination. | |
| 13 | 66 | pub fn commit_log( | |
| 14 | 67 | repo: &Repository, | |
| 15 | - | commit_oid: Oid, | |
| 68 | + | commit_oid: ObjectId, | |
| 16 | 69 | limit: usize, | |
| 17 | 70 | offset: usize, | |
| 18 | 71 | ) -> Result<Vec<CommitInfo>, GitError> { | |
| 19 | - | let mut revwalk = repo.revwalk()?; | |
| 20 | - | revwalk.push(commit_oid)?; | |
| 21 | - | revwalk.set_sorting(Sort::TIME)?; | |
| 72 | + | let walk = repo | |
| 73 | + | .rev_walk([commit_oid]) | |
| 74 | + | .sorting(NEWEST_FIRST) | |
| 75 | + | .all() | |
| 76 | + | .map_err(|_| GitError::RefNotFound)?; | |
| 22 | 77 | ||
| 23 | - | let commits: Vec<CommitInfo> = revwalk | |
| 78 | + | let commits: Vec<CommitInfo> = walk | |
| 24 | 79 | .filter_map(std::result::Result::ok) | |
| 25 | - | .filter_map(|oid| repo.find_commit(oid).ok()) | |
| 80 | + | .filter_map(|info| repo.find_commit(info.id).ok()) | |
| 26 | 81 | .skip(offset) | |
| 27 | 82 | .take(limit) | |
| 28 | - | .map(|commit| { | |
| 29 | - | let oid = commit.id().to_string(); | |
| 30 | - | let short_oid = oid[..7.min(oid.len())].to_string(); | |
| 31 | - | let message = commit.message().unwrap_or(""); | |
| 32 | - | let summary = message.lines().next().unwrap_or("").to_string(); | |
| 33 | - | let author = commit.author(); | |
| 34 | - | let author_name = author.name().unwrap_or("Unknown").to_string(); | |
| 35 | - | let author_email = author.email().unwrap_or("").to_string(); | |
| 36 | - | let time = | |
| 37 | - | chrono::DateTime::from_timestamp(commit.time().seconds(), 0).unwrap_or_default(); | |
| 38 | - | let time_formatted = time.format("%Y-%m-%d %H:%M").to_string(); | |
| 39 | - | ||
| 40 | - | CommitInfo { | |
| 41 | - | oid, | |
| 42 | - | short_oid, | |
| 43 | - | summary, | |
| 44 | - | author_name, | |
| 45 | - | author_email, | |
| 46 | - | time, | |
| 47 | - | time_formatted, | |
| 48 | - | } | |
| 49 | - | }) | |
| 83 | + | .map(|commit| commit_info(&commit)) | |
| 50 | 84 | .collect(); | |
| 51 | 85 | ||
| 52 | 86 | Ok(commits) | |
| @@ -58,56 +92,44 @@ | |||
| 58 | 92 | /// OID at `path` differs from the parent's blob OID (or the file was added/removed). | |
| 59 | 93 | pub fn file_commit_log( | |
| 60 | 94 | repo: &Repository, | |
| 61 | - | commit_oid: Oid, | |
| 95 | + | commit_oid: ObjectId, | |
| 62 | 96 | path: &str, | |
| 63 | 97 | limit: usize, | |
| 64 | 98 | offset: usize, | |
| 65 | 99 | max_walk: usize, | |
| 66 | 100 | ) -> Result<Vec<CommitInfo>, GitError> { | |
| 67 | - | let mut revwalk = repo.revwalk()?; | |
| 68 | - | revwalk.push(commit_oid)?; | |
| 69 | - | revwalk.set_sorting(Sort::TIME)?; | |
| 101 | + | let walk = repo | |
| 102 | + | .rev_walk([commit_oid]) | |
| 103 | + | .sorting(NEWEST_FIRST) | |
| 104 | + | .all() | |
| 105 | + | .map_err(|_| GitError::RefNotFound)?; | |
| 70 | 106 | ||
| 71 | - | let file_path = Path::new(path); | |
| 72 | 107 | let mut result = Vec::new(); | |
| 73 | 108 | let mut skipped = 0; | |
| 74 | 109 | ||
| 75 | - | for (walked, oid_result) in revwalk.enumerate() { | |
| 110 | + | for (walked, info) in walk.enumerate() { | |
| 76 | 111 | if walked >= max_walk || result.len() >= limit { | |
| 77 | 112 | break; | |
| 78 | 113 | } | |
| 79 | 114 | ||
| 80 | - | let Ok(oid) = oid_result else { | |
| 115 | + | let Ok(info) = info else { | |
| 81 | 116 | continue; | |
| 82 | 117 | }; | |
| 83 | - | let Ok(commit) = repo.find_commit(oid) else { | |
| 84 | - | continue; | |
| 85 | - | }; | |
| 86 | - | ||
| 87 | - | let Ok(tree) = commit.tree() else { | |
| 118 | + | let Ok(commit) = repo.find_commit(info.id) else { | |
| 88 | 119 | continue; | |
| 89 | 120 | }; | |
| 90 | 121 | ||
| 91 | - | // Get the blob OID at `path` in this commit (None if file doesn't exist) | |
| 92 | - | let current_blob = tree.get_path(file_path).ok().map(|entry| entry.id()); | |
| 122 | + | // The blob OID at `path` in this commit (None if the file is absent) | |
| 123 | + | let current_blob = blob_id_at(&commit, path); | |
| 93 | 124 | ||
| 94 | - | // Compare against parent(s) | |
| 95 | - | let changed = if commit.parent_count() == 0 { | |
| 96 | - | // Root commit: file is "changed" if it exists | |
| 97 | - | current_blob.is_some() | |
| 98 | - | } else { | |
| 99 | - | // Check first parent only (standard git log behavior) | |
| 100 | - | match commit.parent(0) { | |
| 101 | - | Ok(parent) => { | |
| 102 | - | let parent_blob = parent | |
| 103 | - | .tree() | |
| 104 | - | .ok() | |
| 105 | - | .and_then(|t| t.get_path(file_path).ok()) | |
| 106 | - | .map(|entry| entry.id()); | |
| 107 | - | current_blob != parent_blob | |
| 108 | - | } | |
| 125 | + | // Compare against the first parent only (standard git log behavior). | |
| 126 | + | // A root commit counts as a change if the file exists in it. | |
| 127 | + | let changed = match commit.parent_ids().next() { | |
| 128 | + | None => current_blob.is_some(), | |
| 129 | + | Some(parent_id) => match repo.find_commit(parent_id) { | |
| 130 | + | Ok(parent) => current_blob != blob_id_at(&parent, path), | |
| 109 | 131 | Err(_) => current_blob.is_some(), | |
| 110 | - | } | |
| 132 | + | }, | |
| 111 | 133 | }; | |
| 112 | 134 | ||
| 113 | 135 | if !changed { | |
| @@ -119,60 +141,52 @@ | |||
| 119 | 141 | continue; | |
| 120 | 142 | } | |
| 121 | 143 | ||
| 122 | - | let oid_str = commit.id().to_string(); | |
| 123 | - | let short_oid = oid_str[..7.min(oid_str.len())].to_string(); | |
| 124 | - | let message = commit.message().unwrap_or(""); | |
| 125 | - | let summary = message.lines().next().unwrap_or("").to_string(); | |
| 126 | - | let author = commit.author(); | |
| 127 | - | let author_name = author.name().unwrap_or("Unknown").to_string(); | |
| 128 | - | let author_email = author.email().unwrap_or("").to_string(); | |
| 129 | - | let time = chrono::DateTime::from_timestamp(commit.time().seconds(), 0).unwrap_or_default(); | |
| 130 | - | let time_formatted = time.format("%Y-%m-%d %H:%M").to_string(); | |
| 131 | - | ||
| 132 | - | result.push(CommitInfo { | |
| 133 | - | oid: oid_str, | |
| 134 | - | short_oid, | |
| 135 | - | summary, | |
| 136 | - | author_name, | |
| 137 | - | author_email, | |
| 138 | - | time, | |
| 139 | - | time_formatted, | |
| 140 | - | }); | |
| 144 | + | result.push(commit_info(&commit)); | |
| 141 | 145 | } | |
| 142 | 146 | ||
| 143 | 147 | Ok(result) | |
| 144 | 148 | } | |
| 145 | 149 | ||
| 150 | + | /// The object id of the entry at `path` in a commit's tree, if it exists. | |
| 151 | + | fn blob_id_at(commit: &gix::Commit<'_>, path: &str) -> Option<ObjectId> { | |
| 152 | + | let mut tree = commit.tree().ok()?; | |
| 153 | + | Some(tree.peel_to_entry_by_path(path).ok()??.oid().to_owned()) | |
| 154 | + | } | |
| 155 | + | ||
| 146 | 156 | /// Get full commit detail for the commit page. | |
| 147 | - | pub fn commit_detail(repo: &Repository, oid: Oid) -> Result<CommitDetail, GitError> { | |
| 157 | + | pub fn commit_detail(repo: &Repository, oid: ObjectId) -> Result<CommitDetail, GitError> { | |
| 148 | 158 | let commit = repo.find_commit(oid).map_err(|_| GitError::RefNotFound)?; | |
| 149 | 159 | ||
| 150 | 160 | let oid_str = oid.to_string(); | |
| 151 | - | let short_oid = oid_str[..7.min(oid_str.len())].to_string(); | |
| 161 | + | let short_oid = short(&oid_str); | |
| 152 | 162 | ||
| 153 | - | let message = commit.message().unwrap_or("").to_string(); | |
| 163 | + | let message = commit | |
| 164 | + | .message_raw() | |
| 165 | + | .map(|m| m.to_str_lossy().into_owned()) | |
| 166 | + | .unwrap_or_default(); | |
| 154 | 167 | let summary = message.lines().next().unwrap_or("").to_string(); | |
| 155 | 168 | ||
| 156 | - | let author = commit.author(); | |
| 157 | - | let committer = commit.committer(); | |
| 169 | + | let author = commit.author().ok(); | |
| 170 | + | let committer = commit.committer().ok(); | |
| 158 | 171 | ||
| 159 | - | let author_time = chrono::DateTime::from_timestamp(author.when().seconds(), 0) | |
| 160 | - | .unwrap_or_default() | |
| 161 | - | .format("%Y-%m-%d %H:%M UTC") | |
| 162 | - | .to_string(); | |
| 163 | - | let committer_time = chrono::DateTime::from_timestamp(committer.when().seconds(), 0) | |
| 164 | - | .unwrap_or_default() | |
| 165 | - | .format("%Y-%m-%d %H:%M UTC") | |
| 166 | - | .to_string(); | |
| 172 | + | let format_time = |sig: Option<gix::actor::SignatureRef<'_>>| { | |
| 173 | + | let seconds = sig | |
| 174 | + | .and_then(|s| s.time().ok()) | |
| 175 | + | .map(|t| t.seconds) | |
| 176 | + | .unwrap_or_default(); | |
| 177 | + | chrono::DateTime::from_timestamp(seconds, 0) | |
| 178 | + | .unwrap_or_default() | |
| 179 | + | .format("%Y-%m-%d %H:%M UTC") | |
| 180 | + | .to_string() | |
| 181 | + | }; | |
| 167 | 182 | ||
| 168 | 183 | let parents: Vec<ParentRef> = commit | |
| 169 | 184 | .parent_ids() | |
| 170 | 185 | .map(|pid| { | |
| 171 | 186 | let s = pid.to_string(); | |
| 172 | - | let short = s[..7.min(s.len())].to_string(); | |
| 173 | 187 | ParentRef { | |
| 188 | + | short_oid: short(&s), | |
| 174 | 189 | oid: s, | |
| 175 | - | short_oid: short, | |
| 176 | 190 | } | |
| 177 | 191 | }) | |
| 178 | 192 | .collect(); | |
| @@ -182,126 +196,209 @@ | |||
| 182 | 196 | short_oid, | |
| 183 | 197 | summary, | |
| 184 | 198 | full_message: message, | |
| 185 | - | author_name: author.name().unwrap_or("Unknown").to_string(), | |
| 186 | - | author_email: author.email().unwrap_or("").to_string(), | |
| 187 | - | author_time, | |
| 188 | - | committer_name: committer.name().unwrap_or("Unknown").to_string(), | |
| 189 | - | committer_email: committer.email().unwrap_or("").to_string(), | |
| 190 | - | committer_time, | |
| 199 | + | author_name: author.map_or_else( | |
| 200 | + | || "Unknown".to_string(), | |
| 201 | + | |a| a.name.to_str_lossy().into_owned(), | |
| 202 | + | ), | |
| 203 | + | author_email: author | |
| 204 | + | .map(|a| a.email.to_str_lossy().into_owned()) | |
| 205 | + | .unwrap_or_default(), | |
| 206 | + | author_time: format_time(author), | |
| 207 | + | committer_name: committer.map_or_else( | |
| 208 | + | || "Unknown".to_string(), | |
| 209 | + | |c| c.name.to_str_lossy().into_owned(), | |
| 210 | + | ), | |
| 211 | + | committer_email: committer | |
| 212 | + | .map(|c| c.email.to_str_lossy().into_owned()) | |
| 213 | + | .unwrap_or_default(), | |
| 214 | + | committer_time: format_time(committer), | |
| 191 | 215 | parents, | |
| 192 | 216 | }) | |
| 193 | 217 | } | |
| 194 | 218 | ||
| 219 | + | /// Escape the HTML-significant characters in a line of file content. | |
| 220 | + | fn escape(content: &[u8]) -> String { | |
| 221 | + | content | |
| 222 | + | .to_str_lossy() | |
| 223 | + | .replace('&', "&") | |
| 224 | + | .replace('<', "<") | |
| 225 | + | .replace('>', ">") | |
| 226 | + | } | |
| 227 | + | ||
| 228 | + | /// Binary detection, matching the blob reader: a null byte in the first 8KB. | |
| 229 | + | fn looks_binary(data: &[u8]) -> bool { | |
| 230 | + | data[..data.len().min(8192)].contains(&0) | |
| 231 | + | } | |
| 232 | + | ||
| 233 | + | /// Collects unified-diff hunks into the domain types, stopping once | |
| 234 | + | /// `max_lines` lines have been taken. | |
| 235 | + | struct HunkCollector { | |
| 236 | + | hunks: Vec<DiffHunk>, | |
| 237 | + | additions: usize, | |
| 238 | + | deletions: usize, | |
| 239 | + | total_lines: usize, | |
| 240 | + | max_lines: usize, | |
| 241 | + | truncated: bool, | |
| 242 | + | } | |
| 243 | + | ||
| 244 | + | impl ConsumeHunk for HunkCollector { | |
| 245 | + | type Out = Self; | |
| 246 | + | ||
| 247 | + | fn consume_hunk( | |
| 248 | + | &mut self, | |
| 249 | + | header: HunkHeader, | |
| 250 | + | lines: &[(DiffLineKind, &[u8])], | |
| 251 | + | ) -> std::io::Result<()> { | |
| 252 | + | if self.truncated { | |
| 253 | + | return Ok(()); | |
| 254 | + | } | |
| 255 | + | ||
| 256 | + | let text = format!( | |
| 257 | + | "@@ -{},{} +{},{} @@", | |
| 258 | + | header.before_hunk_start, | |
| 259 | + | header.before_hunk_len, | |
| 260 | + | header.after_hunk_start, | |
| 261 | + | header.after_hunk_len | |
| 262 | + | ); | |
| 263 | + | ||
| 264 | + | let mut old_lineno = header.before_hunk_start; | |
| 265 | + | let mut new_lineno = header.after_hunk_start; | |
| 266 | + | let mut out = Vec::with_capacity(lines.len()); | |
| 267 | + | ||
| 268 | + | for (kind, content) in lines { | |
| 269 | + | if self.total_lines >= self.max_lines { | |
| 270 | + | self.truncated = true; | |
| 271 | + | break; | |
| 272 | + | } | |
| 273 | + | ||
| 274 | + | let (origin, old, new) = match kind { | |
| 275 | + | DiffLineKind::Context => { | |
| 276 | + | let pair = (Some(old_lineno), Some(new_lineno)); | |
| 277 | + | old_lineno += 1; | |
| 278 | + | new_lineno += 1; | |
| 279 | + | (' ', pair.0, pair.1) | |
| 280 | + | } | |
| 281 | + | DiffLineKind::Add => { | |
| 282 | + | let n = new_lineno; | |
| 283 | + | new_lineno += 1; | |
| 284 | + | self.additions += 1; | |
| 285 | + | ('+', None, Some(n)) | |
| 286 | + | } | |
| 287 | + | DiffLineKind::Remove => { | |
| 288 | + | let n = old_lineno; | |
| 289 | + | old_lineno += 1; | |
| 290 | + | self.deletions += 1; | |
| 291 | + | ('-', Some(n), None) | |
| 292 | + | } | |
| 293 | + | }; | |
| 294 | + | ||
| 295 | + | out.push(DiffLine { | |
| 296 | + | origin, | |
| 297 | + | content: escape(content), | |
| 298 | + | old_lineno: old, | |
| 299 | + | new_lineno: new, | |
| 300 | + | }); | |
| 301 | + | self.total_lines += 1; | |
| 302 | + | } | |
| 303 | + | ||
| 304 | + | self.hunks.push(DiffHunk { | |
| 305 | + | header: text, | |
| 306 | + | lines: out, | |
| 307 | + | }); | |
| 308 | + | Ok(()) | |
| 309 | + | } | |
| 310 | + | ||
| 311 | + | fn finish(self) -> Self::Out { | |
| 312 | + | self | |
| 313 | + | } | |
| 314 | + | } | |
| 315 | + | ||
| 195 | 316 | /// Generate a diff for a commit against its first parent (or empty tree for root commits). | |
| 196 | 317 | pub fn commit_diff( | |
| 197 | 318 | repo: &Repository, | |
| 198 | - | oid: Oid, | |
| 319 | + | oid: ObjectId, | |
| 199 | 320 | max_files: usize, | |
| 200 | 321 | max_lines_per_file: usize, | |
| 201 | 322 | ) -> Result<Vec<DiffFile>, GitError> { | |
| 202 | 323 | let commit = repo.find_commit(oid).map_err(|_| GitError::RefNotFound)?; | |
| 203 | 324 | let commit_tree = commit.tree().map_err(|_| GitError::TreeNotFound)?; | |
| 204 | 325 | ||
| 205 | - | let parent_tree = if commit.parent_count() > 0 { | |
| 206 | - | let parent = commit.parent(0)?; | |
| 207 | - | Some(parent.tree().map_err(|_| GitError::TreeNotFound)?) | |
| 208 | - | } else { | |
| 209 | - | None | |
| 326 | + | let parent_tree = match commit.parent_ids().next() { | |
| 327 | + | Some(parent_id) => Some( | |
| 328 | + | repo.find_commit(parent_id) | |
| 329 | + | .map_err(|_| GitError::RefNotFound)? | |
| 330 | + | .tree() | |
| 331 | + | .map_err(|_| GitError::TreeNotFound)?, | |
| 332 | + | ), | |
| 333 | + | None => None, | |
| 210 | 334 | }; | |
| 211 | 335 | ||
| 212 | - | let mut opts = DiffOptions::new(); | |
| 213 | - | opts.context_lines(3); | |
| 214 | - | let diff: Diff = | |
| 215 | - | repo.diff_tree_to_tree(parent_tree.as_ref(), Some(&commit_tree), Some(&mut opts))?; | |
| 336 | + | let changes = repo | |
| 337 | + | .diff_tree_to_tree(parent_tree.as_ref(), Some(&commit_tree), None) | |
| 338 | + | .map_err(|_| GitError::TreeNotFound)?; | |
| 216 | 339 | ||
| 217 | - | let num_deltas = diff.deltas().len(); | |
| 218 | - | let mut files = Vec::with_capacity(num_deltas); | |
| 340 | + | let mut files = Vec::with_capacity(changes.len()); | |
| 219 | 341 | ||
| 220 | - | for (file_idx, delta) in diff.deltas().enumerate() { | |
| 221 | - | let new_file = delta.new_file(); | |
| 222 | - | let old_file = delta.old_file(); | |
| 342 | + | for (file_idx, change) in changes.iter().enumerate() { | |
| 343 | + | // Directories are not rendered as diff entries; only their contents are. | |
| 344 | + | if change.entry_mode().is_tree() { | |
| 345 | + | continue; | |
| 346 | + | } | |
| 223 | 347 | ||
| 224 | - | let path = new_file | |
| 225 | - | .path() | |
| 226 | - | .and_then(|p| p.to_str()) | |
| 227 | - | .unwrap_or("") | |
| 228 | - | .to_string(); | |
| 348 | + | let (path, old_path, status, old_id, new_id) = describe(change); | |
| 229 | 349 | ||
| 230 | - | let status = match delta.status() { | |
| 231 | - | git2::Delta::Added => DiffStatus::Added, | |
| 232 | - | git2::Delta::Deleted => DiffStatus::Deleted, | |
| 233 | - | git2::Delta::Renamed => DiffStatus::Renamed, | |
| 234 | - | _ => DiffStatus::Modified, | |
| 235 | - | }; | |
| 350 | + | let old_data = old_id | |
| 351 | + | .map(|id| read_blob(repo, id)) | |
| 352 | + | .transpose()? | |
| 353 | + | .unwrap_or_default(); | |
| 354 | + | let new_data = new_id | |
| 355 | + | .map(|id| read_blob(repo, id)) | |
| 356 | + | .transpose()? | |
| 357 | + | .unwrap_or_default(); | |
| 236 | 358 | ||
| 237 | - | let old_path = if matches!(status, DiffStatus::Renamed) { | |
| 238 | - | old_file.path().and_then(|p| p.to_str()).map(String::from) | |
| 239 | - | } else { | |
| 240 | - | None | |
| 241 | - | }; | |
| 359 | + | let is_binary = looks_binary(&old_data) || looks_binary(&new_data); | |
| 242 | 360 | ||
| 243 | - | let is_binary = new_file.is_binary() || old_file.is_binary(); | |
| 244 | - | ||
| 245 | - | // Only collect hunks for first max_files files (and non-binary) | |
| 361 | + | // Only collect hunks for the first max_files files (and non-binary) | |
| 246 | 362 | let collect_hunks = file_idx < max_files && !is_binary; | |
| 247 | 363 | ||
| 248 | 364 | let mut hunks = Vec::new(); | |
| 249 | - | let mut additions: usize = 0; | |
| 250 | - | let mut deletions: usize = 0; | |
| 251 | - | let mut total_lines: usize = 0; | |
| 365 | + | let mut additions = 0; | |
| 366 | + | let mut deletions = 0; | |
| 252 | 367 | let mut truncated = false; | |
| 253 | 368 | ||
| 254 | - | if collect_hunks { | |
| 255 | - | // Use patch to get per-file hunks | |
| 256 | - | if let Ok(Some(ref patch)) = git2::Patch::from_diff(&diff, file_idx) { | |
| 257 | - | for hunk_idx in 0..patch.num_hunks() { | |
| 258 | - | if let Ok((hunk_header, _)) = patch.hunk(hunk_idx) { | |
| 259 | - | let header = String::from_utf8_lossy(hunk_header.header()).to_string(); | |
| 260 | - | let mut lines = Vec::new(); |
Lines truncated
| @@ -1,4 +1,5 @@ | |||
| 1 | - | //! Git abstraction layer, pure git2 wrapper with no HTTP/Axum concerns. | |
| 1 | + | //! Git abstraction layer, a thin wrapper with no HTTP/Axum concerns. Reads go | |
| 2 | + | //! through gitoxide; repository creation is still libgit2. | |
| 2 | 3 | //! | |
| 3 | 4 | //! All functions take a filesystem path and return `Result<T>`. | |
| 4 | 5 | //! Repository is opened per-request (cheap, just file descriptors). | |
| @@ -264,20 +265,16 @@ | |||
| 264 | 265 | Repository::open_bare(&canonical_repo).map_err(|_| GitError::RepoNotFound) | |
| 265 | 266 | } | |
| 266 | 267 | ||
| 267 | - | /// Open a bare repository at an already-resolved, validated path (e.g. one | |
| 268 | - | /// returned by [`repo_disk_path`]). Used to (re)open the repo inside a | |
| 269 | - | /// `spawn_blocking` closure, since `git2::Repository` is `!Send` and cannot | |
| 270 | - | /// cross the await boundary. | |
| 271 | - | pub(crate) fn open_repo_at(repo_path: &Path) -> Result<Repository, GitError> { | |
| 272 | - | Repository::open_bare(repo_path).map_err(|_| GitError::RepoNotFound) | |
| 268 | + | /// Open a bare repository with gitoxide, with the same path traversal | |
| 269 | + | /// validation [`open_repo`] applies. | |
| 270 | + | pub(crate) fn open_gix_repo( | |
| 271 | + | repos_root: &Path, | |
| 272 | + | owner: &str, | |
| 273 | + | repo: &str, | |
| 274 | + | ) -> Result<gix::Repository, GitError> { | |
| 275 | + | open_gix_repo_at(&repo_disk_path(repos_root, owner, repo)?) | |
| 273 | 276 | } | |
| 274 | 277 | ||
| 275 | - | /// Open a bare repository with gitoxide at an already-resolved, validated path. | |
| 276 | - | /// | |
| 277 | - | /// Sibling of [`open_repo_at`] while the libgit2 port is in flight. Unlike | |
| 278 | - | /// `git2::Repository`, `gix::Repository` is `Send`, so once every read path has | |
| 279 | - | /// moved over, the reopen-per-call dance in `routes::git::ResolvedRepo` can go | |
| 280 | - | /// away with it. | |
| 281 | 278 | /// Opened with `isolated()` options, so no ambient git configuration (the | |
| 282 | 279 | /// service account's `~/.gitconfig`, `/etc/gitconfig`, `GIT_*` environment | |
| 283 | 280 | /// variables) can change how a visitor's repository renders. | |
| @@ -285,26 +282,19 @@ | |||
| 285 | 282 | gix::open_opts(repo_path, gix::open::Options::isolated()).map_err(|_| GitError::RepoNotFound) | |
| 286 | 283 | } | |
| 287 | 284 | ||
| 288 | - | /// Convert a gitoxide object id to a libgit2 one. | |
| 289 | - | /// | |
| 290 | - | /// Transitional: the ported modules hand back `gix::ObjectId` while | |
| 291 | - | /// `git/history.rs` still takes `git2::Oid`. Delete this along with the last | |
| 292 | - | /// git2 read path. | |
| 293 | - | pub(crate) fn to_git2_oid(id: gix::ObjectId) -> git2::Oid { | |
| 294 | - | git2::Oid::from_bytes(id.as_bytes()).expect("gix and libgit2 agree on hash length") | |
| 295 | - | } | |
| 296 | - | ||
| 297 | 285 | /// Get basic repository info. | |
| 298 | - | pub fn repo_info(repo: &Repository, name: &str) -> RepoInfo { | |
| 286 | + | pub fn repo_info(repo: &gix::Repository, name: &str) -> RepoInfo { | |
| 299 | 287 | let description = std::fs::read_to_string(repo.path().join("description")) | |
| 300 | 288 | .ok() | |
| 301 | 289 | .filter(|d| !d.starts_with("Unnamed repository")); | |
| 302 | 290 | ||
| 291 | + | // An unborn HEAD still names the branch it will point at, which is what the | |
| 292 | + | // browse pages want; an unreadable one falls back to the conventional name. | |
| 303 | 293 | let default_branch = repo | |
| 304 | - | .head() | |
| 294 | + | .head_name() | |
| 305 | 295 | .ok() | |
| 306 | - | .and_then(|r| r.shorthand().ok().map(String::from)) | |
| 307 | - | .unwrap_or_else(|| "main".to_string()); | |
| 296 | + | .flatten() | |
| 297 | + | .map_or_else(|| "main".to_string(), |name| name.shorten().to_string()); | |
| 308 | 298 | ||
| 309 | 299 | RepoInfo { | |
| 310 | 300 | name: name.to_string(), | |
| @@ -573,6 +563,9 @@ | |||
| 573 | 563 | .commit(Some("refs/heads/main"), &sig, &sig, "c", &tree, &[]) | |
| 574 | 564 | .unwrap(); | |
| 575 | 565 | ||
| 566 | + | let repo = open_gix(&bare_path); | |
| 567 | + | let commit_oid = gix::ObjectId::from_bytes_or_panic(commit_oid.as_bytes()); | |
| 568 | + | ||
| 576 | 569 | assert!(matches!( | |
| 577 | 570 | blame_file(&repo, commit_oid, "big.txt"), | |
| 578 | 571 | Err(GitError::PathNotFound) | |
| @@ -628,11 +621,11 @@ | |||
| 628 | 621 | ||
| 629 | 622 | #[test] | |
| 630 | 623 | fn commit_log_returns_commits() { | |
| 631 | - | let (tmp, bare_path) = make_test_repo(); | |
| 632 | - | let repo = open_repo(tmp.path(), "owner", "testrepo").unwrap(); | |
| 633 | - | let oid = resolve_ref(&open_gix(&bare_path), "main").unwrap(); | |
| 624 | + | let (_tmp, bare_path) = make_test_repo(); | |
| 625 | + | let repo = open_gix(&bare_path); | |
| 626 | + | let oid = resolve_ref(&repo, "main").unwrap(); | |
| 634 | 627 | ||
| 635 | - | let commits = commit_log(&repo, to_git2_oid(oid), 10, 0).unwrap(); | |
| 628 | + | let commits = commit_log(&repo, oid, 10, 0).unwrap(); | |
| 636 | 629 | assert_eq!(commits.len(), 1); | |
| 637 | 630 | assert_eq!(commits[0].summary, "Initial commit"); | |
| 638 | 631 | assert_eq!(commits[0].author_name, "Test"); | |
| @@ -662,11 +655,11 @@ | |||
| 662 | 655 | ||
| 663 | 656 | #[test] | |
| 664 | 657 | fn repo_info_reads_description() { | |
| 665 | - | let (tmp, bare_path) = make_test_repo(); | |
| 658 | + | let (_tmp, bare_path) = make_test_repo(); | |
| 666 | 659 | // Write a custom description | |
| 667 | 660 | std::fs::write(bare_path.join("description"), "A test repository").unwrap(); | |
| 668 | 661 | ||
| 669 | - | let repo = open_repo(tmp.path(), "owner", "testrepo").unwrap(); | |
| 662 | + | let repo = open_gix(&bare_path); | |
| 670 | 663 | let info = repo_info(&repo, "testrepo"); | |
| 671 | 664 | assert_eq!(info.name, "testrepo"); | |
| 672 | 665 | assert_eq!(info.description.as_deref(), Some("A test repository")); | |
| @@ -675,8 +668,8 @@ | |||
| 675 | 668 | ||
| 676 | 669 | #[test] | |
| 677 | 670 | fn repo_info_ignores_default_description() { | |
| 678 | - | let (tmp, _) = make_test_repo(); | |
| 679 | - | let repo = open_repo(tmp.path(), "owner", "testrepo").unwrap(); | |
| 671 | + | let (_tmp, bare_path) = make_test_repo(); | |
| 672 | + | let repo = open_gix(&bare_path); | |
| 680 | 673 | let info = repo_info(&repo, "testrepo"); | |
| 681 | 674 | assert!(info.description.is_none()); | |
| 682 | 675 | } | |
| @@ -747,9 +740,9 @@ | |||
| 747 | 740 | ||
| 748 | 741 | #[test] | |
| 749 | 742 | fn file_commit_log_filters_by_path() { | |
| 750 | - | let (tmp, bare_path) = make_two_commit_repo(); | |
| 751 | - | let repo = open_repo(tmp.path(), "owner", "testrepo").unwrap(); | |
| 752 | - | let oid = to_git2_oid(resolve_ref(&open_gix(&bare_path), "main").unwrap()); | |
| 743 | + | let (_tmp, bare_path) = make_two_commit_repo(); | |
| 744 | + | let repo = open_gix(&bare_path); | |
| 745 | + | let oid = resolve_ref(&repo, "main").unwrap(); | |
| 753 | 746 | ||
| 754 | 747 | // src/main.rs was changed in both commits | |
| 755 | 748 | let main_rs_log = file_commit_log(&repo, oid, "src/main.rs", 10, 0, 1000).unwrap(); | |
| @@ -778,11 +771,122 @@ | |||
| 778 | 771 | ); | |
| 779 | 772 | } | |
| 780 | 773 | ||
| 774 | + | #[test] | |
| 775 | + | fn commit_diff_reports_hunks_and_line_numbers() { | |
| 776 | + | let (_tmp, bare_path) = make_two_commit_repo(); | |
| 777 | + | let repo = open_gix(&bare_path); | |
| 778 | + | let head = resolve_ref(&repo, "main").unwrap(); | |
| 779 | + | ||
| 780 | + | let files = commit_diff(&repo, head, 10, 1000).unwrap(); | |
| 781 | + | assert_eq!( | |
| 782 | + | files.len(), | |
| 783 | + | 1, | |
| 784 | + | "only src/main.rs changed in the second commit" | |
| 785 | + | ); | |
| 786 | + | ||
| 787 | + | let file = &files[0]; | |
| 788 | + | assert_eq!(file.path, "src/main.rs"); | |
| 789 | + | assert!(matches!(file.status, DiffStatus::Modified)); | |
| 790 | + | assert_eq!(file.additions, 1); | |
| 791 | + | assert_eq!(file.deletions, 1); | |
| 792 | + | assert!(!file.is_binary); | |
| 793 | + | assert!(!file.truncated); | |
| 794 | + | ||
| 795 | + | let hunk = &file.hunks[0]; | |
| 796 | + | assert!(hunk.header.starts_with("@@")); | |
| 797 | + | let removed: Vec<_> = hunk.lines.iter().filter(|l| l.origin == '-').collect(); | |
| 798 | + | let added: Vec<_> = hunk.lines.iter().filter(|l| l.origin == '+').collect(); | |
| 799 | + | assert_eq!(removed[0].old_lineno, Some(1)); | |
| 800 | + | assert_eq!(removed[0].new_lineno, None); | |
| 801 | + | assert_eq!(added[0].new_lineno, Some(1)); | |
| 802 | + | assert_eq!(added[0].old_lineno, None); | |
| 803 | + | assert!(added[0].content.contains("println!")); | |
| 804 | + | } | |
| 805 | + | ||
| 806 | + | #[test] | |
| 807 | + | fn commit_diff_detects_a_rename() { | |
| 808 | + | // Rename tracking comes from gitoxide's diff configuration. libgit2 was | |
| 809 | + | // called without `find_similar`, so a rename used to render as an | |
| 810 | + | // addition plus a deletion and the Renamed status never appeared. | |
| 811 | + | let tmp = tempfile::TempDir::new().unwrap(); | |
| 812 | + | let bare_path = tmp.path().join("owner").join("rename.git"); | |
| 813 | + | std::fs::create_dir_all(&bare_path).unwrap(); | |
| 814 | + | let bare = Repository::init_bare(&bare_path).unwrap(); | |
| 815 | + | let sig = git2::Signature::now("Test", "test@example.com").unwrap(); | |
| 816 | + | ||
| 817 | + | let body = b"one\ntwo\nthree\nfour\nfive\nsix\n"; | |
| 818 | + | let blob = bare.blob(body).unwrap(); | |
| 819 | + | ||
| 820 | + | let mut tb = bare.treebuilder(None).unwrap(); | |
| 821 | + | tb.insert("before.txt", blob, 0o100_644).unwrap(); | |
| 822 | + | let tree = bare.find_tree(tb.write().unwrap()).unwrap(); | |
| 823 | + | let first = bare | |
| 824 | + | .commit(Some("refs/heads/main"), &sig, &sig, "add", &tree, &[]) | |
| 825 | + | .unwrap(); | |
| 826 | + | bare.set_head("refs/heads/main").unwrap(); | |
| 827 | + | ||
| 828 | + | // Same blob, new name. | |
| 829 | + | let mut tb2 = bare.treebuilder(None).unwrap(); | |
| 830 | + | tb2.insert("after.txt", blob, 0o100_644).unwrap(); | |
| 831 | + | let tree2 = bare.find_tree(tb2.write().unwrap()).unwrap(); | |
| 832 | + | let parent = bare.find_commit(first).unwrap(); | |
| 833 | + | bare.commit( | |
| 834 | + | Some("refs/heads/main"), | |
| 835 | + | &sig, | |
| 836 | + | &sig, | |
| 837 | + | "rename", | |
| 838 | + | &tree2, | |
| 839 | + | &[&parent], | |
| 840 | + | ) | |
| 841 | + | .unwrap(); | |
| 842 | + | ||
| 843 | + | let repo = open_gix(&bare_path); | |
| 844 | + | let head = resolve_ref(&repo, "main").unwrap(); | |
| 845 | + | let files = commit_diff(&repo, head, 10, 1000).unwrap(); | |
| 846 | + | ||
| 847 | + | assert_eq!( | |
| 848 | + | files.len(), | |
| 849 | + | 1, | |
| 850 | + | "a rename is one entry, not an add and a delete" | |
| 851 | + | ); | |
| 852 | + | assert!(matches!(files[0].status, DiffStatus::Renamed)); | |
| 853 | + | assert_eq!(files[0].path, "after.txt"); | |
| 854 | + | assert_eq!(files[0].old_path.as_deref(), Some("before.txt")); | |
| 855 | + | } | |
| 856 | + | ||
| 857 | + | #[test] | |
| 858 | + | fn commit_diff_escapes_html_in_content() { | |
| 859 | + | // Diff lines are rendered into the commit page unescaped by the | |
| 860 | + | // template, so the escaping has to happen here. | |
| 861 | + | let tmp = tempfile::TempDir::new().unwrap(); | |
| 862 | + | let bare_path = tmp.path().join("owner").join("escape.git"); | |
| 863 | + | std::fs::create_dir_all(&bare_path).unwrap(); | |
| 864 | + | let bare = Repository::init_bare(&bare_path).unwrap(); | |
| 865 | + | let sig = git2::Signature::now("Test", "test@example.com").unwrap(); | |
| 866 | + | ||
| 867 | + | let blob = bare.blob(b"<script>alert('x') && y</script>\n").unwrap(); | |
| 868 | + | let mut tb = bare.treebuilder(None).unwrap(); | |
| 869 | + | tb.insert("x.html", blob, 0o100_644).unwrap(); | |
| 870 | + | let tree = bare.find_tree(tb.write().unwrap()).unwrap(); | |
| 871 | + | bare.commit(Some("refs/heads/main"), &sig, &sig, "add", &tree, &[]) | |
| 872 | + | .unwrap(); | |
| 873 | + | bare.set_head("refs/heads/main").unwrap(); | |
| 874 | + | ||
| 875 | + | let repo = open_gix(&bare_path); | |
| 876 | + | let head = resolve_ref(&repo, "main").unwrap(); | |
| 877 | + | let files = commit_diff(&repo, head, 10, 1000).unwrap(); | |
| 878 | + | ||
| 879 | + | let content = &files[0].hunks[0].lines[0].content; | |
| 880 | + | assert!(!content.contains("<script>")); | |
| 881 | + | assert!(content.contains("<script>")); | |
| 882 | + | assert!(content.contains("&&")); | |
| 883 | + | } | |
| 884 | + | ||
| 781 | 885 | #[test] | |
| 782 | 886 | fn file_commit_log_pagination() { | |
| 783 | - | let (tmp, bare_path) = make_two_commit_repo(); | |
| 784 | - | let repo = open_repo(tmp.path(), "owner", "testrepo").unwrap(); | |
| 785 | - | let oid = to_git2_oid(resolve_ref(&open_gix(&bare_path), "main").unwrap()); | |
| 887 | + | let (_tmp, bare_path) = make_two_commit_repo(); | |
| 888 | + | let repo = open_gix(&bare_path); | |
| 889 | + | let oid = resolve_ref(&repo, "main").unwrap(); | |
| 786 | 890 | ||
| 787 | 891 | // Get first commit only (limit=1) | |
| 788 | 892 | let page1 = file_commit_log(&repo, oid, "src/main.rs", 1, 0, 1000).unwrap(); |
| @@ -48,8 +48,8 @@ | |||
| 48 | 48 | .await?; | |
| 49 | 49 | let repo_name_c = repo_name.clone(); | |
| 50 | 50 | let (info, refs, tree_items, readme_html) = resolved | |
| 51 | - | .with_repo(move |repo, gix_repo| { | |
| 52 | - | let info = git::repo_info(repo, &repo_name_c); | |
| 51 | + | .with_repo(move |gix_repo| { | |
| 52 | + | let info = git::repo_info(gix_repo, &repo_name_c); | |
| 53 | 53 | let refs = git::list_refs(gix_repo); | |
| 54 | 54 | let commit_oid = git::resolve_ref(gix_repo, &info.default_branch)?; | |
| 55 | 55 | let tree_items = git::list_tree(gix_repo, commit_oid, "")?; | |
| @@ -111,7 +111,7 @@ | |||
| 111 | 111 | .await?; | |
| 112 | 112 | let git_ref_c = git_ref.clone(); | |
| 113 | 113 | let (refs, tree_items, readme_html) = resolved | |
| 114 | - | .with_repo(move |_repo, gix_repo| { | |
| 114 | + | .with_repo(move |gix_repo| { | |
| 115 | 115 | let refs = git::list_refs(gix_repo); | |
| 116 | 116 | let commit_oid = git::resolve_ref(gix_repo, &git_ref_c)?; | |
| 117 | 117 | let tree_items = git::list_tree(gix_repo, commit_oid, "")?; | |
| @@ -186,7 +186,7 @@ | |||
| 186 | 186 | let git_ref_c = git_ref.clone(); | |
| 187 | 187 | let path_c = path.clone(); | |
| 188 | 188 | let dir_or_file = resolved | |
| 189 | - | .with_repo(move |_repo, gix_repo| { | |
| 189 | + | .with_repo(move |gix_repo| { | |
| 190 | 190 | let commit_oid = git::resolve_ref(gix_repo, &git_ref_c)?; | |
| 191 | 191 | match git::list_tree(gix_repo, commit_oid, &path_c) { | |
| 192 | 192 | Ok(tree_items) => Ok(DirOrFile::Dir(tree_items, git::list_refs(gix_repo))), | |
| @@ -300,10 +300,10 @@ | |||
| 300 | 300 | ||
| 301 | 301 | let git_ref_c = git_ref.clone(); | |
| 302 | 302 | let (refs, commits) = resolved | |
| 303 | - | .with_repo(move |repo, gix_repo| { | |
| 303 | + | .with_repo(move |gix_repo| { | |
| 304 | 304 | let refs = git::list_refs(gix_repo); | |
| 305 | 305 | let commit_oid = git::resolve_ref(gix_repo, &git_ref_c)?; | |
| 306 | - | let commits = git::commit_log(repo, git::to_git2_oid(commit_oid), limit + 1, offset)?; | |
| 306 | + | let commits = git::commit_log(gix_repo, commit_oid, limit + 1, offset)?; | |
| 307 | 307 | Ok((refs, commits)) | |
| 308 | 308 | }) | |
| 309 | 309 | .await?; | |
| @@ -351,20 +351,20 @@ | |||
| 351 | 351 | ) | |
| 352 | 352 | .await?; | |
| 353 | 353 | ||
| 354 | - | let oid = git2::Oid::from_str(&oid_str).map_err(|_| AppError::NotFound)?; | |
| 354 | + | let oid = gix::ObjectId::from_hex(oid_str.as_bytes()).map_err(|_| AppError::NotFound)?; | |
| 355 | 355 | let repo_name_c = repo_name.clone(); | |
| 356 | 356 | let (detail, diff_files, refs, info) = resolved | |
| 357 | - | .with_repo(move |repo, gix_repo| { | |
| 358 | - | repo.find_commit(oid).map_err(|_| AppError::NotFound)?; | |
| 359 | - | let detail = git::commit_detail(repo, oid)?; | |
| 357 | + | .with_repo(move |gix_repo| { | |
| 358 | + | gix_repo.find_commit(oid).map_err(|_| AppError::NotFound)?; | |
| 359 | + | let detail = git::commit_detail(gix_repo, oid)?; | |
| 360 | 360 | let diff_files = git::commit_diff( | |
| 361 | - | repo, | |
| 361 | + | gix_repo, | |
| 362 | 362 | oid, | |
| 363 | 363 | constants::GIT_DIFF_MAX_FILES, | |
| 364 | 364 | constants::GIT_DIFF_MAX_LINES, | |
| 365 | 365 | )?; | |
| 366 | 366 | let refs = git::list_refs(gix_repo); | |
| 367 | - | let info = git::repo_info(repo, &repo_name_c); | |
| 367 | + | let info = git::repo_info(gix_repo, &repo_name_c); | |
| 368 | 368 | Ok((detail, diff_files, refs, info)) | |
| 369 | 369 | }) | |
| 370 | 370 | .await?; | |
| @@ -417,9 +417,9 @@ | |||
| 417 | 417 | let git_ref_c = git_ref.clone(); | |
| 418 | 418 | let path_c = path.clone(); | |
| 419 | 419 | let (blame_lines, refs) = resolved | |
| 420 | - | .with_repo(move |repo, gix_repo| { | |
| 420 | + | .with_repo(move |gix_repo| { | |
| 421 | 421 | let commit_oid = git::resolve_ref(gix_repo, &git_ref_c)?; | |
| 422 | - | let blame_lines = git::blame_file(repo, git::to_git2_oid(commit_oid), &path_c)?; | |
| 422 | + | let blame_lines = git::blame_file(gix_repo, commit_oid, &path_c)?; | |
| 423 | 423 | let refs = git::list_refs(gix_repo); | |
| 424 | 424 | Ok((blame_lines, refs)) | |
| 425 | 425 | }) | |
| @@ -544,12 +544,12 @@ | |||
| 544 | 544 | let git_ref_c = git_ref.clone(); | |
| 545 | 545 | let path_c = path.clone(); | |
| 546 | 546 | let (refs, commits) = resolved | |
| 547 | - | .with_repo(move |repo, gix_repo| { | |
| 547 | + | .with_repo(move |gix_repo| { | |
| 548 | 548 | let refs = git::list_refs(gix_repo); | |
| 549 | 549 | let commit_oid = git::resolve_ref(gix_repo, &git_ref_c)?; | |
| 550 | 550 | let commits = git::file_commit_log( | |
| 551 | - | repo, | |
| 552 | - | git::to_git2_oid(commit_oid), | |
| 551 | + | gix_repo, | |
| 552 | + | commit_oid, | |
| 553 | 553 | &path_c, | |
| 554 | 554 | limit + 1, | |
| 555 | 555 | offset, |
| @@ -149,25 +149,18 @@ | |||
| 149 | 149 | } | |
| 150 | 150 | ||
| 151 | 151 | impl ResolvedRepo { | |
| 152 | - | /// Run a synchronous git closure on the blocking pool against freshly opened | |
| 153 | - | /// handles. `git2::Repository` is `!Send`, so the handle is reopened inside | |
| 154 | - | /// the closure rather than moved across the await. Keeps the disk + zlib | |
| 155 | - | /// work off the Tokio worker threads (Run #14 Perf LOW). | |
| 156 | - | /// | |
| 157 | - | /// Both handles are passed while the port from libgit2 to gitoxide is in | |
| 158 | - | /// flight: ported reads take the `gix` handle, the rest still take the | |
| 159 | - | /// `git2` one. The `git2` parameter goes away with the last git2 read path, | |
| 160 | - | /// and the reopen itself can go with it, since `gix::Repository` is `Send`. | |
| 152 | + | /// Run a synchronous git closure on the blocking pool against a freshly | |
| 153 | + | /// opened handle. Keeps the disk + zlib work off the Tokio worker threads | |
| 154 | + | /// (Run #14 Perf LOW). | |
| 161 | 155 | pub(crate) async fn with_repo<T, F>(&self, f: F) -> Result<T> | |
| 162 | 156 | where | |
| 163 | 157 | T: Send + 'static, | |
| 164 | - | F: FnOnce(&git2::Repository, &gix::Repository) -> Result<T> + Send + 'static, | |
| 158 | + | F: FnOnce(&gix::Repository) -> Result<T> + Send + 'static, | |
| 165 | 159 | { | |
| 166 | 160 | let path = self.repo_path.clone(); | |
| 167 | 161 | tokio::task::spawn_blocking(move || { | |
| 168 | - | let repo = git::open_repo_at(&path)?; | |
| 169 | - | let gix_repo = git::open_gix_repo_at(&path)?; | |
| 170 | - | f(&repo, &gix_repo) | |
| 162 | + | let repo = git::open_gix_repo_at(&path)?; | |
| 163 | + | f(&repo) | |
| 171 | 164 | }) | |
| 172 | 165 | .await | |
| 173 | 166 | .map_err(|e| AppError::Internal(anyhow::anyhow!("git worker task failed: {e}")))? |
| @@ -42,7 +42,7 @@ | |||
| 42 | 42 | let git_ref_c = git_ref.clone(); | |
| 43 | 43 | let path_c = path.clone(); | |
| 44 | 44 | let content: Vec<u8> = resolved | |
| 45 | - | .with_repo(move |_repo, gix_repo| { | |
| 45 | + | .with_repo(move |gix_repo| { | |
| 46 | 46 | let commit_oid = git::resolve_ref(gix_repo, &git_ref_c)?; | |
| 47 | 47 | let commit = gix_repo | |
| 48 | 48 | .find_commit(commit_oid) |
| @@ -53,15 +53,15 @@ | |||
| 53 | 53 | }; | |
| 54 | 54 | let owner = owner.to_string(); | |
| 55 | 55 | let repo_name = repo_name.to_string(); | |
| 56 | - | // libgit2 open + repo_info are blocking filesystem work; run them on the | |
| 57 | - | // blocking pool so a nav-bar ref lookup can't stall a worker thread | |
| 56 | + | // Opening the repo and reading its info is blocking filesystem work; run it | |
| 57 | + | // on the blocking pool so a nav-bar ref lookup can't stall a worker thread | |
| 58 | 58 | // (ultra-fuzz Run 10 Perf S4). | |
| 59 | - | tokio::task::spawn_blocking( | |
| 60 | - | move || match crate::git::open_repo(&root, &owner, &repo_name) { | |
| 59 | + | tokio::task::spawn_blocking(move || { | |
| 60 | + | match crate::git::open_gix_repo(&root, &owner, &repo_name) { | |
| 61 | 61 | Ok(repo) => crate::git::repo_info(&repo, &repo_name).default_branch, | |
| 62 | 62 | Err(_) => "main".to_string(), | |
| 63 | - | }, | |
| 64 | - | ) | |
| 63 | + | } | |
| 64 | + | }) | |
| 65 | 65 | .await | |
| 66 | 66 | .unwrap_or_else(|_| "main".to_string()) | |
| 67 | 67 | } |