fix: show revision-aware blame in diff buffers
Diff and snapshot sources were excluded before scheduling, and the separate diff renderer never painted attribution. Capture per-side Git provenance, preserve private object stores for historical tabs, and reuse the current-line worker and cache.
This commit is contained in:
+118
-53
@@ -17,7 +17,7 @@ pub(crate) const LIMIT: usize = 16 * 1024 * 1024;
|
||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
||||
static NEXT_TEMP: AtomicU64 = AtomicU64::new(0);
|
||||
type ResultFile = Result<Option<Vec<Option<Arc<LineBlame>>>>, String>;
|
||||
type Request = (u64, u64, PathBuf, String);
|
||||
type Request = (u64, u64, PathBuf, String, Option<RevisionSource>);
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) struct LineBlame {
|
||||
@@ -27,6 +27,14 @@ pub(crate) struct LineBlame {
|
||||
pub(crate) uncommitted: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct RevisionSource {
|
||||
pub(crate) store: PathBuf,
|
||||
pub(crate) commit: String,
|
||||
pub(crate) path: PathBuf,
|
||||
pub(crate) keepalive: Option<Arc<crate::repository::Temporary>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
request: Option<Request>,
|
||||
@@ -63,13 +71,16 @@ impl BlameWorker {
|
||||
if worker.stopped.load(Ordering::Acquire) {
|
||||
break;
|
||||
}
|
||||
let (generation, id, path, contents) = state.request.take().unwrap();
|
||||
let (generation, id, path, contents, source) = state.request.take().unwrap();
|
||||
drop(state);
|
||||
let cancelled = || {
|
||||
worker.stopped.load(Ordering::Acquire)
|
||||
|| worker.generation.load(Ordering::Acquire) != generation
|
||||
};
|
||||
let result = read_file(&path, &contents, &cancelled);
|
||||
let result = match source {
|
||||
Some(source) => read_revision(&source, &contents, &cancelled),
|
||||
None => read_file(&path, &contents, &cancelled),
|
||||
};
|
||||
let Ok(mut state) = worker.state.lock() else {
|
||||
return;
|
||||
};
|
||||
@@ -84,7 +95,13 @@ impl BlameWorker {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn request(&mut self, id: u64, path: PathBuf, contents: String) -> io::Result<()> {
|
||||
pub(crate) fn request(
|
||||
&mut self,
|
||||
id: u64,
|
||||
path: PathBuf,
|
||||
contents: String,
|
||||
source: Option<RevisionSource>,
|
||||
) -> io::Result<()> {
|
||||
if contents.len() > LIMIT {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
@@ -101,7 +118,7 @@ impl BlameWorker {
|
||||
.generation
|
||||
.fetch_add(1, Ordering::AcqRel)
|
||||
.wrapping_add(1);
|
||||
state.request = Some((generation, id, path, contents));
|
||||
state.request = Some((generation, id, path, contents, source));
|
||||
state.result = None;
|
||||
self.shared.wake.notify_one();
|
||||
Ok(())
|
||||
@@ -279,19 +296,93 @@ pub(crate) fn git_without_literal_pathspecs(root: &Path) -> Command {
|
||||
fn oid(value: &str) -> bool {
|
||||
matches!(value.len(), 40 | 64) && value.bytes().all(|byte| byte.is_ascii_hexdigit())
|
||||
}
|
||||
fn path_bytes(bytes: &[u8]) -> Result<PathBuf, String> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::{ffi::OsString, os::unix::ffi::OsStringExt};
|
||||
Ok(PathBuf::from(OsString::from_vec(bytes.to_vec())))
|
||||
fn read_revision(
|
||||
source: &RevisionSource,
|
||||
contents: &str,
|
||||
cancelled: &dyn Fn() -> bool,
|
||||
) -> ResultFile {
|
||||
if contents.len() > LIMIT {
|
||||
return Err("Blame input exceeds 16 MiB".into());
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
Ok(PathBuf::from(
|
||||
std::str::from_utf8(bytes).map_err(|_| "Invalid repository path")?,
|
||||
))
|
||||
if !oid(&source.commit) {
|
||||
return Err("Invalid blame commit".into());
|
||||
}
|
||||
crate::repository::safe_path(&source.path)?;
|
||||
let rows = contents.split_inclusive('\n').count();
|
||||
if rows == 0 || source.commit.bytes().all(|byte| byte == b'0') {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut objects = git(&source.store);
|
||||
objects.arg("--git-dir").arg(&source.store).args([
|
||||
"rev-parse",
|
||||
"--path-format=absolute",
|
||||
"--git-path",
|
||||
"objects",
|
||||
]);
|
||||
let objects = run(objects, &[], cancelled)?.ok_or("Cannot resolve blame object store")?;
|
||||
let private = Temporary::new()?;
|
||||
fs::create_dir(private.0.join("refs"))
|
||||
.and_then(|()| fs::create_dir_all(private.0.join("objects/info")))
|
||||
.map_err(|_| "Cannot prepare private blame repository")?;
|
||||
// Keep relative alternates anchored in their original object directory.
|
||||
let mut alternate = vec![b'"'];
|
||||
for &byte in objects.strip_suffix(b"\n").unwrap_or(&objects) {
|
||||
if (b' '..=b'~').contains(&byte) && !matches!(byte, b'"' | b'\\') {
|
||||
alternate.push(byte);
|
||||
} else {
|
||||
write!(&mut alternate, "\\{byte:03o}").unwrap();
|
||||
}
|
||||
}
|
||||
alternate.extend_from_slice(b"\"\n");
|
||||
fs::write(private.0.join("objects/info/alternates"), alternate)
|
||||
.and_then(|()| fs::write(private.0.join("HEAD"), format!("{}\n", source.commit)))
|
||||
.map_err(|_| "Cannot prepare blame parent")?;
|
||||
if source.commit.len() == 64 {
|
||||
fs::write(
|
||||
private.0.join("config"),
|
||||
"[core]\nrepositoryformatversion = 1\n[extensions]\nobjectFormat = sha256\n",
|
||||
)
|
||||
.map_err(|_| "Cannot prepare blame object format")?;
|
||||
}
|
||||
let command = || {
|
||||
let mut command = git(&private.0);
|
||||
command
|
||||
.arg("--git-dir")
|
||||
.arg(&private.0)
|
||||
.arg("--work-tree")
|
||||
.arg(&private.0);
|
||||
command
|
||||
};
|
||||
let mut present = command();
|
||||
present
|
||||
.args(["ls-tree", "-r", "-z", "--full-tree", "HEAD", "--"])
|
||||
.arg(&source.path);
|
||||
let Some(entry) = run(present, &[], cancelled)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(tab) = entry.iter().position(|&byte| byte == b'\t') else {
|
||||
return Ok(None);
|
||||
};
|
||||
if (!entry.starts_with(b"100644 blob ") && !entry.starts_with(b"100755 blob "))
|
||||
|| entry[tab + 1..].strip_suffix(b"\0") != Some(source.path.as_os_str().as_encoded_bytes())
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
let mut blame = command();
|
||||
blame
|
||||
.args([
|
||||
"blame",
|
||||
"--incremental",
|
||||
"--no-textconv",
|
||||
"--contents",
|
||||
"-",
|
||||
"--",
|
||||
])
|
||||
.arg(&source.path);
|
||||
let output = run(blame, contents.as_bytes(), cancelled)?.ok_or("Cannot read Git blame")?;
|
||||
parse(&output, rows).map(Some)
|
||||
}
|
||||
|
||||
fn read_file(path: &Path, contents: &str, cancelled: &dyn Fn() -> bool) -> ResultFile {
|
||||
if contents.len() > LIMIT {
|
||||
return Err("Blame input exceeds 16 MiB".into());
|
||||
@@ -320,7 +411,6 @@ fn read_file(path: &Path, contents: &str, cancelled: &dyn Fn() -> bool) -> Resul
|
||||
}) {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut temporary = None;
|
||||
let mut command = git(root);
|
||||
if root.join(".jj").exists() {
|
||||
let mut jj = Command::new("jj");
|
||||
@@ -352,44 +442,19 @@ fn read_file(path: &Path, contents: &str, cancelled: &dyn Fn() -> bool) -> Resul
|
||||
if !oid(parent) {
|
||||
return Err("Invalid Jujutsu blame metadata".into());
|
||||
}
|
||||
let mut repo = root.join(".jj/repo");
|
||||
if repo.is_file() {
|
||||
let pointer = fs::read(&repo).map_err(|_| "Cannot read Jujutsu repository pointer")?;
|
||||
repo = root
|
||||
.join(".jj")
|
||||
.join(path_bytes(pointer.strip_suffix(b"\n").unwrap_or(&pointer))?);
|
||||
}
|
||||
let store = repo.join("store");
|
||||
let target = fs::read(store.join("git_target"))
|
||||
.map_err(|_| "Only Git-backed Jujutsu blame is supported")?;
|
||||
let store = fs::canonicalize(
|
||||
store.join(path_bytes(target.strip_suffix(b"\n").unwrap_or(&target))?),
|
||||
)
|
||||
.map_err(|_| "Cannot resolve Jujutsu Git store")?;
|
||||
let private = Temporary::new()?;
|
||||
fs::create_dir(private.0.join("refs"))
|
||||
.map_err(|_| "Cannot prepare private blame repository")?;
|
||||
#[cfg(unix)]
|
||||
std::os::unix::fs::symlink(store.join("objects"), private.0.join("objects"))
|
||||
.map_err(|_| "Cannot link blame objects")?;
|
||||
#[cfg(not(unix))]
|
||||
return Err("Jujutsu blame requires Unix object links".into());
|
||||
fs::write(private.0.join("HEAD"), format!("{parent}\n"))
|
||||
.map_err(|_| "Cannot prepare blame parent")?;
|
||||
command
|
||||
.arg("--git-dir")
|
||||
.arg(&private.0)
|
||||
.arg("--work-tree")
|
||||
.arg(root);
|
||||
temporary = Some(private);
|
||||
let store = crate::repository::store_for_jj(root)?;
|
||||
return read_revision(
|
||||
&RevisionSource {
|
||||
store,
|
||||
commit: parent.into(),
|
||||
path: relative.to_path_buf(),
|
||||
keepalive: None,
|
||||
},
|
||||
contents,
|
||||
cancelled,
|
||||
);
|
||||
}
|
||||
let mut present = if let Some(private) = &temporary {
|
||||
let mut command = git(root);
|
||||
command.arg("--git-dir").arg(&private.0);
|
||||
command
|
||||
} else {
|
||||
git(root)
|
||||
};
|
||||
let mut present = git(root);
|
||||
let mut object = std::ffi::OsString::from("HEAD:");
|
||||
object.push(relative);
|
||||
present.args(["cat-file", "-e"]).arg(object);
|
||||
|
||||
+11
@@ -4,6 +4,7 @@ use std::{
|
||||
borrow::Cow,
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
@@ -141,10 +142,19 @@ pub struct ChangedFile {
|
||||
pub hunks: Vec<Hunk>,
|
||||
pub notice: Option<String>,
|
||||
}
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BlameRevision {
|
||||
pub store: PathBuf,
|
||||
pub commit: String,
|
||||
pub(crate) keepalive: Option<Arc<crate::repository::Temporary>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Comparison {
|
||||
/// Immutable comparison-context IDs; captured review text also freezes live edits.
|
||||
pub revisions: [String; 2],
|
||||
/// Git object stores and immutable attribution baselines for Before/After.
|
||||
pub blame: [Option<BlameRevision>; 2],
|
||||
pub before: String,
|
||||
pub after: String,
|
||||
pub files: Vec<ChangedFile>,
|
||||
@@ -670,6 +680,7 @@ impl DiffState {
|
||||
pub(crate) fn set_resolver_paths(&mut self, root: PathBuf, paths: Vec<PathBuf>, file: usize) {
|
||||
let comparison = self.comparison.get_or_insert_with(|| Comparison {
|
||||
revisions: Default::default(),
|
||||
blame: Default::default(),
|
||||
before: "Resolution".into(),
|
||||
after: "Other version".into(),
|
||||
files: Vec::new(),
|
||||
|
||||
+15
-4
@@ -1952,6 +1952,7 @@ struct Buffer {
|
||||
resolver_readonly: bool,
|
||||
snapshot_document_path: Option<PathBuf>,
|
||||
snapshot_root: Option<PathBuf>,
|
||||
snapshot_blame: Option<Arc<crate::blame::RevisionSource>>,
|
||||
lines: Vec<String>,
|
||||
saved_lines: Vec<String>,
|
||||
// Invalidate whenever text or the saved baseline changes, including temporary swaps.
|
||||
@@ -2012,6 +2013,7 @@ impl Buffer {
|
||||
resolver_readonly: false,
|
||||
snapshot_document_path: None,
|
||||
snapshot_root: None,
|
||||
snapshot_blame: None,
|
||||
saved_lines: lines.clone(),
|
||||
dirty_cache: Cell::new(Some(false)),
|
||||
disk_stamp,
|
||||
@@ -2175,6 +2177,7 @@ pub(crate) struct BlameSource {
|
||||
buffer: usize,
|
||||
revision: u64,
|
||||
pub(crate) path: Arc<Path>,
|
||||
pub(crate) snapshot: Option<Arc<crate::blame::RevisionSource>>,
|
||||
pub(crate) row: usize,
|
||||
save_revision: u64,
|
||||
disk_stamp: Option<FileStamp>,
|
||||
@@ -5116,6 +5119,14 @@ impl Editor {
|
||||
};
|
||||
let source = comparison.root.join(relative);
|
||||
let root = comparison.root.clone();
|
||||
let blame = comparison.blame[side].as_ref().map(|baseline| {
|
||||
Arc::new(crate::blame::RevisionSource {
|
||||
store: baseline.store.clone(),
|
||||
commit: baseline.commit.clone(),
|
||||
keepalive: baseline.keepalive.clone(),
|
||||
path: relative.clone(),
|
||||
})
|
||||
});
|
||||
let lines = diff.snapshot_lines_at(file_index, focus);
|
||||
let key = (
|
||||
diff.comparison_id.expect("snapshot comparison"),
|
||||
@@ -5155,6 +5166,7 @@ impl Editor {
|
||||
buffer.path = Some(source);
|
||||
buffer.snapshot_document_path = Some(document_path);
|
||||
buffer.snapshot_root = Some(root);
|
||||
buffer.snapshot_blame = blame;
|
||||
buffer.lines = lines;
|
||||
buffer.dirty_cache.set(None);
|
||||
buffer
|
||||
@@ -7156,8 +7168,7 @@ impl Editor {
|
||||
pub(crate) fn blame_source(&self) -> Option<BlameSource> {
|
||||
if !self.git_blame
|
||||
|| self.should_quit()
|
||||
|| self.diff.is_some()
|
||||
|| self.active.diff_snapshot.is_some()
|
||||
|| (self.active.diff_snapshot.is_some() && self.active.snapshot_blame.is_none())
|
||||
|| self.active.reload_error.is_some()
|
||||
{
|
||||
return None;
|
||||
@@ -7166,6 +7177,7 @@ impl Editor {
|
||||
buffer: self.buffer_id(),
|
||||
revision: self.text_revision(),
|
||||
path: Arc::from(self.path()?),
|
||||
snapshot: self.active.snapshot_blame.clone(),
|
||||
row: self.cursor().row,
|
||||
save_revision: self.active.save_revision,
|
||||
disk_stamp: self.active.disk_stamp,
|
||||
@@ -7177,13 +7189,12 @@ impl Editor {
|
||||
pub(crate) fn blame_file_source_is_current(&self, source: &BlameSource) -> bool {
|
||||
self.git_blame
|
||||
&& !self.should_quit()
|
||||
&& self.diff.is_none()
|
||||
&& self.active.diff_snapshot.is_none()
|
||||
&& self.active.reload_error.is_none()
|
||||
&& source.buffer == self.buffer_id()
|
||||
&& source.revision == self.text_revision()
|
||||
&& source.save_revision == self.active.save_revision
|
||||
&& source.disk_stamp == self.active.disk_stamp
|
||||
&& source.snapshot == self.active.snapshot_blame
|
||||
&& Some(source.path.as_ref()) == self.path()
|
||||
}
|
||||
|
||||
|
||||
+70
-5
@@ -1,7 +1,7 @@
|
||||
//! Read-only active-repository discovery and isolated revision comparison.
|
||||
use crate::diff::{
|
||||
Catalog, Category, ChangedFile, Comparison, ComparisonMode, FileStatus, PullRequestPage,
|
||||
RepoKind, Revision, RevisionTarget, line_hunks,
|
||||
BlameRevision, Catalog, Category, ChangedFile, Comparison, ComparisonMode, FileStatus,
|
||||
PullRequestPage, RepoKind, Revision, RevisionTarget, line_hunks,
|
||||
};
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
@@ -28,7 +28,8 @@ const WORKING_SNAPSHOT_LIMIT: usize = 512 * 1024 * 1024;
|
||||
const TIMEOUT: Duration = Duration::from_secs(60);
|
||||
static NEXT_TEMP: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
struct Temporary(PathBuf);
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) struct Temporary(PathBuf);
|
||||
impl Temporary {
|
||||
fn new() -> Result<Self, String> {
|
||||
for _ in 0..100 {
|
||||
@@ -1054,6 +1055,7 @@ struct PreparedComparison {
|
||||
selected: [Revision; 2],
|
||||
selected_ids: [Option<String>; 2],
|
||||
revisions: [String; 2],
|
||||
blame: [Option<BlameRevision>; 2],
|
||||
mode: ComparisonMode,
|
||||
left: Snapshot,
|
||||
right: Snapshot,
|
||||
@@ -1077,7 +1079,7 @@ pub struct Repository {
|
||||
root: PathBuf,
|
||||
kind: RepoKind,
|
||||
store: PathBuf,
|
||||
temporary: Temporary,
|
||||
temporary: Arc<Temporary>,
|
||||
private: Option<(PathBuf, PathBuf)>, // workspace and Git object store
|
||||
remotes: Vec<Remote>,
|
||||
remote_refs: BTreeMap<(String, String), String>,
|
||||
@@ -1128,7 +1130,7 @@ impl Repository {
|
||||
root,
|
||||
kind,
|
||||
store,
|
||||
temporary: Temporary::new()?,
|
||||
temporary: Arc::new(Temporary::new()?),
|
||||
private: None,
|
||||
remotes: vec![],
|
||||
remote_refs: BTreeMap::new(),
|
||||
@@ -2390,6 +2392,48 @@ impl Repository {
|
||||
}
|
||||
Ok(Some(text(checked(output)?)?.trim().into()))
|
||||
}
|
||||
fn blame_baseline(&mut self, target: &RevisionTarget) -> Result<Option<BlameRevision>, String> {
|
||||
let source = if *target == RevisionTarget::Working {
|
||||
let id = if self.kind == RepoKind::Git {
|
||||
self.working_anchor()?
|
||||
} else {
|
||||
let parents = text(self.jj(
|
||||
&self.root,
|
||||
&[
|
||||
"log",
|
||||
"--no-graph",
|
||||
"-r",
|
||||
"@-",
|
||||
"-T",
|
||||
"commit_id ++ \"\\n\"",
|
||||
],
|
||||
)?)?;
|
||||
let mut parents = parents.lines();
|
||||
let parent = parents.next();
|
||||
// A merge has no single attribution baseline.
|
||||
if parents.next().is_some() {
|
||||
None
|
||||
} else {
|
||||
parent.map(str::to_owned)
|
||||
}
|
||||
};
|
||||
id.map(|id| (self.store.clone(), id))
|
||||
} else {
|
||||
Some(self.resolve(target)?)
|
||||
};
|
||||
Ok(source
|
||||
.filter(|(_, id)| !id.bytes().all(|byte| byte == b'0'))
|
||||
.map(|(store, commit)| BlameRevision {
|
||||
keepalive: self
|
||||
.private
|
||||
.as_ref()
|
||||
.filter(|(_, private_store)| private_store == &store)
|
||||
.map(|_| Arc::clone(&self.temporary)),
|
||||
store,
|
||||
commit,
|
||||
}))
|
||||
}
|
||||
|
||||
fn working_advanced(&self, before: Option<&str>, after: Option<&str>) -> Result<bool, String> {
|
||||
if before == after || after.is_none() {
|
||||
return Ok(false);
|
||||
@@ -2492,11 +2536,16 @@ impl Repository {
|
||||
} else {
|
||||
self.snapshot_revision(after_target, &right)?
|
||||
};
|
||||
let blame = [
|
||||
self.blame_baseline(before_target)?,
|
||||
self.blame_baseline(after_target)?,
|
||||
];
|
||||
let mut prepared = PreparedComparison {
|
||||
id: comparison_id,
|
||||
selected: [before.clone(), after.clone()],
|
||||
selected_ids,
|
||||
revisions: [before_revision, after_revision],
|
||||
blame,
|
||||
mode,
|
||||
left,
|
||||
right,
|
||||
@@ -2614,10 +2663,12 @@ impl Repository {
|
||||
}
|
||||
let right = self.working_snapshot(buffers)?;
|
||||
let revision = self.snapshot_revision(&RevisionTarget::Working, &right)?;
|
||||
let blame = self.blame_baseline(&RevisionTarget::Working)?;
|
||||
let mut prepared = self.comparison.take().unwrap();
|
||||
let previous_right = std::mem::replace(&mut prepared.right, right);
|
||||
let previous_files = std::mem::take(&mut prepared.files);
|
||||
let previous_revision = std::mem::replace(&mut prepared.revisions[1], revision);
|
||||
let previous_blame = std::mem::replace(&mut prepared.blame[1], blame);
|
||||
let result = self
|
||||
.build_comparison(&mut prepared, false)
|
||||
.and_then(|comparison| {
|
||||
@@ -2640,6 +2691,7 @@ impl Repository {
|
||||
prepared.right = previous_right;
|
||||
prepared.files = previous_files;
|
||||
prepared.revisions[1] = previous_revision;
|
||||
prepared.blame[1] = previous_blame;
|
||||
}
|
||||
self.comparison = Some(prepared);
|
||||
result.map(|comparison| Some(CheckoutRefresh::Checkout(comparison)))
|
||||
@@ -2671,13 +2723,16 @@ impl Repository {
|
||||
&& follow_working
|
||||
&& self.working_advanced(prepared.working_anchor.as_deref(), anchor.as_deref())?;
|
||||
let mut revisions = prepared.revisions.clone();
|
||||
let mut blame = prepared.blame.clone();
|
||||
let (left, before_label) = if branch {
|
||||
let base = self.merge_base(ids[0].as_deref().unwrap(), ids[1].as_deref().unwrap())?;
|
||||
let label = format!("merge base of {label} and {after_label} · {base}");
|
||||
revisions[0] = base.clone();
|
||||
blame[0] = self.blame_baseline(&RevisionTarget::Commit(base.clone()))?;
|
||||
(Some(self.snapshot(&RevisionTarget::Commit(base))?), label)
|
||||
} else if changed_before {
|
||||
revisions[0] = ids[0].clone().unwrap();
|
||||
blame[0] = self.blame_baseline(&RevisionTarget::Commit(revisions[0].clone()))?;
|
||||
(
|
||||
Some(self.snapshot(&RevisionTarget::Commit(ids[0].clone().unwrap()))?),
|
||||
label,
|
||||
@@ -2699,6 +2754,13 @@ impl Repository {
|
||||
ids[1].clone().unwrap()
|
||||
};
|
||||
}
|
||||
if right.is_some() {
|
||||
blame[1] = self.blame_baseline(&if advance_working {
|
||||
RevisionTarget::Working
|
||||
} else {
|
||||
RevisionTarget::Commit(revisions[1].clone())
|
||||
})?;
|
||||
}
|
||||
let pinned_checkout = ids[1]
|
||||
.as_deref()
|
||||
.map(|id| self.pinned_checkout(id))
|
||||
@@ -2711,6 +2773,7 @@ impl Repository {
|
||||
let previous_before = std::mem::replace(&mut prepared.before, before_label);
|
||||
let previous_editable = prepared.editable_after;
|
||||
let previous_revisions = std::mem::replace(&mut prepared.revisions, revisions);
|
||||
let previous_blame = std::mem::replace(&mut prepared.blame, blame);
|
||||
prepared.editable_after =
|
||||
working_after || (pinned_checkout.is_some() && pinned_checkout == anchor);
|
||||
let result = self
|
||||
@@ -2747,6 +2810,7 @@ impl Repository {
|
||||
prepared.before = previous_before;
|
||||
prepared.editable_after = previous_editable;
|
||||
prepared.revisions = previous_revisions;
|
||||
prepared.blame = previous_blame;
|
||||
}
|
||||
self.comparison = Some(prepared);
|
||||
result
|
||||
@@ -2861,6 +2925,7 @@ impl Repository {
|
||||
};
|
||||
Comparison {
|
||||
revisions: prepared.revisions.clone(),
|
||||
blame: prepared.blame.clone(),
|
||||
before: prepared.before.clone(),
|
||||
after: prepared.after.clone(),
|
||||
files,
|
||||
|
||||
@@ -1690,6 +1690,7 @@ impl EditorSession {
|
||||
self.blame_request_id,
|
||||
source.path.to_path_buf(),
|
||||
self.editor.lines().join("\n"),
|
||||
source.snapshot.as_deref().cloned(),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
|
||||
+34
-16
@@ -2665,22 +2665,13 @@ fn draw_document(
|
||||
.find(|(_, (row, _, _))| *row == cursor.row)
|
||||
{
|
||||
let line = &editor.lines()[cursor.row];
|
||||
let end = display_column(line, usize::MAX);
|
||||
let start = end.saturating_sub(segment.start).saturating_add(2);
|
||||
let label_width = UnicodeWidthStr::width(label);
|
||||
if end >= segment.start
|
||||
&& end <= segment.end
|
||||
&& (!wrapped || continuation + 1 == document.wrap.segments(cursor.row, line).len())
|
||||
&& start.saturating_add(label_width) <= width
|
||||
{
|
||||
frame.render_widget(
|
||||
Paragraph::new(label).style(Style::default().fg(Color::DarkGray)),
|
||||
Rect::new(
|
||||
viewport.x + start as u16,
|
||||
viewport.y + screen_row as u16,
|
||||
label_width as u16,
|
||||
1,
|
||||
),
|
||||
if !wrapped || continuation + 1 == document.wrap.segments(cursor.row, line).len() {
|
||||
paint_inline_blame(
|
||||
frame,
|
||||
label,
|
||||
line,
|
||||
Rect::new(viewport.x, viewport.y + screen_row as u16, width as u16, 1),
|
||||
segment.start,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3236,6 +3227,18 @@ fn paint_editor_line(
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_inline_blame(frame: &mut Frame, label: &str, line: &str, area: Rect, left: usize) {
|
||||
let end = display_column(line, usize::MAX);
|
||||
let start = end.saturating_sub(left).saturating_add(2);
|
||||
let label_width = UnicodeWidthStr::width(label);
|
||||
if end >= left && start.saturating_add(label_width) <= usize::from(area.width) {
|
||||
frame.render_widget(
|
||||
Paragraph::new(label).style(Style::default().fg(Color::DarkGray)),
|
||||
Rect::new(area.x + start as u16, area.y, label_width as u16, 1),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_diff_editor(
|
||||
frame: &mut Frame,
|
||||
editor: &Editor,
|
||||
@@ -3250,6 +3253,7 @@ fn paint_diff_editor(
|
||||
let source_cursor = editor.cursor();
|
||||
let cursor_column = display_column(&editor.lines()[source_cursor.row], source_cursor.column);
|
||||
let mut cursor = None;
|
||||
let mut blame_area = None;
|
||||
let focus = view.info.focus;
|
||||
for (screen, display, segments) in
|
||||
diff_screen_rows(view.info.display_len, top, area.height, wrapped)
|
||||
@@ -3283,6 +3287,20 @@ fn paint_diff_editor(
|
||||
cursor = Some(Position::new(area.x + (column - left) as u16, line_area.y));
|
||||
}
|
||||
}
|
||||
if source == source_cursor.row
|
||||
&& (segments.is_none()
|
||||
|| wrapped.is_some_and(|rows| {
|
||||
rows[screen].final_segments[usize::from(focus == Focus::After)]
|
||||
}))
|
||||
{
|
||||
blame_area = Some((Rect::new(area.x, line_area.y, area.width, 1), left));
|
||||
}
|
||||
}
|
||||
if cursor.is_some()
|
||||
&& let Some((area, left)) = blame_area
|
||||
&& let Some(label) = editor.line_blame()
|
||||
{
|
||||
paint_inline_blame(frame, label, &editor.lines()[source_cursor.row], area, left);
|
||||
}
|
||||
cursor
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user