Skip to main content

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}