Read each worktree's symbolic HEAD or private HEAD pin so remembered branch labels stay at the branch's actual tip while detached checkout commits receive their own marker. Attached and fallback labels retain the history view's current- and linked-worktree forms. Remove tree pin creation and removal along with ordinary pin anchors. The tree and its ASCII diagnostics now reserve the pin marker for detached worktrees, making detached checkout state visible without exposing internal pin references.
Unified · Split
| @@ -136,11 +136,12 @@ without trading responsiveness for metadata that is not visible. · 135 unmodified lines | |||
| 136 | visible. If it has visible descendants, its unselected non-whitespace content | 136 | visible. If it has visible descendants, its unselected non-whitespace content |
| 137 | is underlined and `@` is bold; a selected row keeps only bold `@`. | 137 | is underlined and `@` is bold; a selected row keeps only bold `@`. |
| 138 | - Local branches checked out in other worktrees are displayed as `short-name@` | 138 | - Local branches checked out in other worktrees are displayed as `short-name@` |
| 139 | in light blue instead of their plain branch decoration. Other detached | 139 | in light blue instead of their plain branch decoration. The current worktree's |
| 140 | worktrees use their checkout directory basename. The current worktree's | 140 | symbolic branch is displayed as `@short-name` in the local-reference color. |
| 141 | symbolic branch is displayed as `@short-name` in the local-reference color; | 141 | A detached worktree with a symbolic HEAD pin labels the remembered branch at |
| 142 | if its HEAD is detached, a branch that happens to point at the same commit | 142 | that branch's actual tip, even when the checkout is elsewhere; otherwise its |
| 143 | remains an ordinary branch label. Identical labels are deduplicated. | 143 | checkout directory basename labels the detached commit. Identical labels are |
| 144 | deduplicated. | ||
| 144 | - When reference labels are hidden, worktree labels are visible only on the | 145 | - When reference labels are hidden, worktree labels are visible only on the |
| 145 | selected row. Stale, malformed, unborn, and otherwise unreadable worktree | 146 | selected row. Stale, malformed, unborn, and otherwise unreadable worktree |
| 146 | entries are skipped without failing history loading. | 147 | entries are skipped without failing history loading. |
| @@ -228,19 +229,20 @@ without trading responsiveness for metadata that is not visible. · 81 unmodified lines | |||
| 228 | map uniquely through a named remote's fetch refspecs; it deletes every resolved | 229 | map uniquely through a named remote's fetch refspecs; it deletes every resolved |
| 229 | remote reference, grouped into one Git push per remote. Pushes continue after | 230 | remote reference, grouped into one Git push per remote. Pushes continue after |
| 230 | individual failures and run with the terminal suspended for output and authentication. | 231 | individual failures and run with the terminal suspended for output and authentication. |
| 231 | - Ordinary pins use the history view's single blue `📌`, without exposing their | 232 | - Worktree branch labels keep the history view's `@branch`, `branch@`, and |
| 232 | internal ref names. `p` removes all ordinary pins at a pinned node or creates one | 233 | `★branch` forms at the branch's actual tip. Every detached worktree checkout is |
| 233 | at a visible reference/raw tip. A sole visible reference produces a symbolic pin; | 234 | additionally shown with one `📌`; ordinary tix pins neither anchor nor decorate |
| 234 | ambiguous or unnamed targets produce a direct pin. Hidden tags do not participate. | 235 | the tree. A detached worktree without a usable remembered branch falls back to |
| 235 | - After deleting selected branches or pins, refresh keeps the commit when it remains | 236 | its checkout directory basename. |
| 236 | a tree node, otherwise selects the next surviving node row or the nearest previous | 237 | - After deleting selected local or remote references, refresh keeps the commit |
| 237 | row when nothing below survives. | 238 | when it remains a tree node, otherwise selects the next surviving node row or |
| 239 | the nearest previous row when nothing below survives. | ||
| 238 | 240 | ||
| 239 | ### Tree diagnostics | 241 | ### Tree diagnostics |
| 240 | 242 | ||
| 241 | - `tix tree` prints the entire reference projection to standard output without | 243 | - `tix tree` prints the entire reference projection to standard output without |
| 242 | terminal colors, selection state, counts, or viewport clipping. Pins render as | 244 | terminal colors, selection state, counts, or viewport clipping. Detached |
| 243 | `[pin]` in ASCII output and `📌` with `--unicode`. It traverses | 245 | worktrees render as `[pin]` in ASCII output and `📌` with `--unicode`. It traverses |
| 244 | all normal references by default; positional revisions scope traversal. | 246 | all normal references by default; positional revisions scope traversal. |
| 245 | - `--no-tags`, `--worktrees`, and repeatable `--hide <revision>` match the | 247 | - `--no-tags`, `--worktrees`, and repeatable `--hide <revision>` match the |
| 246 | corresponding tree inputs. Output uses ASCII lines and `o` nodes by default; | 248 | corresponding tree inputs. Output uses ASCII lines and `o` nodes by default; |
| @@ -653,42 +653,6 @@ pub(crate) fn create_or_reuse_pin( · 652 unmodified lines | |||
| 653 | Ok((history::Pin { name, target, id }, true)) | 653 | Ok((history::Pin { name, target, id }, true)) |
| 654 | } | 654 | } |
| 655 | 655 | ||
| 656 | pub(crate) fn create_tree_pin( | ||
| 657 | repository: &gix::Repository, | ||
| 658 | id: ObjectId, | ||
| 659 | include_tags: bool, | ||
| 660 | ) -> Result<(history::Pin, bool)> { | ||
| 661 | let mut references = Vec::new(); | ||
| 662 | for reference in repository | ||
| 663 | .references() | ||
| 664 | .context("could not open references for tree pinning")? | ||
| 665 | .all() | ||
| 666 | .context("could not iterate references for tree pinning")? | ||
| 667 | { | ||
| 668 | let mut reference = | ||
| 669 | reference.map_err(|err| anyhow::anyhow!("could not read a reference for tree pinning: {err}"))?; | ||
| 670 | let name = reference.name().to_owned(); | ||
| 671 | let kind = history::decoration_kind(name.as_bstr()); | ||
| 672 | if matches!( | ||
| 673 | kind, | ||
| 674 | history::DecorationKind::Pin | history::DecorationKind::Stash | history::DecorationKind::Special | ||
| 675 | ) || !include_tags && matches!(kind, history::DecorationKind::Tag) | ||
| 676 | || name.as_bstr().starts_with(history::REVIEW_STASH_PREFIX) | ||
| 677 | { | ||
| 678 | continue; | ||
| 679 | } | ||
| 680 | let Ok(target) = reference.peel_to_id() else { continue }; | ||
| 681 | if target.detach() == id && repository.find_header(target)?.kind() == gix::object::Kind::Commit { | ||
| 682 | references.push(name); | ||
| 683 | } | ||
| 684 | } | ||
| 685 | let target = match references.as_slice() { | ||
| 686 | [name] => Target::Symbolic(name.clone()), | ||
| 687 | _ => Target::Object(id), | ||
| 688 | }; | ||
| 689 | create_or_reuse_pin(repository, target, id, "tix tree pin") | ||
| 690 | } | ||
| 691 | |||
| 692 | pub(super) fn delete_pin(repository: &gix::Repository, pin: &history::Pin) -> Result<()> { | 656 | pub(super) fn delete_pin(repository: &gix::Repository, pin: &history::Pin) -> Result<()> { |
| 693 | repository | 657 | repository |
| 694 | .edit_references([delete_pin_edit(pin)]) | 658 | .edit_references([delete_pin_edit(pin)]) |
| @@ -1320,44 +1284,6 @@ mod tests { · 625 unmodified lines | |||
| 1320 | Ok(()) | 1284 | Ok(()) |
| 1321 | } | 1285 | } |
| 1322 | 1286 | ||
| 1323 | #[test] | ||
| 1324 | fn tree_pins_follow_one_visible_reference_and_freeze_ambiguous_targets() -> gix_testtools::Result { | ||
| 1325 | let fixture = gix_testtools::scripted_fixture_writable("history.sh")?; | ||
| 1326 | let repository = crate::test_repository::open(fixture.path())?; | ||
| 1327 | let main = repository.rev_parse_single("main")?.detach(); | ||
| 1328 | let (pin, created) = create_tree_pin(&repository, main, true)?; | ||
| 1329 | assert!(created, "the first tree pin is created"); | ||
| 1330 | assert_eq!( | ||
| 1331 | pin.target.try_name().map(gix::refs::FullNameRef::as_bstr), | ||
| 1332 | Some(b"refs/heads/main".as_bstr()), | ||
| 1333 | "one visible reference produces a symbolic pin" | ||
| 1334 | ); | ||
| 1335 | remove_pins(repository.git_dir(), repository.is_bare(), main)?; | ||
| 1336 | |||
| 1337 | repository.reference( | ||
| 1338 | "refs/tags/also-main", | ||
| 1339 | main, | ||
| 1340 | PreviousValue::MustNotExist, | ||
| 1341 | "test ambiguous tree pin", | ||
| 1342 | )?; | ||
| 1343 | let (pin, created) = create_tree_pin(&repository, main, true)?; | ||
| 1344 | assert!(created); | ||
| 1345 | assert_eq!( | ||
| 1346 | pin.target, | ||
| 1347 | Target::Object(main), | ||
| 1348 | "multiple visible refs produce a direct pin" | ||
| 1349 | ); | ||
| 1350 | remove_pins(repository.git_dir(), repository.is_bare(), main)?; | ||
| 1351 | |||
| 1352 | let (pin, created) = create_tree_pin(&repository, main, false)?; | ||
| 1353 | assert!(created); | ||
| 1354 | assert!( | ||
| 1355 | pin.target.try_name().is_some(), | ||
| 1356 | "a hidden tag does not make the branch target ambiguous" | ||
| 1357 | ); | ||
| 1358 | Ok(()) | ||
| 1359 | } | ||
| 1360 | |||
| 1361 | #[test] | 1287 | #[test] |
| 1362 | fn sideways_travel_preserves_an_unreferenced_departure() -> gix_testtools::Result { | 1288 | fn sideways_travel_preserves_an_unreferenced_departure() -> gix_testtools::Result { |
| 1363 | let fixture = gix_testtools::scripted_fixture_writable("history.sh")?; | 1289 | let fixture = gix_testtools::scripted_fixture_writable("history.sh")?; |
| @@ -34,6 +34,7 @@ pub(crate) enum DecorationKind { · 33 unmodified lines | |||
| 34 | Stash, | 34 | Stash, |
| 35 | Review, | 35 | Review, |
| 36 | CurrentWorktreeBranch, | 36 | CurrentWorktreeBranch, |
| 37 | CurrentWorktreeDetached, | ||
| 37 | WorktreeBranch, | 38 | WorktreeBranch, |
| 38 | WorktreeDetached, | 39 | WorktreeDetached, |
| 39 | Local, | 40 | Local, |
| @@ -67,9 +68,11 @@ impl Pin { · 27 unmodified lines | |||
| 67 | #[derive(Clone, Debug, Eq, PartialEq)] | 68 | #[derive(Clone, Debug, Eq, PartialEq)] |
| 68 | pub(crate) struct WorktreeCheckout { | 69 | pub(crate) struct WorktreeCheckout { |
| 69 | pub id: ObjectId, | 70 | pub id: ObjectId, |
| 71 | pub label_id: ObjectId, | ||
| 70 | pub name: BString, | 72 | pub name: BString, |
| 71 | pub reference: Option<gix::refs::FullName>, | 73 | pub reference: Option<gix::refs::FullName>, |
| 72 | pub is_current: bool, | 74 | pub is_current: bool, |
| 75 | pub is_detached: bool, | ||
| 73 | } | 76 | } |
| 74 | 77 | ||
| 75 | #[derive(Clone, Debug, Eq, PartialEq)] | 78 | #[derive(Clone, Debug, Eq, PartialEq)] |
| @@ -1213,7 +1216,7 @@ pub(crate) fn tree_revisions(repo: &gix::Repository, include_tags: bool) -> Resu · 1137 unmodified lines | |||
| 1213 | }; | 1216 | }; |
| 1214 | let name = reference.name().as_bstr().to_owned(); | 1217 | let name = reference.name().as_bstr().to_owned(); |
| 1215 | let kind = decoration_kind(&name); | 1218 | let kind = decoration_kind(&name); |
| 1216 | if kind == DecorationKind::Special | 1219 | if matches!(kind, DecorationKind::Special | DecorationKind::Pin) |
| 1217 | || matches!(kind, DecorationKind::Tag) && !include_tags | 1220 | || matches!(kind, DecorationKind::Tag) && !include_tags |
| 1218 | || name.starts_with(STASH_PREFIX) | 1221 | || name.starts_with(STASH_PREFIX) |
| 1219 | || name.starts_with(REVIEW_STASH_PREFIX) | 1222 | || name.starts_with(REVIEW_STASH_PREFIX) |
| @@ -1299,6 +1302,7 @@ pub(crate) fn worktree_checkouts(repo: &gix::Repository) -> Vec<WorktreeCheckout · 79 unmodified lines | |||
| 1299 | } | 1302 | } |
| 1300 | out.sort_by(|a, b| { | 1303 | out.sort_by(|a, b| { |
| 1301 | a.id.cmp(&b.id) | 1304 | a.id.cmp(&b.id) |
| 1305 | .then_with(|| a.label_id.cmp(&b.label_id)) | ||
| 1302 | .then_with(|| a.name.cmp(&b.name)) | 1306 | .then_with(|| a.name.cmp(&b.name)) |
| 1303 | .then_with(|| a.reference.cmp(&b.reference)) | 1307 | .then_with(|| a.reference.cmp(&b.reference)) |
| 1304 | }); | 1308 | }); |
| @@ -1320,7 +1324,8 @@ fn add_worktree_checkout( · 15 unmodified lines | |||
| 1320 | return; | 1324 | return; |
| 1321 | } | 1325 | } |
| 1322 | }; | 1326 | }; |
| 1323 | let reference = head.referent_name().map(ToOwned::to_owned); | 1327 | let is_detached = head.is_detached(); |
| 1328 | let head_reference = head.referent_name().map(ToOwned::to_owned); | ||
| 1324 | let id = match head.try_peel_to_id() { | 1329 | let id = match head.try_peel_to_id() { |
| 1325 | Ok(Some(id)) => id.detach(), | 1330 | Ok(Some(id)) => id.detach(), |
| 1326 | Ok(None) => return, | 1331 | Ok(None) => return, |
| @@ -1329,6 +1334,12 @@ fn add_worktree_checkout( · 2 unmodified lines | |||
| 1329 | return; | 1334 | return; |
| 1330 | } | 1335 | } |
| 1331 | }; | 1336 | }; |
| 1337 | let remembered = is_detached | ||
| 1338 | .then(|| remembered_worktree_branch(repo, worktree)) | ||
| 1339 | .flatten(); | ||
| 1340 | let (reference, label_id) = remembered | ||
| 1341 | .or_else(|| head_reference.map(|reference| (reference, id))) | ||
| 1342 | .map_or((None, id), |(reference, id)| (Some(reference), id)); | ||
| 1332 | let name = match &reference { | 1343 | let name = match &reference { |
| 1333 | Some(reference) => reference.shorten().to_owned(), | 1344 | Some(reference) => reference.shorten().to_owned(), |
| 1334 | None => match detached_name { | 1345 | None => match detached_name { |
| @@ -1341,12 +1352,51 @@ fn add_worktree_checkout( · 6 unmodified lines | |||
| 1341 | }; | 1352 | }; |
| 1342 | out.push(WorktreeCheckout { | 1353 | out.push(WorktreeCheckout { |
| 1343 | id, | 1354 | id, |
| 1355 | label_id, | ||
| 1344 | name, | 1356 | name, |
| 1345 | reference, | 1357 | reference, |
| 1346 | is_current, | 1358 | is_current, |
| 1359 | is_detached, | ||
| 1347 | }); | 1360 | }); |
| 1348 | } | 1361 | } |
| 1349 | 1362 | ||
| 1363 | fn remembered_worktree_branch(repo: &gix::Repository, worktree: &BStr) -> Option<(gix::refs::FullName, ObjectId)> { | ||
| 1364 | let mut pin = match repo.try_find_reference(HEAD_PIN_NAME.as_bstr()) { | ||
| 1365 | Ok(Some(pin)) => pin, | ||
| 1366 | Ok(None) => return None, | ||
| 1367 | Err(err) => { | ||
| 1368 | tracing::warn!(%worktree, error = %err, "ignoring unreadable worktree HEAD pin"); | ||
| 1369 | return None; | ||
| 1370 | } | ||
| 1371 | }; | ||
| 1372 | let Some(reference) = pin.target().try_name().map(ToOwned::to_owned) else { | ||
| 1373 | tracing::warn!(%worktree, "ignoring direct worktree HEAD pin"); | ||
| 1374 | return None; | ||
| 1375 | }; | ||
| 1376 | if !reference.as_bstr().starts_with(b"refs/heads/") { | ||
| 1377 | tracing::warn!(%worktree, name = %reference, "ignoring non-branch worktree HEAD pin"); | ||
| 1378 | return None; | ||
| 1379 | } | ||
| 1380 | let id = match pin.peel_to_id() { | ||
| 1381 | Ok(id) => id.detach(), | ||
| 1382 | Err(err) => { | ||
| 1383 | tracing::warn!(%worktree, name = %reference, error = %err, "ignoring unresolved worktree HEAD pin"); | ||
| 1384 | return None; | ||
| 1385 | } | ||
| 1386 | }; | ||
| 1387 | match repo.find_header(id) { | ||
| 1388 | Ok(header) if header.kind() == gix::object::Kind::Commit => Some((reference, id)), | ||
| 1389 | Ok(_) => { | ||
| 1390 | tracing::warn!(%worktree, name = %reference, "ignoring worktree HEAD pin with a non-commit target"); | ||
| 1391 | None | ||
| 1392 | } | ||
| 1393 | Err(err) => { | ||
| 1394 | tracing::warn!(%worktree, name = %reference, error = %err, "ignoring unreadable worktree HEAD pin target"); | ||
| 1395 | None | ||
| 1396 | } | ||
| 1397 | } | ||
| 1398 | } | ||
| 1399 | |||
| 1350 | fn worktree_basename(path: &std::path::Path) -> Option<BString> { | 1400 | fn worktree_basename(path: &std::path::Path) -> Option<BString> { |
| 1351 | path.file_name() | 1401 | path.file_name() |
| 1352 | .and_then(|name| gix::path::os_str_into_bstr(name).ok()) | 1402 | .and_then(|name| gix::path::os_str_into_bstr(name).ok()) |
| @@ -1742,13 +1792,12 @@ pub(crate) fn decorations(repo: &gix::Repository, pins: &[Pin], worktrees: &[Wor · 389 unmodified lines | |||
| 1742 | continue; | 1792 | continue; |
| 1743 | }; | 1793 | }; |
| 1744 | let id = id.detach(); | 1794 | let id = id.detach(); |
| 1745 | if worktrees | 1795 | if worktrees.iter().any(|worktree| { |
| 1746 | .iter() | 1796 | worktree.is_current && worktree.label_id == id && worktree.reference.as_ref() == Some(&full_name) |
| 1747 | .any(|worktree| worktree.is_current && worktree.id == id && worktree.reference.as_ref() == Some(&full_name)) | 1797 | }) { |
| 1748 | { | ||
| 1749 | kind = DecorationKind::CurrentWorktreeBranch; | 1798 | kind = DecorationKind::CurrentWorktreeBranch; |
| 1750 | } else if worktrees.iter().any(|worktree| { | 1799 | } else if worktrees.iter().any(|worktree| { |
| 1751 | !worktree.is_current && worktree.id == id && worktree.reference.as_ref() == Some(&full_name) | 1800 | !worktree.is_current && worktree.label_id == id && worktree.reference.as_ref() == Some(&full_name) |
| 1752 | }) { | 1801 | }) { |
| 1753 | kind = DecorationKind::WorktreeBranch; | 1802 | kind = DecorationKind::WorktreeBranch; |
| 1754 | } | 1803 | } |
| @@ -1775,13 +1824,14 @@ pub(crate) fn decorations(repo: &gix::Repository, pins: &[Pin], worktrees: &[Wor · 20 unmodified lines | |||
| 1775 | } | 1824 | } |
| 1776 | out.entry(id).or_default().push(Decoration { name, kind }); | 1825 | out.entry(id).or_default().push(Decoration { name, kind }); |
| 1777 | } | 1826 | } |
| 1778 | for worktree in worktrees.iter().filter(|worktree| !worktree.is_current) { | 1827 | for worktree in worktrees.iter().filter(|worktree| worktree.reference.is_none()) { |
| 1779 | let kind = if worktree.reference.is_some() { | 1828 | let kind = match (worktree.is_current, worktree.is_detached) { |
| 1780 | DecorationKind::WorktreeBranch | 1829 | (true, true) => DecorationKind::CurrentWorktreeDetached, |
| 1781 | } else { | 1830 | (true, false) => DecorationKind::CurrentWorktreeBranch, |
| 1782 | DecorationKind::WorktreeDetached | 1831 | (false, true) => DecorationKind::WorktreeDetached, |
| 1832 | (false, false) => DecorationKind::WorktreeBranch, | ||
| 1783 | }; | 1833 | }; |
| 1784 | let decorations = out.entry(worktree.id).or_default(); | 1834 | let decorations = out.entry(worktree.label_id).or_default(); |
| 1785 | if !decorations | 1835 | if !decorations |
| 1786 | .iter() | 1836 | .iter() |
| 1787 | .any(|decoration| decoration.kind == kind && decoration.name == worktree.name) | 1837 | .any(|decoration| decoration.kind == kind && decoration.name == worktree.name) |
| @@ -2094,8 +2144,10 @@ mod tests { · 306 unmodified lines | |||
| 2094 | let worktrees = worktree_checkouts(&repo); | 2144 | let worktrees = worktree_checkouts(&repo); |
| 2095 | assert!(worktrees.iter().any(|worktree| { | 2145 | assert!(worktrees.iter().any(|worktree| { |
| 2096 | worktree.id == main | 2146 | worktree.id == main |
| 2147 | && worktree.label_id == main | ||
| 2097 | && worktree.name == "main" | 2148 | && worktree.name == "main" |
| 2098 | && worktree.is_current | 2149 | && worktree.is_current |
| 2150 | && !worktree.is_detached | ||
| 2099 | && worktree | 2151 | && worktree |
| 2100 | .reference | 2152 | .reference |
| 2101 | .as_ref() | 2153 | .as_ref() |
| @@ -2103,8 +2155,10 @@ mod tests { · 1 unmodified line | |||
| 2103 | })); | 2155 | })); |
| 2104 | assert!(worktrees.iter().any(|worktree| { | 2156 | assert!(worktrees.iter().any(|worktree| { |
| 2105 | worktree.id == topic | 2157 | worktree.id == topic |
| 2158 | && worktree.label_id == topic | ||
| 2106 | && worktree.name == "topic" | 2159 | && worktree.name == "topic" |
| 2107 | && !worktree.is_current | 2160 | && !worktree.is_current |
| 2161 | && !worktree.is_detached | ||
| 2108 | && worktree | 2162 | && worktree |
| 2109 | .reference | 2163 | .reference |
| 2110 | .as_ref() | 2164 | .as_ref() |
| @@ -2112,9 +2166,11 @@ mod tests { · 1 unmodified line | |||
| 2112 | })); | 2166 | })); |
| 2113 | assert!(worktrees.iter().any(|worktree| { | 2167 | assert!(worktrees.iter().any(|worktree| { |
| 2114 | worktree.id == root | 2168 | worktree.id == root |
| 2169 | && worktree.label_id == root | ||
| 2115 | && worktree.name == "detached-wt" | 2170 | && worktree.name == "detached-wt" |
| 2116 | && worktree.reference.is_none() | 2171 | && worktree.reference.is_none() |
| 2117 | && !worktree.is_current | 2172 | && !worktree.is_current |
| 2173 | && worktree.is_detached | ||
| 2118 | })); | 2174 | })); |
| 2119 | assert_eq!(worktrees.len(), 3, "the malformed worktree is ignored"); | 2175 | assert_eq!(worktrees.len(), 3, "the malformed worktree is ignored"); |
| 2120 | 2176 | ||
| @@ -2181,14 +2237,43 @@ mod tests { · 60 unmodified lines | |||
| 2181 | .find(|worktree| worktree.is_current) | 2237 | .find(|worktree| worktree.is_current) |
| 2182 | .expect("the current detached worktree is discovered"); | 2238 | .expect("the current detached worktree is discovered"); |
| 2183 | assert!(current.reference.is_none()); | 2239 | assert!(current.reference.is_none()); |
| 2240 | assert!(current.is_detached); | ||
| 2241 | assert_eq!(current.label_id, current.id); | ||
| 2184 | let detached_decorations = decorations(&detached_repo, &[], &detached_worktrees)?; | 2242 | let detached_decorations = decorations(&detached_repo, &[], &detached_worktrees)?; |
| 2243 | assert!(detached_decorations.get(¤t.id).is_some_and(|decorations| { | ||
| 2244 | decorations.iter().any(|decoration| { | ||
| 2245 | decoration.kind == DecorationKind::CurrentWorktreeDetached && decoration.name == current.name | ||
| 2246 | }) | ||
| 2247 | })); | ||
| 2248 | |||
| 2249 | let symbolic = Command::new("git") | ||
| 2250 | .current_dir(&linked_path) | ||
| 2251 | .args(["symbolic-ref", "refs/worktree/tix/pins/HEAD", "refs/heads/topic"]) | ||
| 2252 | .status()?; | ||
| 2253 | assert!(symbolic.success(), "git remembers the detached worktree's branch"); | ||
| 2254 | let remembered_repo = crate::test_repository::open(&linked_path)?; | ||
| 2255 | let remembered_worktrees = worktree_checkouts(&remembered_repo); | ||
| 2256 | let current = remembered_worktrees | ||
| 2257 | .iter() | ||
| 2258 | .find(|worktree| worktree.is_current) | ||
| 2259 | .expect("the remembered detached worktree is discovered"); | ||
| 2260 | assert_ne!(current.id, topic, "the worktree remains detached away from its branch"); | ||
| 2261 | assert_eq!(current.label_id, topic, "the label follows the remembered branch tip"); | ||
| 2262 | assert_eq!(current.name, "topic"); | ||
| 2263 | assert!(current.is_detached); | ||
| 2185 | assert!( | 2264 | assert!( |
| 2186 | !detached_decorations | 2265 | current |
| 2187 | .get(¤t.id) | 2266 | .reference |
| 2188 | .is_some_and(|decorations| decorations.iter().any(|decoration| { | 2267 | .as_ref() |
| 2189 | decoration.kind == DecorationKind::WorktreeDetached && decoration.name == current.name | 2268 | .is_some_and(|name| name.as_bstr() == b"refs/heads/topic") |
| 2190 | })) | ||
| 2191 | ); | 2269 | ); |
| 2270 | let remembered_pins = applicable_pins(&remembered_repo)?; | ||
| 2271 | let remembered_decorations = decorations(&remembered_repo, &remembered_pins, &remembered_worktrees)?; | ||
| 2272 | assert!(remembered_decorations.get(&topic).is_some_and(|decorations| { | ||
| 2273 | decorations | ||
| 2274 | .iter() | ||
| 2275 | .any(|decoration| decoration.kind == DecorationKind::HeadPinBranch && decoration.name == "topic") | ||
| 2276 | })); | ||
| 2192 | Ok(()) | 2277 | Ok(()) |
| 2193 | } | 2278 | } |
| 2194 | 2279 | ||
| @@ -2531,8 +2616,14 @@ mod tests { · 336 unmodified lines | |||
| 2531 | 2616 | ||
| 2532 | #[test] | 2617 | #[test] |
| 2533 | fn tree_revisions_cover_normal_refs_and_optionally_tags() -> gix_testtools::Result { | 2618 | fn tree_revisions_cover_normal_refs_and_optionally_tags() -> gix_testtools::Result { |
| 2534 | let fixture = fixture()?; | 2619 | let fixture = gix_testtools::scripted_fixture_writable("history.sh")?; |
| 2535 | let repo = crate::test_repository::open(&fixture)?; | 2620 | let repo = crate::test_repository::open(fixture.path())?; |
| 2621 | repo.reference( | ||
| 2622 | "refs/worktree/tix/pins/test", | ||
| 2623 | repo.head_id()?, | ||
| 2624 | gix::refs::transaction::PreviousValue::MustNotExist, | ||
| 2625 | "test tree revisions", | ||
| 2626 | )?; | ||
| 2536 | let names = |include_tags| { | 2627 | let names = |include_tags| { |
| 2537 | tree_revisions(&repo, include_tags).map(|revisions| { | 2628 | tree_revisions(&repo, include_tags).map(|revisions| { |
| 2538 | revisions | 2629 | revisions |
| @@ -2556,6 +2647,12 @@ mod tests { · 17 unmodified lines | |||
| 2556 | without_tags.iter().all(|name| !name.starts_with("refs/tags/")), | 2647 | without_tags.iter().all(|name| !name.starts_with("refs/tags/")), |
| 2557 | "tagless rendering does not traverse tag-only history" | 2648 | "tagless rendering does not traverse tag-only history" |
| 2558 | ); | 2649 | ); |
| 2650 | assert!( | ||
| 2651 | with_tags | ||
| 2652 | .iter() | ||
| 2653 | .all(|name| !name.starts_with("refs/worktree/tix/pins/")), | ||
| 2654 | "ordinary pins never become tree traversal tips" | ||
| 2655 | ); | ||
| 2559 | Ok(()) | 2656 | Ok(()) |
| 2560 | } | 2657 | } |
| 2561 | 2658 | ||
| @@ -1571,40 +1571,6 @@ fn event_loop( · 1570 unmodified lines | |||
| 1571 | urgent = true; | 1571 | urgent = true; |
| 1572 | continue; | 1572 | continue; |
| 1573 | } | 1573 | } |
| 1574 | tree::Input::Pin { id, include_tags } => { | ||
| 1575 | match open_repository(&repository_path, repository_is_bare, false) { | ||
| 1576 | Ok(repository) => match edit::time_travel::create_tree_pin(&repository, id, include_tags) { | ||
| 1577 | Ok((pin, created)) => { | ||
| 1578 | let label = edit::time_travel::pin_label(&pin); | ||
| 1579 | tree.leave_message(if created { | ||
| 1580 | format!("created {label}") | ||
| 1581 | } else { | ||
| 1582 | format!("already pinned as {label}") | ||
| 1583 | }); | ||
| 1584 | refresh_pending |= created; | ||
| 1585 | } | ||
| 1586 | Err(err) => tree.leave_message(format!("pin: {err:#}")), | ||
| 1587 | }, | ||
| 1588 | Err(err) => tree.leave_message(format!("pin: {err:#}")), | ||
| 1589 | } | ||
| 1590 | dirty = true; | ||
| 1591 | urgent = true; | ||
| 1592 | continue; | ||
| 1593 | } | ||
| 1594 | tree::Input::Unpin { id, fallback } => { | ||
| 1595 | match edit::time_travel::remove_pins(&repository_path, repository_is_bare, id) { | ||
| 1596 | Ok(0) => tree.leave_message("no pins removed"), | ||
| 1597 | Ok(count) => { | ||
| 1598 | tree.leave_message(format!("removed {count} pin{}", if count == 1 { "" } else { "s" })); | ||
| 1599 | tree.select_after_reference_deletion(fallback); | ||
| 1600 | refresh_pending = true; | ||
| 1601 | } | ||
| 1602 | Err(err) => tree.leave_message(format!("unpin: {err:#}")), | ||
| 1603 | } | ||
| 1604 | dirty = true; | ||
| 1605 | urgent = true; | ||
| 1606 | continue; | ||
| 1607 | } | ||
| 1608 | tree::Input::Quit => return Ok(None), | 1574 | tree::Input::Quit => return Ok(None), |
| 1609 | }, | 1575 | }, |
| 1610 | TerminalEvent::Mouse(mouse) if tree.handle_mouse(mouse.kind, 1) => { | 1576 | TerminalEvent::Mouse(mouse) if tree.handle_mouse(mouse.kind, 1) => { |
| @@ -46,8 +46,7 @@ struct Node { · 45 unmodified lines | |||
| 46 | decorations: Vec<Decoration>, | 46 | decorations: Vec<Decoration>, |
| 47 | is_head: bool, | 47 | is_head: bool, |
| 48 | is_anchor: bool, | 48 | is_anchor: bool, |
| 49 | is_pinned: bool, | 49 | is_detached_worktree: bool, |
| 50 | can_pin: bool, | ||
| 51 | raw_tip: bool, | 50 | raw_tip: bool, |
| 52 | sort_key: String, | 51 | sort_key: String, |
| 53 | } | 52 | } |
| @@ -129,14 +128,6 @@ pub(crate) enum Input { · 75 unmodified lines | |||
| 129 | groups: Vec<RemoteDeletion>, | 128 | groups: Vec<RemoteDeletion>, |
| 130 | fallback: SelectionFallback, | 129 | fallback: SelectionFallback, |
| 131 | }, | 130 | }, |
| 132 | Pin { | ||
| 133 | id: ObjectId, | ||
| 134 | include_tags: bool, | ||
| 135 | }, | ||
| 136 | Unpin { | ||
| 137 | id: ObjectId, | ||
| 138 | fallback: SelectionFallback, | ||
| 139 | }, | ||
| 140 | Quit, | 131 | Quit, |
| 141 | } | 132 | } |
| 142 | 133 | ||
| @@ -314,30 +305,6 @@ impl Tree { · 171 unmodified lines | |||
| 314 | self.toggle_count_anchor(); | 305 | self.toggle_count_anchor(); |
| 315 | return Input::Handled; | 306 | return Input::Handled; |
| 316 | } | 307 | } |
| 317 | if key.code == KeyCode::Char('p') && key.modifiers.is_empty() { | ||
| 318 | let Some(node) = self | ||
| 319 | .selected | ||
| 320 | .and_then(|selected| self.overview.as_ref()?.nodes.get(selected)) | ||
| 321 | else { | ||
| 322 | return Input::Handled; | ||
| 323 | }; | ||
| 324 | if node.is_pinned { | ||
| 325 | return Input::Unpin { | ||
| 326 | id: node.id, | ||
| 327 | fallback: self | ||
| 328 | .reference_deletion_fallback() | ||
| 329 | .expect("a selected pin has a deletion fallback"), | ||
| 330 | }; | ||
| 331 | } | ||
| 332 | if !node.can_pin { | ||
| 333 | self.message = Some("no reference at the selected node".into()); | ||
| 334 | return Input::Handled; | ||
| 335 | } | ||
| 336 | return Input::Pin { | ||
| 337 | id: node.id, | ||
| 338 | include_tags: !self.hide_tags, | ||
| 339 | }; | ||
| 340 | } | ||
| 341 | if key.code == KeyCode::Char('G') | 308 | if key.code == KeyCode::Char('G') |
| 342 | || key.code == KeyCode::Char('g') && key.modifiers.contains(KeyModifiers::SHIFT) | 309 | || key.code == KeyCode::Char('g') && key.modifiers.contains(KeyModifiers::SHIFT) |
| 343 | { | 310 | { |
| @@ -514,21 +481,8 @@ impl Tree { · 170 unmodified lines | |||
| 514 | || "Space counts:auto".into(), | 481 | || "Space counts:auto".into(), |
| 515 | |id| format!("Space counts:{}", self.node_label(id)), | 482 | |id| format!("Space counts:{}", self.node_label(id)), |
| 516 | ); | 483 | ); |
| 517 | let pin = self | ||
| 518 | .selected | ||
| 519 | .and_then(|selected| self.overview.as_ref()?.nodes.get(selected)) | ||
| 520 | .and_then(|node| { | ||
| 521 | if node.is_pinned { | ||
| 522 | Some(" · p unpin") | ||
| 523 | } else if node.can_pin { | ||
| 524 | Some(" · p pin") | ||
| 525 | } else { | ||
| 526 | None | ||
| 527 | } | ||
| 528 | }) | ||
| 529 | .unwrap_or_default(); | ||
| 530 | format!( | 484 | format!( |
| 531 | "tree · {motion} · {counts}{pin} · g top · G root · T tags:{tags} · Shift+directions pan · e edit · t/Esc history" | 485 | "tree · {motion} · {counts} · g top · G root · T tags:{tags} · Shift+directions pan · e edit · t/Esc history" |
| 532 | ) | 486 | ) |
| 533 | }; | 487 | }; |
| 534 | frame.render_widget( | 488 | frame.render_widget( |
| @@ -751,8 +705,7 @@ impl Overview { · 216 unmodified lines | |||
| 751 | let mut labels = HashMap::<CommitIndex, Vec<Decoration>>::new(); | 705 | let mut labels = HashMap::<CommitIndex, Vec<Decoration>>::new(); |
| 752 | let mut heads = HashSet::new(); | 706 | let mut heads = HashSet::new(); |
| 753 | let mut anchors = HashSet::new(); | 707 | let mut anchors = HashSet::new(); |
| 754 | let mut pinned = HashSet::new(); | 708 | let mut detached_worktrees = HashSet::new(); |
| 755 | let mut pinnable = HashSet::new(); | ||
| 756 | for (id, decorations) in decorations { | 709 | for (id, decorations) in decorations { |
| 757 | let Some(index) = graph.index(*id) else { continue }; | 710 | let Some(index) = graph.index(*id) else { continue }; |
| 758 | for decoration in decorations { | 711 | for decoration in decorations { |
| @@ -762,27 +715,30 @@ impl Overview { · 3 unmodified lines | |||
| 762 | if decoration.kind == DecorationKind::Head { | 715 | if decoration.kind == DecorationKind::Head { |
| 763 | heads.insert(index); | 716 | heads.insert(index); |
| 764 | anchors.insert(index); | 717 | anchors.insert(index); |
| 765 | pinnable.insert(index); | ||
| 766 | } else if decoration.kind == DecorationKind::Pin { | 718 | } else if decoration.kind == DecorationKind::Pin { |
| 767 | pinned.insert(index); | 719 | continue; |
| 768 | anchors.insert(index); | ||
| 769 | } else if decoration.kind != DecorationKind::Special { | 720 | } else if decoration.kind != DecorationKind::Special { |
| 770 | labels.entry(index).or_default().push(decoration.clone()); | 721 | labels.entry(index).or_default().push(decoration.clone()); |
| 771 | anchors.insert(index); | 722 | anchors.insert(index); |
| 772 | if decoration.kind != DecorationKind::Stash { | ||
| 773 | pinnable.insert(index); | ||
| 774 | } | ||
| 775 | } | 723 | } |
| 776 | } | 724 | } |
| 777 | } | 725 | } |
| 726 | for worktree in refs.worktrees.iter().filter(|worktree| worktree.is_detached) { | ||
| 727 | let Some(index) = graph.index(worktree.id) else { | ||
| 728 | continue; | ||
| 729 | }; | ||
| 730 | detached_worktrees.insert(index); | ||
| 731 | anchors.insert(index); | ||
| 732 | } | ||
| 733 | let pin_tips: HashSet<_> = refs.pins.iter().map(|pin| pin.id).collect(); | ||
| 778 | let raw: HashSet<_> = refs | 734 | let raw: HashSet<_> = refs |
| 779 | .view_tips | 735 | .view_tips |
| 780 | .iter() | 736 | .iter() |
| 781 | .chain(&refs.hidden_tips) | 737 | .chain(&refs.hidden_tips) |
| 738 | .filter(|id| !pin_tips.contains(*id)) | ||
| 782 | .filter_map(|id| graph.index(*id)) | 739 | .filter_map(|id| graph.index(*id)) |
| 783 | .inspect(|index| { | 740 | .inspect(|index| { |
| 784 | anchors.insert(*index); | 741 | anchors.insert(*index); |
| 785 | pinnable.insert(*index); | ||
| 786 | }) | 742 | }) |
| 787 | .collect(); | 743 | .collect(); |
| 788 | if anchors.is_empty() { | 744 | if anchors.is_empty() { |
| @@ -849,8 +805,7 @@ impl Overview { · 60 unmodified lines | |||
| 849 | decorations, | 805 | decorations, |
| 850 | is_head: heads.contains(&commit), | 806 | is_head: heads.contains(&commit), |
| 851 | is_anchor: anchors.contains(&commit), | 807 | is_anchor: anchors.contains(&commit), |
| 852 | is_pinned: pinned.contains(&commit), | 808 | is_detached_worktree: detached_worktrees.contains(&commit), |
| 853 | can_pin: pinnable.contains(&commit), | ||
| 854 | raw_tip: raw.contains(&commit), | 809 | raw_tip: raw.contains(&commit), |
| 855 | sort_key, | 810 | sort_key, |
| 856 | } | 811 | } |
| @@ -1249,7 +1204,7 @@ fn rail_label(node: &Node, count: Option<usize>) -> String { · 392 unmodified lines | |||
| 1249 | out.push(' '); | 1204 | out.push(' '); |
| 1250 | } | 1205 | } |
| 1251 | out.push_str(&suffix); | 1206 | out.push_str(&suffix); |
| 1252 | if node.is_pinned { | 1207 | if node.is_detached_worktree { |
| 1253 | if !out.is_empty() { | 1208 | if !out.is_empty() { |
| 1254 | out.push(' '); | 1209 | out.push(' '); |
| 1255 | } | 1210 | } |
| @@ -1262,7 +1217,17 @@ fn node_label(node: &Node) -> String { · 6 unmodified lines | |||
| 1262 | let labels: Vec<_> = node | 1217 | let labels: Vec<_> = node |
| 1263 | .decorations | 1218 | .decorations |
| 1264 | .iter() | 1219 | .iter() |
| 1265 | .map(|decoration| decoration.name.to_str_lossy()) | 1220 | .map(|decoration| { |
| 1221 | let name = decoration.name.to_str_lossy(); | ||
| 1222 | match decoration.kind { | ||
| 1223 | DecorationKind::CurrentWorktreeBranch | DecorationKind::CurrentWorktreeDetached => { | ||
| 1224 | format!("@{name}") | ||
| 1225 | } | ||
| 1226 | DecorationKind::WorktreeBranch | DecorationKind::WorktreeDetached => format!("{name}@"), | ||
| 1227 | DecorationKind::HeadPinBranch => format!("★{name}"), | ||
| 1228 | _ => name.into_owned(), | ||
| 1229 | } | ||
| 1230 | }) | ||
| 1266 | .collect(); | 1231 | .collect(); |
| 1267 | if !labels.is_empty() { | 1232 | if !labels.is_empty() { |
| 1268 | labels.join(", ") | 1233 | labels.join(", ") |
| @@ -1854,8 +1819,8 @@ mod tests { · 585 unmodified lines | |||
| 1854 | } | 1819 | } |
| 1855 | 1820 | ||
| 1856 | #[test] | 1821 | #[test] |
| 1857 | fn pins_are_markers_and_p_toggles_only_reference_nodes() { | 1822 | fn detached_worktrees_are_pin_markers_while_ordinary_pins_and_p_are_hidden() { |
| 1858 | let (graph, refs, mut decorations) = fixture(); | 1823 | let (graph, mut refs, mut decorations) = fixture(); |
| 1859 | *decorations.get_mut(&id(5)).expect("topic is decorated") = vec![ | 1824 | *decorations.get_mut(&id(5)).expect("topic is decorated") = vec![ |
| 1860 | Decoration { | 1825 | Decoration { |
| 1861 | name: "pin:first".into(), | 1826 | name: "pin:first".into(), |
| @@ -1866,46 +1831,41 @@ mod tests { · 4 unmodified lines | |||
| 1866 | kind: DecorationKind::Pin, | 1831 | kind: DecorationKind::Pin, |
| 1867 | }, | 1832 | }, |
| 1868 | ]; | 1833 | ]; |
| 1834 | refs.pins.push(crate::history::Pin { | ||
| 1835 | name: "refs/worktree/tix/pins/first".try_into().expect("valid"), | ||
| 1836 | target: gix::refs::Target::Object(id(5)), | ||
| 1837 | id: id(5), | ||
| 1838 | }); | ||
| 1839 | refs.worktrees.push(crate::history::WorktreeCheckout { | ||
| 1840 | id: id(3), | ||
| 1841 | label_id: id(6), | ||
| 1842 | name: "main".into(), | ||
| 1843 | reference: Some("refs/heads/main".try_into().expect("valid")), | ||
| 1844 | is_current: true, | ||
| 1845 | is_detached: true, | ||
| 1846 | }); | ||
| 1847 | decorations.get_mut(&id(6)).expect("main is decorated")[0].kind = DecorationKind::HeadPinBranch; | ||
| 1869 | let mut tree = Tree::default(); | 1848 | let mut tree = Tree::default(); |
| 1870 | tree.rebuild(&graph, &refs, &decorations); | 1849 | tree.rebuild(&graph, &refs, &decorations); |
| 1871 | let topic = | ||
| 1872 | tree.overview.as_ref().expect("overview exists").by_commit[&graph.index(id(5)).expect("topic exists")]; | ||
| 1873 | tree.selected = Some(topic); | ||
| 1874 | |||
| 1875 | assert!(matches!( | ||
| 1876 | tree.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE)), | ||
| 1877 | Input::Unpin { id: selected, .. } if selected == id(5) | ||
| 1878 | )); | ||
| 1879 | let overview = tree.overview.as_ref().expect("overview exists"); | 1850 | let overview = tree.overview.as_ref().expect("overview exists"); |
| 1880 | assert!( | 1851 | assert!( |
| 1881 | overview.nodes[topic].decorations.is_empty(), | 1852 | !overview.nodes.iter().any(|node| node.id == id(5)), |
| 1882 | "pin-only tips need no label" | 1853 | "ordinary pin-only tips are absent" |
| 1883 | ); | 1854 | ); |
| 1884 | let unicode = render_overview(overview, true); | 1855 | let unicode = render_overview(overview, true); |
| 1885 | let ascii = render_overview(overview, false); | 1856 | let ascii = render_overview(overview, false); |
| 1886 | assert_eq!(unicode.matches('📌').count(), 1, "multiple pins share one marker"); | 1857 | assert_eq!(unicode.matches('📌').count(), 1, "detached HEAD has one marker"); |
| 1887 | assert!(ascii.contains("[pin]"), "ASCII diagnostics retain the pin marker"); | 1858 | assert!( |
| 1888 | assert!(!unicode.contains("pin:"), "internal pin names are hidden"); | 1859 | unicode.contains("★main"), |
| 1889 | 1860 | "the remembered branch stays at its actual tip" | |
| 1890 | let main = overview.by_commit[&graph.index(id(6)).expect("main exists")]; | ||
| 1891 | tree.selected = Some(main); | ||
| 1892 | assert_eq!( | ||
| 1893 | tree.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE)), | ||
| 1894 | Input::Pin { | ||
| 1895 | id: id(6), | ||
| 1896 | include_tags: true, | ||
| 1897 | }, | ||
| 1898 | "an unpinned reference offers pin creation" | ||
| 1899 | ); | 1861 | ); |
| 1900 | let fork = | 1862 | assert!(ascii.contains("[pin]"), "ASCII diagnostics retain detached state"); |
| 1901 | tree.overview.as_ref().expect("overview exists").by_commit[&graph.index(id(2)).expect("fork exists")]; | 1863 | assert!(!unicode.contains("pin:"), "ordinary pin names remain hidden"); |
| 1902 | tree.selected = Some(fork); | ||
| 1903 | assert_eq!( | 1864 | assert_eq!( |
| 1904 | tree.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE)), | 1865 | tree.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE)), |
| 1905 | Input::Handled, | 1866 | Input::Handled, |
| 1906 | "structural-only nodes cannot be pinned" | 1867 | "the tree has no pin action" |
| 1907 | ); | 1868 | ); |
| 1908 | assert_eq!(tree.message.as_deref(), Some("no reference at the selected node")); | ||
| 1909 | } | 1869 | } |
| 1910 | 1870 | ||
| 1911 | #[test] | 1871 | #[test] |
| @@ -1753,7 +1753,9 @@ fn metadata_line<'a>( · 1752 unmodified lines | |||
| 1753 | selected | 1753 | selected |
| 1754 | && matches!( | 1754 | && matches!( |
| 1755 | decoration.kind, | 1755 | decoration.kind, |
| 1756 | DecorationKind::WorktreeBranch | DecorationKind::WorktreeDetached | 1756 | DecorationKind::WorktreeBranch |
| 1757 | | DecorationKind::WorktreeDetached | ||
| 1758 | | DecorationKind::CurrentWorktreeDetached | ||
| 1757 | ) | 1759 | ) |
| 1758 | } | 1760 | } |
| 1759 | }) | 1761 | }) |
| @@ -1773,7 +1775,10 @@ fn metadata_line<'a>( · 13 unmodified lines | |||
| 1773 | format!("{name}@").into() | 1775 | format!("{name}@").into() |
| 1774 | } else if decoration.kind == DecorationKind::HeadPinBranch { | 1776 | } else if decoration.kind == DecorationKind::HeadPinBranch { |
| 1775 | format!("★{name}").into() | 1777 | format!("★{name}").into() |
| 1776 | } else if decoration.kind == DecorationKind::CurrentWorktreeBranch { | 1778 | } else if matches!( |
| 1779 | decoration.kind, | ||
| 1780 | DecorationKind::CurrentWorktreeBranch | DecorationKind::CurrentWorktreeDetached | ||
| 1781 | ) { | ||
| 1777 | format!("@{name}").into() | 1782 | format!("@{name}").into() |
| 1778 | } else { | 1783 | } else { |
| 1779 | name | 1784 | name |
| @@ -1951,7 +1956,9 @@ pub(crate) fn decoration_style(kind: DecorationKind) -> Style { · 171 unmodified lines | |||
| 1951 | DecorationKind::HeadPinBranch => Style::default().fg(Color::Cyan), | 1956 | DecorationKind::HeadPinBranch => Style::default().fg(Color::Cyan), |
| 1952 | DecorationKind::Stash => Style::default().fg(Color::LightMagenta).add_modifier(Modifier::BOLD), | 1957 | DecorationKind::Stash => Style::default().fg(Color::LightMagenta).add_modifier(Modifier::BOLD), |
| 1953 | DecorationKind::Review => Style::default().fg(Color::LightMagenta).add_modifier(Modifier::BOLD), | 1958 | DecorationKind::Review => Style::default().fg(Color::LightMagenta).add_modifier(Modifier::BOLD), |
| 1954 | DecorationKind::CurrentWorktreeBranch => Style::default().fg(Color::Cyan), | 1959 | DecorationKind::CurrentWorktreeBranch | DecorationKind::CurrentWorktreeDetached => { |
| 1960 | Style::default().fg(Color::Cyan) | ||
| 1961 | } | ||
| 1955 | DecorationKind::WorktreeBranch | DecorationKind::WorktreeDetached => Style::default().fg(Color::LightBlue), | 1962 | DecorationKind::WorktreeBranch | DecorationKind::WorktreeDetached => Style::default().fg(Color::LightBlue), |
| 1956 | DecorationKind::Local => Style::default().fg(Color::Cyan), | 1963 | DecorationKind::Local => Style::default().fg(Color::Cyan), |
| 1957 | DecorationKind::Remote => Style::default().fg(Color::Yellow), | 1964 | DecorationKind::Remote => Style::default().fg(Color::Yellow), |