feat: show jj ancestry in the floating diff picker
This commit is contained in:
+248
-3
@@ -96,10 +96,22 @@ pub struct Revision {
|
||||
pub description: String,
|
||||
pub category: Category,
|
||||
}
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct RevisionGraph {
|
||||
pub change_id: String,
|
||||
pub description: String,
|
||||
pub parents: Vec<String>,
|
||||
pub author: String,
|
||||
pub timestamp: String,
|
||||
pub bookmarks: String,
|
||||
pub current: bool,
|
||||
pub immutable: bool,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct Catalog {
|
||||
pub kind: RepoKind,
|
||||
pub entries: Vec<Revision>,
|
||||
pub graph: BTreeMap<String, RevisionGraph>,
|
||||
pub notice: Option<String>,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
@@ -761,6 +773,7 @@ pub struct DiffState {
|
||||
pub word_wrap: bool,
|
||||
pub view_mode: ViewMode,
|
||||
pub comparison_mode: ComparisonMode,
|
||||
pub follow_working_copy: bool,
|
||||
pub split_axis: crate::panes::SplitAxis,
|
||||
pub writable_side: Option<Focus>,
|
||||
pub side_labels: [&'static str; 2],
|
||||
@@ -808,6 +821,8 @@ pub struct DiffState {
|
||||
pending: Option<Operation>,
|
||||
request_kind: Option<RequestKind>,
|
||||
visible: Vec<usize>,
|
||||
picker_graph: BTreeMap<String, [String; 2]>,
|
||||
bookmark_graph: BTreeMap<String, BTreeMap<Option<String>, String>>,
|
||||
search_fields: Vec<[String; 3]>,
|
||||
search_matches: Vec<([usize; 7], usize)>,
|
||||
max_before: usize,
|
||||
@@ -831,6 +846,7 @@ impl DiffState {
|
||||
word_wrap: false,
|
||||
view_mode: ViewMode::Split,
|
||||
comparison_mode: ComparisonMode::default(),
|
||||
follow_working_copy: false,
|
||||
split_axis: crate::panes::SplitAxis::Vertical,
|
||||
writable_side: Some(Focus::After),
|
||||
side_labels: ["Before", "After"],
|
||||
@@ -875,6 +891,8 @@ impl DiffState {
|
||||
pending: Some(Operation::Discover),
|
||||
request_kind: Some(RequestKind::Catalog),
|
||||
visible: Vec::new(),
|
||||
picker_graph: BTreeMap::new(),
|
||||
bookmark_graph: BTreeMap::new(),
|
||||
search_fields: Vec::new(),
|
||||
search_matches: Vec::new(),
|
||||
max_before: 0,
|
||||
@@ -1178,6 +1196,7 @@ impl DiffState {
|
||||
kind: page.kind,
|
||||
entries: Vec::new(),
|
||||
notice: Some(format!("Pull requests from {}", page.remote)),
|
||||
graph: BTreeMap::new(),
|
||||
});
|
||||
for (revision, metadata) in page.entries {
|
||||
let RevisionTarget::PullRequest { number, .. } = &revision.target else {
|
||||
@@ -1436,6 +1455,20 @@ impl DiffState {
|
||||
true
|
||||
}
|
||||
fn prepare_search(&mut self) {
|
||||
self.bookmark_graph.clear();
|
||||
if let Some(catalog) = &self.catalog {
|
||||
for entry in &catalog.entries {
|
||||
if let RevisionTarget::Bookmark { name, remote } = &entry.target
|
||||
&& let Some(id) = entry.description.split_whitespace().nth(1)
|
||||
&& catalog.graph.contains_key(id)
|
||||
{
|
||||
self.bookmark_graph
|
||||
.entry(name.clone())
|
||||
.or_default()
|
||||
.insert(remote.clone(), id.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
self.search_fields = self
|
||||
.catalog
|
||||
.as_ref()
|
||||
@@ -1552,6 +1585,186 @@ impl DiffState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn graph_revision(&self, revision: &Revision) -> Option<(&str, &RevisionGraph)> {
|
||||
let catalog = self.catalog.as_ref()?;
|
||||
let id = match &revision.target {
|
||||
RevisionTarget::Commit(id) => id.as_str(),
|
||||
RevisionTarget::Bookmark { name, remote } => {
|
||||
self.bookmark_graph.get(name)?.get(remote)?
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
catalog
|
||||
.graph
|
||||
.get_key_value(id)
|
||||
.map(|(id, graph)| (id.as_str(), graph))
|
||||
}
|
||||
|
||||
pub fn picker_graph(&self) -> &BTreeMap<String, [String; 2]> {
|
||||
&self.picker_graph
|
||||
}
|
||||
|
||||
fn rebuild_picker_graph(&mut self) {
|
||||
let mut result = BTreeMap::new();
|
||||
let Some(catalog) = &self.catalog else {
|
||||
self.picker_graph = result;
|
||||
return;
|
||||
};
|
||||
let visible: Vec<_> = self
|
||||
.entries()
|
||||
.filter_map(|entry| self.graph_revision(entry))
|
||||
.map(|(id, _)| id)
|
||||
.collect();
|
||||
let selected: BTreeSet<_> = visible.iter().copied().collect();
|
||||
if selected.is_empty() {
|
||||
self.picker_graph = result;
|
||||
return;
|
||||
}
|
||||
let mut nearest: BTreeMap<&str, BTreeMap<&str, bool>> = BTreeMap::new();
|
||||
for entry in catalog
|
||||
.entries
|
||||
.iter()
|
||||
.rev()
|
||||
.filter(|entry| entry.category == Category::Revisions)
|
||||
{
|
||||
let Some((id, graph)) = self.graph_revision(entry) else {
|
||||
continue;
|
||||
};
|
||||
if nearest.contains_key(id) {
|
||||
continue;
|
||||
}
|
||||
let mut ancestors = BTreeMap::new();
|
||||
if selected.contains(id) {
|
||||
ancestors.insert(id, false);
|
||||
} else {
|
||||
for parent in &graph.parents {
|
||||
if let Some(parents) = nearest.get(parent.as_str()) {
|
||||
ancestors.extend(parents.keys().map(|&parent| (parent, true)));
|
||||
}
|
||||
}
|
||||
}
|
||||
nearest.insert(id, ancestors);
|
||||
}
|
||||
let mut lanes: Vec<(&str, bool)> = Vec::new();
|
||||
for id in visible {
|
||||
if result.contains_key(id) {
|
||||
continue;
|
||||
}
|
||||
let graph = &catalog.graph[id];
|
||||
let mut parents = BTreeMap::new();
|
||||
for parent in &graph.parents {
|
||||
if let Some(ancestors) = nearest.get(parent.as_str()) {
|
||||
for (&ancestor, &skipped) in ancestors {
|
||||
parents
|
||||
.entry(ancestor)
|
||||
.and_modify(|old| *old |= skipped)
|
||||
.or_insert(skipped);
|
||||
}
|
||||
}
|
||||
}
|
||||
let column = lanes
|
||||
.iter()
|
||||
.position(|&(lane, _)| lane == id)
|
||||
.unwrap_or_else(|| {
|
||||
lanes.push((id, false));
|
||||
lanes.len() - 1
|
||||
});
|
||||
let mut next = lanes.clone();
|
||||
next.remove(column);
|
||||
for (offset, (&parent, &skipped)) in parents.iter().enumerate() {
|
||||
if let Some(lane) = next.iter_mut().find(|(lane, _)| *lane == parent) {
|
||||
lane.1 |= skipped;
|
||||
} else {
|
||||
next.insert((column + offset).min(next.len()), (parent, skipped));
|
||||
}
|
||||
}
|
||||
let width = lanes.len().max(next.len()) * 2;
|
||||
let mut first = vec![' '; width];
|
||||
for (column, &(_, skipped)) in lanes.iter().enumerate() {
|
||||
first[column * 2] = if skipped { '~' } else { '│' };
|
||||
}
|
||||
first[column * 2] = if graph.current {
|
||||
'@'
|
||||
} else if graph.immutable {
|
||||
'◆'
|
||||
} else {
|
||||
'○'
|
||||
};
|
||||
// Low bits connect up/down/left/right; high bits distinguish joins from crossings.
|
||||
let mut links = vec![0u8; width];
|
||||
for (from, &(lane, _)) in lanes.iter().enumerate() {
|
||||
for (to, &(target, _)) in next.iter().enumerate() {
|
||||
if (lane == id && !parents.contains_key(target))
|
||||
|| (lane != id && lane != target)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let (from, to) = (from * 2, to * 2);
|
||||
if links[from] & 1 != 0 {
|
||||
links[from] |= 16;
|
||||
}
|
||||
if links[to] & 2 != 0 {
|
||||
links[to] |= 32;
|
||||
}
|
||||
if from == to {
|
||||
links[from] |= 3;
|
||||
} else {
|
||||
links[from] |= 1 | if from < to { 8 } else { 4 };
|
||||
links[to] |= 2 | if from < to { 4 } else { 8 };
|
||||
for cell in &mut links[from.min(to) + 1..from.max(to)] {
|
||||
*cell |= 12;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut second: Vec<_> = links
|
||||
.into_iter()
|
||||
.map(|link| match link & 15 {
|
||||
3 => '│',
|
||||
5 => '┘',
|
||||
6 => '┐',
|
||||
7 => '┤',
|
||||
9 => '└',
|
||||
10 => '┌',
|
||||
11 => '├',
|
||||
12 => '─',
|
||||
13 => '┴',
|
||||
14 => '┬',
|
||||
15 => {
|
||||
if link & 48 == 0 {
|
||||
'╪'
|
||||
} else {
|
||||
'┼'
|
||||
}
|
||||
}
|
||||
_ => ' ',
|
||||
})
|
||||
.collect();
|
||||
for (column, &(_, skipped)) in next.iter().enumerate() {
|
||||
if skipped {
|
||||
second[column * 2] = '~';
|
||||
}
|
||||
}
|
||||
if !graph.parents.is_empty() && parents.is_empty() {
|
||||
second[column * 2] = '~';
|
||||
}
|
||||
if !parents.is_empty()
|
||||
&& graph
|
||||
.parents
|
||||
.iter()
|
||||
.any(|parent| nearest.get(parent.as_str()).is_none_or(BTreeMap::is_empty))
|
||||
{
|
||||
second.extend(['~', ' ']);
|
||||
}
|
||||
result.insert(
|
||||
id.to_owned(),
|
||||
[first.into_iter().collect(), second.into_iter().collect()],
|
||||
);
|
||||
lanes = next;
|
||||
}
|
||||
self.picker_graph = result;
|
||||
}
|
||||
|
||||
fn refilter(&mut self) {
|
||||
self.visible.clear();
|
||||
self.search_matches.clear();
|
||||
@@ -1619,7 +1832,7 @@ impl DiffState {
|
||||
self.search_matches.push((score, index));
|
||||
}
|
||||
}
|
||||
if self.pull_request_remote.is_none() {
|
||||
if self.pull_request_remote.is_none() && catalog.kind == RepoKind::Git {
|
||||
self.search_matches
|
||||
.sort_unstable_by_key(|&(score, index)| (score, index));
|
||||
}
|
||||
@@ -1663,8 +1876,34 @@ impl DiffState {
|
||||
b_number.cmp(&a_number)
|
||||
});
|
||||
}
|
||||
if self.pull_request_remote.is_none() {
|
||||
if let Some(before) = &self.before {
|
||||
let same = |entry: &Revision| {
|
||||
entry.target == before.target
|
||||
|| self
|
||||
.graph_revision(entry)
|
||||
.zip(self.graph_revision(before))
|
||||
.is_some_and(|(a, b)| a.0 == b.0)
|
||||
};
|
||||
if !self
|
||||
.visible
|
||||
.iter()
|
||||
.any(|&index| same(&catalog.entries[index]))
|
||||
&& let Some(index) = catalog
|
||||
.entries
|
||||
.iter()
|
||||
.position(|entry| entry.target == before.target)
|
||||
{
|
||||
self.visible.push(index);
|
||||
}
|
||||
}
|
||||
if catalog.kind == RepoKind::Jujutsu {
|
||||
self.visible.sort_unstable();
|
||||
}
|
||||
}
|
||||
}
|
||||
self.selected = 0;
|
||||
self.rebuild_picker_graph();
|
||||
}
|
||||
|
||||
fn select_target(&mut self, target: &RevisionTarget) {
|
||||
@@ -1838,7 +2077,7 @@ impl DiffState {
|
||||
|
||||
fn choose(&mut self) {
|
||||
let revision = self.entries().nth(self.selected).cloned();
|
||||
let Some(revision) = revision else {
|
||||
let Some(mut revision) = revision else {
|
||||
self.retry_pull_requests();
|
||||
// Listing can be retried without closing the modal.
|
||||
if self.catalog.is_none()
|
||||
@@ -1861,6 +2100,12 @@ impl DiffState {
|
||||
}
|
||||
if revision.target == RevisionTarget::Working {
|
||||
self.comparison_mode = ComparisonMode::Snapshots;
|
||||
revision.label = if self.phase == Phase::After && self.follow_working_copy {
|
||||
"Follow checkout"
|
||||
} else {
|
||||
"Checkout snapshot"
|
||||
}
|
||||
.into();
|
||||
}
|
||||
if let RevisionTarget::FetchRemote(remote) = &revision.target {
|
||||
self.queue(Operation::FetchRemote(remote.clone()));
|
||||
@@ -1891,7 +2136,7 @@ impl DiffState {
|
||||
return Err("Select a pull request before changing comparison mode");
|
||||
}
|
||||
if self.working_selected() {
|
||||
return Err("Working checkout requires Snapshots");
|
||||
return Err("Checkout selections require Snapshots");
|
||||
}
|
||||
self.comparison_mode = match self.comparison_mode {
|
||||
ComparisonMode::BranchChanges => ComparisonMode::Snapshots,
|
||||
|
||||
@@ -2282,6 +2282,7 @@ pub struct Editor {
|
||||
diff_return_buffer: Option<usize>,
|
||||
diff_context_options: (usize, usize),
|
||||
diff_display_options: (ViewMode, SplitAxis),
|
||||
pub(crate) diff_follow_working_copy: bool,
|
||||
pub(crate) pull_request_review_requested_symbol: String,
|
||||
pub(crate) pull_request_approved_symbol: String,
|
||||
pub(crate) pull_request_preferences: crate::pull_requests::Preferences,
|
||||
@@ -2429,6 +2430,7 @@ impl Editor {
|
||||
diff_return_buffer: None,
|
||||
diff_context_options: (10, 15),
|
||||
diff_display_options: (ViewMode::Split, SplitAxis::Vertical),
|
||||
diff_follow_working_copy: false,
|
||||
pull_request_review_requested_symbol: "🙏".into(),
|
||||
pull_request_approved_symbol: "👍".into(),
|
||||
pull_request_preferences: Default::default(),
|
||||
@@ -4764,6 +4766,7 @@ impl Editor {
|
||||
diff.word_wrap = self.active.view.word_wrap;
|
||||
diff.view_mode = self.diff_display_options.0;
|
||||
diff.split_axis = self.diff_display_options.1;
|
||||
diff.follow_working_copy = self.diff_follow_working_copy;
|
||||
diff.set_context_options(self.diff_context_options.0, self.diff_context_options.1);
|
||||
self.diff = Some(diff);
|
||||
}
|
||||
|
||||
+2
-2
@@ -85,7 +85,7 @@ Restart: :restart closes clean buffers; unsaved buffers remain open and block re
|
||||
Tabs: :tab.new, :tab.previous, :tab.next, :tab.close, :tab.force_close
|
||||
Tabs retain pane layouts and independent views; close protects exclusively displayed unsaved edits
|
||||
Markdown links: :follow_link (primary caret; local files only)
|
||||
Diff: :diff (Normal); choose Before then After; Working checkout After edits live unsaved buffers
|
||||
Diff: :diff (Normal); floating jj log graph; choose Before then After; selected Before is marked inline
|
||||
Before and historical/remote After stay read-only; Tab/Shift+Tab categories/Normal focus
|
||||
All text panes use native motions, selections and copy; read-only panes reject edits/saves
|
||||
Switching Before/After preserves the aligned cursor row and display column
|
||||
@@ -93,7 +93,7 @@ Diff: :diff (Normal); choose Before then After; Working checkout After edits liv
|
||||
Normal [/] explicitly expands context; Insert brackets remain literal text
|
||||
Normal Space v cycles split/unified; Space s flips left/right vs above/below split
|
||||
Defaults: config.diff_view_mode = \"split\"; config.diff_split_orientation = \"vertical\"
|
||||
Working After follows descendant checkouts; config.diff_follow_working_copy = false disables it
|
||||
Follow checkout After edits live buffers and follows descendants; config.diff_follow_working_copy = false disables following
|
||||
Esc closes Before/After selectors; :diff.close exits an opened comparison explicitly
|
||||
Remote refs/history load into temporary comparison repos without changing active refs/checkout
|
||||
Changed files: normal picker/explorer bindings, Ctrl+w close; A/M/D/R status and rename paths
|
||||
|
||||
+108
-12
@@ -1,7 +1,8 @@
|
||||
//! Read-only active-repository discovery and isolated revision comparison.
|
||||
use crate::diff::{
|
||||
BlameRevision, Catalog, Category, ChangedFile, Comparison, ComparisonMode, FileStatus,
|
||||
PullRequestMetadata, PullRequestPage, RepoKind, Revision, RevisionTarget, line_hunks,
|
||||
PullRequestMetadata, PullRequestPage, RepoKind, Revision, RevisionGraph, RevisionTarget,
|
||||
line_hunks,
|
||||
};
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
@@ -1180,10 +1181,25 @@ impl Repository {
|
||||
root: &Path,
|
||||
store: &Path,
|
||||
entries: &mut Vec<Revision>,
|
||||
graph: &mut BTreeMap<String, RevisionGraph>,
|
||||
notices: &mut Vec<String>,
|
||||
) -> Result<(), String> {
|
||||
if self.kind == RepoKind::Jujutsu {
|
||||
let template = r#""[" ++ json(commit_id) ++ "," ++ json(change_id) ++ "," ++ json(description) ++ "," ++ json(local_bookmarks.map(|b| b.name())) ++ "," ++ json(remote_bookmarks.map(|b| b.name())) ++ "," ++ json(remote_bookmarks.map(|b| b.remote())) ++ "]\n""#;
|
||||
#[derive(serde::Deserialize)]
|
||||
struct JjRevision(
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
Vec<String>,
|
||||
Vec<String>,
|
||||
Vec<String>,
|
||||
Vec<String>,
|
||||
String,
|
||||
String,
|
||||
bool,
|
||||
bool,
|
||||
);
|
||||
let template = r#""[" ++ json(commit_id) ++ "," ++ json(change_id) ++ "," ++ json(description) ++ "," ++ json(local_bookmarks.map(|b| b.name())) ++ "," ++ json(remote_bookmarks.map(|b| b.name())) ++ "," ++ json(remote_bookmarks.map(|b| b.remote())) ++ "," ++ json(parents.map(|p| p.commit_id())) ++ "," ++ json(author.name()) ++ "," ++ json(author.timestamp().format("%Y-%m-%d %H:%M")) ++ "," ++ json(current_working_copy) ++ "," ++ json(immutable) ++ "]\n""#;
|
||||
let revset = if root == self.root {
|
||||
"all()"
|
||||
} else {
|
||||
@@ -1192,15 +1208,40 @@ impl Repository {
|
||||
let output =
|
||||
text(self.jj(root, &["log", "--no-graph", "-r", revset, "-T", template])?)?;
|
||||
for line in output.lines() {
|
||||
let (id, change, description, bookmarks, remote_names, remotes): (
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
Vec<String>,
|
||||
Vec<String>,
|
||||
Vec<String>,
|
||||
let JjRevision(
|
||||
id,
|
||||
change,
|
||||
description,
|
||||
bookmarks,
|
||||
remote_names,
|
||||
remotes,
|
||||
parents,
|
||||
author,
|
||||
timestamp,
|
||||
current,
|
||||
immutable,
|
||||
) = serde_json::from_str(line)
|
||||
.map_err(|e| format!("Cannot decode Jujutsu revision: {e}"))?;
|
||||
graph.entry(id.clone()).or_insert_with(|| RevisionGraph {
|
||||
change_id: change.clone(),
|
||||
description: description.trim_end_matches('\n').into(),
|
||||
parents,
|
||||
author,
|
||||
timestamp,
|
||||
bookmarks: bookmarks
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(
|
||||
remote_names
|
||||
.iter()
|
||||
.zip(&remotes)
|
||||
.map(|(name, remote)| format!("{name}@{remote}")),
|
||||
)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" "),
|
||||
current: current && root == self.root,
|
||||
immutable,
|
||||
});
|
||||
let target = RevisionTarget::Commit(id.clone());
|
||||
entries.push(Revision {
|
||||
target: target.clone(),
|
||||
@@ -1370,15 +1411,69 @@ impl Repository {
|
||||
};
|
||||
let mut entries = vec![Revision {
|
||||
target: RevisionTarget::Working,
|
||||
label: "Working checkout".into(),
|
||||
label: "Checkout snapshot".into(),
|
||||
description:
|
||||
"On-disk tracked and eligible untracked files; unsaved buffers are excluded".into(),
|
||||
category,
|
||||
}];
|
||||
let mut notices = vec![];
|
||||
self.revisions(&self.root, &self.store, &mut entries, &mut notices)?;
|
||||
let mut graph = BTreeMap::new();
|
||||
self.revisions(
|
||||
&self.root,
|
||||
&self.store,
|
||||
&mut entries,
|
||||
&mut graph,
|
||||
&mut notices,
|
||||
)?;
|
||||
if let Some((root, store)) = &self.private {
|
||||
self.revisions(root, store, &mut entries, &mut notices)?;
|
||||
self.revisions(root, store, &mut entries, &mut graph, &mut notices)?;
|
||||
}
|
||||
if self.kind == RepoKind::Jujutsu && self.private.is_some() {
|
||||
let order: BTreeMap<_, _> = entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, entry)| match &entry.target {
|
||||
RevisionTarget::Commit(id) => Some((id.as_str(), index)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let mut children: BTreeMap<_, usize> =
|
||||
graph.keys().map(|id| (id.as_str(), 0)).collect();
|
||||
for node in graph.values() {
|
||||
for parent in &node.parents {
|
||||
if let Some(count) = children.get_mut(parent.as_str()) {
|
||||
*count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut ready: BTreeSet<_> = children
|
||||
.iter()
|
||||
.filter(|(_, count)| **count == 0)
|
||||
.map(|(&id, _)| (order[id], id))
|
||||
.collect();
|
||||
let mut ranks = BTreeMap::new();
|
||||
while let Some((_, id)) = ready.pop_first() {
|
||||
ranks.insert(id, ranks.len() + 1);
|
||||
for parent in &graph[id].parents {
|
||||
if let Some(count) = children.get_mut(parent.as_str()) {
|
||||
*count -= 1;
|
||||
if *count == 0 {
|
||||
ready.insert((order[parent.as_str()], parent.as_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
entries.sort_by_key(|entry| match &entry.target {
|
||||
RevisionTarget::Working => 0,
|
||||
RevisionTarget::Commit(id) => ranks.get(id.as_str()).copied().unwrap_or(usize::MAX),
|
||||
RevisionTarget::Bookmark { .. } => entry
|
||||
.description
|
||||
.split_whitespace()
|
||||
.nth(1)
|
||||
.and_then(|id| ranks.get(id).copied())
|
||||
.unwrap_or(usize::MAX),
|
||||
_ => usize::MAX,
|
||||
});
|
||||
}
|
||||
for remote in &self.remotes {
|
||||
for &view in self.kind.categories() {
|
||||
@@ -1439,6 +1534,7 @@ impl Repository {
|
||||
Ok(Catalog {
|
||||
kind: self.kind,
|
||||
entries,
|
||||
graph,
|
||||
notice: if notices.is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
||||
@@ -580,6 +580,7 @@ impl EditorSession {
|
||||
editor.set_tab_spaces(config.tab_spaces, config.language_tab_spaces);
|
||||
editor.set_diff_context_options(config.diff_context_lines, config.diff_expand_lines);
|
||||
editor.set_diff_display_options(config.diff_view_mode, config.diff_split_orientation);
|
||||
editor.diff_follow_working_copy = config.diff_follow_working_copy;
|
||||
editor.pull_request_review_requested_symbol = config.pull_request_review_requested_symbol;
|
||||
editor.pull_request_approved_symbol = config.pull_request_approved_symbol;
|
||||
editor.resolve_history_layout = config.resolve_history_layout;
|
||||
|
||||
+95
-12
@@ -3588,6 +3588,8 @@ fn draw_diff(
|
||||
}
|
||||
} else if pull_requests {
|
||||
" Pull requests ".into()
|
||||
} else if panel.width < 50 {
|
||||
format!(" Choose {stage} ")
|
||||
} else {
|
||||
format!(" Diff · Choose {stage} · {} ", diff.comparison_mode.label())
|
||||
};
|
||||
@@ -3700,10 +3702,6 @@ fn draw_diff(
|
||||
Constraint::Length(u16::from(content.height >= 7)),
|
||||
])
|
||||
.areas(content);
|
||||
let before = diff
|
||||
.before
|
||||
.as_ref()
|
||||
.map_or("not selected", |revision| revision.label.as_str());
|
||||
let stage_text = if pull_requests {
|
||||
let filters = &diff.pull_request_preferences.filters;
|
||||
let review = if filters.len() == 3 {
|
||||
@@ -3735,11 +3733,7 @@ fn draw_diff(
|
||||
},
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{} · Ctrl+b toggle · Before: {}",
|
||||
diff.comparison_mode.label(),
|
||||
display_text(before)
|
||||
)
|
||||
format!("{} · Ctrl+b toggle", diff.comparison_mode.label())
|
||||
};
|
||||
frame.render_widget(Paragraph::new(stage_text), stage_area);
|
||||
if pull_requests || diff.catalog.is_some() {
|
||||
@@ -3803,11 +3797,34 @@ fn draw_diff(
|
||||
);
|
||||
}
|
||||
let mut entries = diff.entries();
|
||||
let before_matches = |revision: &crate::diff::Revision| {
|
||||
diff.before.as_ref().is_some_and(|before| {
|
||||
match (diff.graph_revision(before), diff.graph_revision(revision)) {
|
||||
(Some((before, _)), Some((candidate, _))) => before == candidate,
|
||||
_ => before.target == revision.target,
|
||||
}
|
||||
})
|
||||
};
|
||||
if !pull_requests && let Some(working) = entries.next() {
|
||||
let follow = diff.phase == Phase::After && diff.follow_working_copy;
|
||||
let label = format!(
|
||||
"{} {} · pinned",
|
||||
"{} {}{} · pinned · {}",
|
||||
if diff.selected == 0 { ">" } else { " " },
|
||||
display_text(&working.label)
|
||||
if follow {
|
||||
"Follow checkout"
|
||||
} else {
|
||||
"Checkout snapshot"
|
||||
},
|
||||
if before_matches(working) {
|
||||
" [Before]"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if follow {
|
||||
"Live files; follows descendant checkouts"
|
||||
} else {
|
||||
"Live files at this checkout"
|
||||
},
|
||||
);
|
||||
frame.render_widget(
|
||||
Paragraph::new(visible_line(&label, 0, usize::from(pinned_area.width))).style(
|
||||
@@ -3863,6 +3880,7 @@ fn draw_diff(
|
||||
0
|
||||
};
|
||||
let decoration_width = symbol_width + usize::from(symbol_width > 0);
|
||||
let graph = diff.picker_graph();
|
||||
for (index, revision) in entries
|
||||
.enumerate()
|
||||
.skip(first)
|
||||
@@ -3876,6 +3894,71 @@ fn draw_diff(
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
let before_badge = if before_matches(revision) {
|
||||
" [Before]"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
if let Some((id, metadata)) = diff.graph_revision(revision)
|
||||
&& let Some(prefix) = graph.get(id)
|
||||
{
|
||||
let width = usize::from(results.width);
|
||||
let change = metadata.change_id.as_str();
|
||||
let change = change.get(..12).unwrap_or(change);
|
||||
let bookmark = if revision.category == crate::diff::Category::Bookmarks {
|
||||
format!("[{}] ", display_text(&revision.label))
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let label = format!(
|
||||
"{} {}{bookmark}{}{} {} {} {} {}",
|
||||
if index == diff.selected { ">" } else { " " },
|
||||
prefix[0],
|
||||
display_text(change),
|
||||
before_badge,
|
||||
display_text(&metadata.author),
|
||||
display_text(&metadata.timestamp),
|
||||
if metadata.bookmarks.is_empty() || !bookmark.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("[{}]", display_text(&metadata.bookmarks))
|
||||
},
|
||||
&id[..id.len().min(12)],
|
||||
);
|
||||
if row_height == 2 {
|
||||
lines.push(Line::styled(visible_line(&label, 0, width), style));
|
||||
lines.push(Line::styled(
|
||||
visible_line(
|
||||
&format!(" {}{}", prefix[1], display_text(&metadata.description)),
|
||||
0,
|
||||
width,
|
||||
),
|
||||
style.fg(Color::DarkGray),
|
||||
));
|
||||
} else {
|
||||
lines.push(Line::styled(
|
||||
visible_line(
|
||||
&format!(
|
||||
"{} {}{}{}{bookmark}{} · {}",
|
||||
if index == diff.selected { ">" } else { " " },
|
||||
prefix[0],
|
||||
if prefix[1].contains('~') { "~ " } else { "" },
|
||||
if before_badge.is_empty() {
|
||||
""
|
||||
} else {
|
||||
"[Before] "
|
||||
},
|
||||
display_text(change),
|
||||
display_text(&metadata.description),
|
||||
),
|
||||
0,
|
||||
width,
|
||||
),
|
||||
style,
|
||||
));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let symbol = if pull_requests {
|
||||
match diff.pull_request_review_state(revision) {
|
||||
crate::diff::PullRequestReviewState::Requested => requested_symbol,
|
||||
@@ -3886,7 +3969,7 @@ fn draw_diff(
|
||||
""
|
||||
};
|
||||
let label = format!(
|
||||
"{} {symbol}{:padding$}{}",
|
||||
"{} {symbol}{:padding$}{}{before_badge}",
|
||||
if index == diff.selected { ">" } else { " " },
|
||||
"",
|
||||
display_text(&revision.label),
|
||||
|
||||
Reference in New Issue
Block a user