workon/stack.rs
1//! Stacked diff workflow support.
2//!
3//! This module provides the infrastructure for detecting and interacting with
4//! stacked-diff tools alongside git-workon's worktree management.
5//!
6//! ## Model
7//!
8//! Stack awareness is two-dimensional:
9//!
10//! - [`StackModel`] — which tool manages stacks (v1: Graphite; future: branchless, sapling, spr)
11//! - [`Granularity`] — how worktrees map to stacks (v1: [`Granularity::Stack`], one per stack)
12//!
13//! ## Default-on behavior
14//!
15//! When `workon.stackModel` resolves to anything other than [`StackModel::None`] (by explicit
16//! config or auto-detection), every command that has a meaningful stack-aware variant uses it
17//! by default. A `--no-stack` CLI flag (global across subcommands) downgrades any single
18//! invocation back to branch-flat behavior.
19//!
20//! ## Graphite (v1)
21//!
22//! Stack metadata is read directly from `refs/branch-metadata/*` git refs — blobs containing
23//! JSON written by the `gt` CLI. No `gt` process is needed for detection or visualization.
24//! `gt track` is invoked only when registering a new branch (in `workon new` when creating a
25//! fork off a stack-worktree's branch).
26
27mod graphite;
28
29use git2::Repository;
30
31use crate::error::Result;
32
33/// Which stacked-diff tool is managing stacks in this repository.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum StackModel {
36 /// No stack tool; today's branch-flat behavior.
37 None,
38 /// Graphite (`gt`) manages stacks via `refs/branch-metadata/*`.
39 Graphite,
40}
41
42impl StackModel {
43 /// Auto-detect the active stack model from the repository environment.
44 ///
45 /// Returns [`StackModel::Graphite`] when `gt` is on PATH **and** the repo has been
46 /// initialized with `gt init` (`.graphite_repo_config` exists). Otherwise returns
47 /// [`StackModel::None`].
48 pub fn detect(repo: &Repository) -> Self {
49 if graphite::detect_gt() && graphite::is_graphite_repo(repo) {
50 Self::Graphite
51 } else {
52 Self::None
53 }
54 }
55}
56
57/// How worktrees map to stacks.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum Granularity {
60 /// One worktree hosts an entire stack. The user navigates between branches inside it
61 /// using the stack tool's own commands (e.g. `gt up` / `gt down`).
62 Stack,
63}
64
65/// A stack of branches rooted at a trunk branch.
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct Stack {
68 /// The trunk branch this stack is rooted on (e.g., `"main"`).
69 pub trunk: String,
70 /// All non-trunk branches in the stack, in BFS order from bottom to top.
71 pub branches: Vec<String>,
72 /// The branch that is currently HEAD in the worktree.
73 pub current: String,
74}
75
76/// Return the stack for the worktree whose HEAD is `head_branch`, or `None` if the branch
77/// is not part of a tracked stack under `model`.
78///
79/// The returned [`Stack`] includes all branches reachable from the same stack root, not just
80/// the ancestors of `head_branch`, so branching stacks are fully represented.
81pub fn current_stack(
82 repo: &Repository,
83 head_branch: &str,
84 model: StackModel,
85) -> Result<Option<Stack>> {
86 match model {
87 StackModel::None => Ok(None),
88 StackModel::Graphite => graphite::current_stack(repo, head_branch).map_err(Into::into),
89 }
90}
91
92/// Returns `true` if `gt` is on PATH and this repository has been Graphite-initialized.
93pub fn is_graphite_active(repo: &Repository) -> bool {
94 graphite::detect_gt() && graphite::is_graphite_repo(repo)
95}