git: Compute and synchronize diffs from HEAD (#23626)
This PR builds on #21258 to make it possible to use HEAD as a diff base. The buffer store is extended to support holding multiple change sets, and collab gains support for synchronizing the committed text of files when any collaborator requires it. Not implemented in this PR: - Exposing the diff from HEAD to the user - Decorating the diff from HEAD with information about which hunks are staged `test_random_multibuffer` now fails first at `SEED=13277`, similar to the previous high-water mark, but with various bugs in the multibuffer logic now shaken out. Release Notes: - N/A --------- Co-authored-by: Max <max@zed.dev> Co-authored-by: Ben <ben@zed.dev> Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com> Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com> Co-authored-by: Conrad <conrad@zed.dev>
This commit is contained in:
parent
871f98bc4d
commit
5704b50fb1
29 changed files with 2799 additions and 603 deletions
|
@ -74,31 +74,34 @@ impl BufferDiff {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn build(diff_base: &str, buffer: &text::BufferSnapshot) -> Self {
|
||||
pub fn build(diff_base: Option<&str>, buffer: &text::BufferSnapshot) -> Self {
|
||||
let mut tree = SumTree::new(buffer);
|
||||
|
||||
let buffer_text = buffer.as_rope().to_string();
|
||||
let patch = Self::diff(diff_base, &buffer_text);
|
||||
if let Some(diff_base) = diff_base {
|
||||
let buffer_text = buffer.as_rope().to_string();
|
||||
let patch = Self::diff(diff_base, &buffer_text);
|
||||
|
||||
// A common case in Zed is that the empty buffer is represented as just a newline,
|
||||
// but if we just compute a naive diff you get a "preserved" line in the middle,
|
||||
// which is a bit odd.
|
||||
if buffer_text == "\n" && diff_base.ends_with("\n") && diff_base.len() > 1 {
|
||||
tree.push(
|
||||
InternalDiffHunk {
|
||||
buffer_range: buffer.anchor_before(0)..buffer.anchor_before(0),
|
||||
diff_base_byte_range: 0..diff_base.len() - 1,
|
||||
},
|
||||
buffer,
|
||||
);
|
||||
return Self { tree };
|
||||
}
|
||||
// A common case in Zed is that the empty buffer is represented as just a newline,
|
||||
// but if we just compute a naive diff you get a "preserved" line in the middle,
|
||||
// which is a bit odd.
|
||||
if buffer_text == "\n" && diff_base.ends_with("\n") && diff_base.len() > 1 {
|
||||
tree.push(
|
||||
InternalDiffHunk {
|
||||
buffer_range: buffer.anchor_before(0)..buffer.anchor_before(0),
|
||||
diff_base_byte_range: 0..diff_base.len() - 1,
|
||||
},
|
||||
buffer,
|
||||
);
|
||||
return Self { tree };
|
||||
}
|
||||
|
||||
if let Some(patch) = patch {
|
||||
let mut divergence = 0;
|
||||
for hunk_index in 0..patch.num_hunks() {
|
||||
let hunk = Self::process_patch_hunk(&patch, hunk_index, buffer, &mut divergence);
|
||||
tree.push(hunk, buffer);
|
||||
if let Some(patch) = patch {
|
||||
let mut divergence = 0;
|
||||
for hunk_index in 0..patch.num_hunks() {
|
||||
let hunk =
|
||||
Self::process_patch_hunk(&patch, hunk_index, buffer, &mut divergence);
|
||||
tree.push(hunk, buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -125,11 +128,14 @@ impl BufferDiff {
|
|||
range: Range<Anchor>,
|
||||
buffer: &'a BufferSnapshot,
|
||||
) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||
let range = range.to_offset(buffer);
|
||||
|
||||
let mut cursor = self
|
||||
.tree
|
||||
.filter::<_, DiffHunkSummary>(buffer, move |summary| {
|
||||
let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
|
||||
let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt();
|
||||
let summary_range = summary.buffer_range.to_offset(buffer);
|
||||
let before_start = summary_range.end < range.start;
|
||||
let after_end = summary_range.start > range.end;
|
||||
!before_start && !after_end
|
||||
});
|
||||
|
||||
|
@ -151,21 +157,25 @@ impl BufferDiff {
|
|||
});
|
||||
|
||||
let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter);
|
||||
iter::from_fn(move || {
|
||||
iter::from_fn(move || loop {
|
||||
let (start_point, (start_anchor, start_base)) = summaries.next()?;
|
||||
let (mut end_point, (mut end_anchor, end_base)) = summaries.next()?;
|
||||
|
||||
if !start_anchor.is_valid(buffer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if end_point.column > 0 {
|
||||
end_point.row += 1;
|
||||
end_point.column = 0;
|
||||
end_anchor = buffer.anchor_before(end_point);
|
||||
}
|
||||
|
||||
Some(DiffHunk {
|
||||
return Some(DiffHunk {
|
||||
row_range: start_point.row..end_point.row,
|
||||
diff_base_byte_range: start_base..end_base,
|
||||
buffer_range: start_anchor..end_anchor,
|
||||
})
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -270,7 +280,7 @@ impl BufferDiff {
|
|||
}
|
||||
|
||||
pub fn update(&mut self, diff_base: &Rope, buffer: &text::BufferSnapshot) {
|
||||
*self = Self::build(&diff_base.to_string(), buffer);
|
||||
*self = Self::build(Some(&diff_base.to_string()), buffer);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -536,7 +546,7 @@ mod tests {
|
|||
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text_1);
|
||||
|
||||
let empty_diff = BufferDiff::new(&buffer);
|
||||
let diff_1 = BufferDiff::build(&base_text, &buffer);
|
||||
let diff_1 = BufferDiff::build(Some(&base_text), &buffer);
|
||||
let range = diff_1.compare(&empty_diff, &buffer).unwrap();
|
||||
assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
|
||||
|
||||
|
@ -554,7 +564,7 @@ mod tests {
|
|||
"
|
||||
.unindent(),
|
||||
);
|
||||
let diff_2 = BufferDiff::build(&base_text, &buffer);
|
||||
let diff_2 = BufferDiff::build(Some(&base_text), &buffer);
|
||||
assert_eq!(None, diff_2.compare(&diff_1, &buffer));
|
||||
|
||||
// Edit turns a deletion hunk into a modification.
|
||||
|
@ -571,7 +581,7 @@ mod tests {
|
|||
"
|
||||
.unindent(),
|
||||
);
|
||||
let diff_3 = BufferDiff::build(&base_text, &buffer);
|
||||
let diff_3 = BufferDiff::build(Some(&base_text), &buffer);
|
||||
let range = diff_3.compare(&diff_2, &buffer).unwrap();
|
||||
assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0));
|
||||
|
||||
|
@ -588,7 +598,7 @@ mod tests {
|
|||
"
|
||||
.unindent(),
|
||||
);
|
||||
let diff_4 = BufferDiff::build(&base_text, &buffer);
|
||||
let diff_4 = BufferDiff::build(Some(&base_text), &buffer);
|
||||
let range = diff_4.compare(&diff_3, &buffer).unwrap();
|
||||
assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0));
|
||||
|
||||
|
@ -606,7 +616,7 @@ mod tests {
|
|||
"
|
||||
.unindent(),
|
||||
);
|
||||
let diff_5 = BufferDiff::build(&base_text, &buffer);
|
||||
let diff_5 = BufferDiff::build(Some(&base_text), &buffer);
|
||||
let range = diff_5.compare(&diff_4, &buffer).unwrap();
|
||||
assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0));
|
||||
|
||||
|
@ -624,7 +634,7 @@ mod tests {
|
|||
"
|
||||
.unindent(),
|
||||
);
|
||||
let diff_6 = BufferDiff::build(&base_text, &buffer);
|
||||
let diff_6 = BufferDiff::build(Some(&base_text), &buffer);
|
||||
let range = diff_6.compare(&diff_5, &buffer).unwrap();
|
||||
assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));
|
||||
}
|
||||
|
|
|
@ -29,9 +29,15 @@ pub struct Branch {
|
|||
pub trait GitRepository: Send + Sync {
|
||||
fn reload_index(&self);
|
||||
|
||||
/// Loads a git repository entry's contents.
|
||||
/// Returns the contents of an entry in the repository's index, or None if there is no entry for the given path.
|
||||
///
|
||||
/// Note that for symlink entries, this will return the contents of the symlink, not the target.
|
||||
fn load_index_text(&self, relative_file_path: &Path) -> Option<String>;
|
||||
fn load_index_text(&self, path: &RepoPath) -> Option<String>;
|
||||
|
||||
/// Returns the contents of an entry in the repository's HEAD, or None if HEAD does not exist or has no entry for the given path.
|
||||
///
|
||||
/// Note that for symlink entries, this will return the contents of the symlink, not the target.
|
||||
fn load_committed_text(&self, path: &RepoPath) -> Option<String>;
|
||||
|
||||
/// Returns the URL of the remote with the given name.
|
||||
fn remote_url(&self, name: &str) -> Option<String>;
|
||||
|
@ -106,15 +112,15 @@ impl GitRepository for RealGitRepository {
|
|||
repo.path().into()
|
||||
}
|
||||
|
||||
fn load_index_text(&self, relative_file_path: &Path) -> Option<String> {
|
||||
fn logic(repo: &git2::Repository, relative_file_path: &Path) -> Result<Option<String>> {
|
||||
fn load_index_text(&self, path: &RepoPath) -> Option<String> {
|
||||
fn logic(repo: &git2::Repository, path: &RepoPath) -> Result<Option<String>> {
|
||||
const STAGE_NORMAL: i32 = 0;
|
||||
let index = repo.index()?;
|
||||
|
||||
// This check is required because index.get_path() unwraps internally :(
|
||||
check_path_to_repo_path_errors(relative_file_path)?;
|
||||
check_path_to_repo_path_errors(path)?;
|
||||
|
||||
let oid = match index.get_path(relative_file_path, STAGE_NORMAL) {
|
||||
let oid = match index.get_path(path, STAGE_NORMAL) {
|
||||
Some(entry) if entry.mode != GIT_MODE_SYMLINK => entry.id,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
@ -123,13 +129,22 @@ impl GitRepository for RealGitRepository {
|
|||
Ok(Some(String::from_utf8(content)?))
|
||||
}
|
||||
|
||||
match logic(&self.repository.lock(), relative_file_path) {
|
||||
match logic(&self.repository.lock(), path) {
|
||||
Ok(value) => return value,
|
||||
Err(err) => log::error!("Error loading head text: {:?}", err),
|
||||
Err(err) => log::error!("Error loading index text: {:?}", err),
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn load_committed_text(&self, path: &RepoPath) -> Option<String> {
|
||||
let repo = self.repository.lock();
|
||||
let head = repo.head().ok()?.peel_to_tree().log_err()?;
|
||||
let oid = head.get_path(path).ok()?.id();
|
||||
let content = repo.find_blob(oid).log_err()?.content().to_owned();
|
||||
let content = String::from_utf8(content).log_err()?;
|
||||
Some(content)
|
||||
}
|
||||
|
||||
fn remote_url(&self, name: &str) -> Option<String> {
|
||||
let repo = self.repository.lock();
|
||||
let remote = repo.find_remote(name).ok()?;
|
||||
|
@ -325,8 +340,9 @@ pub struct FakeGitRepository {
|
|||
pub struct FakeGitRepositoryState {
|
||||
pub dot_git_dir: PathBuf,
|
||||
pub event_emitter: smol::channel::Sender<PathBuf>,
|
||||
pub index_contents: HashMap<PathBuf, String>,
|
||||
pub blames: HashMap<PathBuf, Blame>,
|
||||
pub head_contents: HashMap<RepoPath, String>,
|
||||
pub index_contents: HashMap<RepoPath, String>,
|
||||
pub blames: HashMap<RepoPath, Blame>,
|
||||
pub statuses: HashMap<RepoPath, FileStatus>,
|
||||
pub current_branch_name: Option<String>,
|
||||
pub branches: HashSet<String>,
|
||||
|
@ -343,6 +359,7 @@ impl FakeGitRepositoryState {
|
|||
FakeGitRepositoryState {
|
||||
dot_git_dir,
|
||||
event_emitter,
|
||||
head_contents: Default::default(),
|
||||
index_contents: Default::default(),
|
||||
blames: Default::default(),
|
||||
statuses: Default::default(),
|
||||
|
@ -355,9 +372,14 @@ impl FakeGitRepositoryState {
|
|||
impl GitRepository for FakeGitRepository {
|
||||
fn reload_index(&self) {}
|
||||
|
||||
fn load_index_text(&self, path: &Path) -> Option<String> {
|
||||
fn load_index_text(&self, path: &RepoPath) -> Option<String> {
|
||||
let state = self.state.lock();
|
||||
state.index_contents.get(path).cloned()
|
||||
state.index_contents.get(path.as_ref()).cloned()
|
||||
}
|
||||
|
||||
fn load_committed_text(&self, path: &RepoPath) -> Option<String> {
|
||||
let state = self.state.lock();
|
||||
state.head_contents.get(path.as_ref()).cloned()
|
||||
}
|
||||
|
||||
fn remote_url(&self, _name: &str) -> Option<String> {
|
||||
|
@ -529,6 +551,12 @@ impl From<&Path> for RepoPath {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Arc<Path>> for RepoPath {
|
||||
fn from(value: Arc<Path>) -> Self {
|
||||
RepoPath(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PathBuf> for RepoPath {
|
||||
fn from(value: PathBuf) -> Self {
|
||||
RepoPath::new(value)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue