-
Notifications
You must be signed in to change notification settings - Fork 1
More Git Dialog UI cleanup #95
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
51ea8dc
d4f686f
654b1fe
b069bc7
90aacf3
3294fb7
53b5978
45ea0de
868a9a2
bb3dfa4
eb1ff02
2bc8cf4
c7fc0bf
3f9e8a6
ba55a8b
c166996
910aaee
e5ba578
166d874
df301f5
c5a8e9c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,106 +1,118 @@ | ||
| #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] | ||
| #[cfg_attr( | ||
| feature = "serde", | ||
| derive(serde::Serialize), | ||
| serde(rename_all = "lowercase") | ||
| )] | ||
| #[derive(PartialEq)] | ||
| enum DiffKind { | ||
| Binary, | ||
| Text, | ||
| } | ||
| use std::collections::hash_map; | ||
|
|
||
| use crate::{Result, error}; | ||
|
|
||
| #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] | ||
| #[cfg_attr( | ||
| feature = "serde", | ||
| derive(serde::Serialize), | ||
| serde(rename_all = "camelCase") | ||
| )] | ||
| struct DiffFile { | ||
| /// Path to the diff file | ||
| path: Option<String>, | ||
| /// Content of the diff file | ||
| content: Option<String>, | ||
| pub(crate) struct DiffStats { | ||
| /// Number of files changed | ||
| files_changed: usize, | ||
| /// Number of insertions | ||
| insertions: usize, | ||
| /// Number of deletions | ||
| deletions: usize, | ||
| /// Number of lines in the old versions of all affected files | ||
| total_old_num_lines: usize, | ||
| } | ||
|
|
||
| impl DiffFile { | ||
| pub fn try_from_repo_and_diff_file( | ||
| repo: &git2::Repository, | ||
| diff_file: &git2::DiffFile, | ||
| ) -> Option<DiffFile> { | ||
| let oid = diff_file.id(); | ||
| if oid.is_zero() { | ||
| return None; | ||
| impl DiffStats { | ||
| fn from_stats_and_total_old_num_lines( | ||
| stats: &git2::DiffStats, | ||
| total_old_num_lines: usize, | ||
| ) -> Self { | ||
| Self { | ||
| files_changed: stats.files_changed(), | ||
| insertions: stats.insertions(), | ||
| deletions: stats.deletions(), | ||
| total_old_num_lines, | ||
| } | ||
|
|
||
| let content = match repo.find_blob(oid) { | ||
| Ok(blob) if diff_file.is_not_binary() => { | ||
| Some(String::from_utf8_lossy(blob.content()).into_owned()) | ||
| } | ||
| _ => { | ||
| log::warn!("Did not find blob for {:?}", diff_file.path()); | ||
| None | ||
| } | ||
| }; | ||
|
|
||
| Some(DiffFile { | ||
| path: diff_file | ||
| .path() | ||
| .map(|p| p.to_str().unwrap_or("unknown").to_owned()), | ||
| content, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| type Path = String; | ||
| type FileContent = String; | ||
|
|
||
| #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] | ||
| #[cfg_attr( | ||
| feature = "serde", | ||
| derive(serde::Serialize), | ||
| serde(rename_all = "camelCase") | ||
| )] | ||
| pub struct Diff { | ||
| /// Old file path and content | ||
| old: Option<DiffFile>, | ||
| /// New file path and content | ||
| new: Option<DiffFile>, | ||
| /// Kind of the diff | ||
| kind: DiffKind, | ||
| /// Patch between old and new | ||
| patch: String, | ||
|
jonasehrlich marked this conversation as resolved.
|
||
| /// Stats of the diff | ||
| stats: DiffStats, | ||
| /// Map of old source paths to the old content | ||
| old_sources: hash_map::HashMap<Path, FileContent>, | ||
| } | ||
|
|
||
| impl Diff { | ||
| pub fn from_repo_and_patch(repo: &git2::Repository, patch: &mut git2::Patch) -> Diff { | ||
| let (old, new, diff_type) = { | ||
| let delta = patch.delta(); | ||
| let diff_type = if delta.flags().is_binary() { | ||
| DiffKind::Binary | ||
| } else { | ||
| DiffKind::Text | ||
| }; | ||
| ( | ||
| DiffFile::try_from_repo_and_diff_file(repo, &delta.old_file()), | ||
| DiffFile::try_from_repo_and_diff_file(repo, &delta.new_file()), | ||
| diff_type, | ||
| ) | ||
| }; | ||
| pub fn try_from_repo_and_diff(repo: &git2::Repository, diff: &git2::Diff) -> Result<Self> { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function got really long and hard to read. Can we split it down?
|
||
| let mut patch_output = String::new(); | ||
| let mut total_num_lines: usize = 0; | ||
| let mut old_files: hash_map::HashMap<Path, FileContent> = hash_map::HashMap::new(); | ||
| // Collect old file contents from each delta | ||
| diff.foreach( | ||
| &mut |delta, _| { | ||
| let oid = delta.old_file().id(); | ||
| if !oid.is_zero() { | ||
| let path = delta | ||
| .old_file() | ||
| .path() | ||
| .map(|p| p.to_string_lossy().to_string()) | ||
| .unwrap_or_else(|| "<unknown>".to_string()); | ||
|
|
||
| let patch_buf = patch.to_buf().expect("failed unwrapping patch buffer"); | ||
| let patch_text = patch_buf.as_str().unwrap_or("").to_owned(); | ||
| Self { | ||
| old, | ||
| new, | ||
| kind: diff_type, | ||
| patch: patch_text, | ||
| } | ||
| } | ||
| if let Ok(blob) = repo.find_blob(oid) { | ||
| if let Ok(content) = std::str::from_utf8(blob.content()) { | ||
| let text = content.to_string(); | ||
| total_num_lines += text.lines().count(); | ||
| old_files.insert(path, text); | ||
| } else { | ||
| old_files.insert(path, "<binary or invalid utf8>".to_string()); | ||
| } | ||
| } | ||
| } | ||
| true | ||
| }, | ||
| None, | ||
| None, | ||
| None, | ||
| ) | ||
| .map_err(|e| error::Error::from_ctx_and_error("Error getting old file contents", e))?; | ||
|
|
||
| pub fn binary_from_repo_and_delta(repo: &git2::Repository, delta: &git2::DiffDelta) -> Diff { | ||
| let old = DiffFile::try_from_repo_and_diff_file(repo, &delta.old_file()); | ||
| let new = DiffFile::try_from_repo_and_diff_file(repo, &delta.new_file()); | ||
| Self { | ||
| old, | ||
| new, | ||
| kind: DiffKind::Binary, | ||
| patch: "".to_string(), | ||
| } | ||
| diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| { | ||
| match line.origin() { | ||
| // For Addition, Deletion, and Context lines, the prefix needs to be prepended | ||
| '+' | '-' | ' ' | '@' => { | ||
| patch_output.push(line.origin()); | ||
| } | ||
| // For any other line type (e.g., file headers, hunk headers), | ||
| // the content is already fully formatted and should not be prefixed. | ||
| _ => {} | ||
| } | ||
| if let Ok(text) = str::from_utf8(line.content()) { | ||
|
jonasehrlich marked this conversation as resolved.
|
||
| patch_output.push_str(text); | ||
| } else { | ||
| patch_output.push_str("<invalid utf8>"); | ||
| } | ||
| true | ||
| }) | ||
| .map_err(|e| error::Error::from_ctx_and_error("Error creating patch", e))?; | ||
|
|
||
| Ok(Self { | ||
| patch: patch_output, | ||
| stats: DiffStats::from_stats_and_total_old_num_lines( | ||
| &diff | ||
| .stats() | ||
| .map_err(|e| error::Error::from_ctx_and_error("Error getting diff stats", e))?, | ||
| total_num_lines, | ||
| ), | ||
| old_sources: old_files, | ||
| }) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -92,15 +92,11 @@ impl Repository { | |
| Commit::try_for_revision(&self.repo, rev) | ||
| } | ||
|
|
||
| /// Returns an iterator over diffs between two revisions `base_rev` and `head_rev` | ||
| /// | ||
| /// * `base_rev` - Base revision to use as a tree, uses initial commit if set to `None` | ||
| /// * `head_rev` - Head revision until which to diff. Using current `HEAD` if set to `None` | ||
| pub fn iter_diffs_between_revisions( | ||
| fn git2_diff_for_revisions( | ||
| &self, | ||
| base_rev: Option<&str>, | ||
| head_rev: Option<&str>, | ||
| ) -> Result<impl Iterator<Item = Result<Diff>>> { | ||
| ) -> Result<git2::Diff> { | ||
| let head = head_rev.unwrap_or("HEAD"); | ||
| let tree = utils::get_tree_for_revision(&self.repo, head)?; | ||
|
|
||
|
|
@@ -112,31 +108,25 @@ impl Repository { | |
| None => None, | ||
| }; | ||
|
|
||
| let diff = self | ||
| let mut diff = self | ||
| .repo | ||
| .diff_tree_to_tree(base_tree.as_ref(), Some(&tree), None) | ||
| .map_err(|e| { | ||
| Error::from_ctx_and_error(format!("Failed to diff tree {base_rev:?} to {head}"), e) | ||
| })?; | ||
|
|
||
| let num_deltas = diff.deltas().len(); | ||
|
|
||
| Ok((0..num_deltas).filter_map(move |delta_idx| { | ||
| // `diff` is owned by this function and must not be moved into the closure. | ||
| // Using `move` only for delta_idx, not for diff. | ||
| match git2::Patch::from_diff(&diff, delta_idx) { | ||
| Err(e) => Some(Err(Error::from_ctx_and_error("Failed to create patch", e))), | ||
| Ok(Some(mut patch)) => Some(Ok(Diff::from_repo_and_patch(&self.repo, &mut patch))), | ||
| Ok(None) => { | ||
| if let Some(delta) = diff.get_delta(delta_idx) { | ||
| Some(Ok(Diff::binary_from_repo_and_delta(&self.repo, &delta))) | ||
| } else { | ||
| log::error!("Failed to get delta for idx {delta_idx}"); | ||
| None | ||
| } | ||
| } | ||
| } | ||
| })) | ||
| // Enable rename detection with DiffFindOptions | ||
| let mut find_opts = git2::DiffFindOptions::new(); | ||
| find_opts.renames(true); | ||
| // Transform a diff marking file renames, copies, etc. | ||
| diff.find_similar(Some(&mut find_opts)) | ||
| .map_err(|e| Error::from_ctx_and_error("Failed to find similar files in diff", e))?; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: same as in #92, i don't think we need to propagate the error to the user and can just ignore it |
||
| Ok(diff) | ||
| } | ||
|
|
||
| pub fn diff(&self, base_rev: Option<&str>, head_rev: Option<&str>) -> Result<Diff> { | ||
| let diff = self.git2_diff_for_revisions(base_rev, head_rev)?; | ||
| Diff::try_from_repo_and_diff(self.repo(), &diff) | ||
| } | ||
|
|
||
| /// Returns an iterator over tags in the repository which names contain `filter` | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.