Skip to main content

grit_lib/
gitbutler.rs

1//! Recognize a GitButler workspace.
2//!
3//! GitButler applies several branches at once by checking out `gitbutler/workspace`, whose tip
4//! is a merge of every applied branch that GitButler maintains itself. That tip is bookkeeping,
5//! not work: tools that show history or status should show the applied branches instead, and
6//! should not commit on top of it, because GitButler rewrites the workspace branch and would
7//! lose such a commit.
8//!
9//! ```no_run
10//! # fn main() -> grit_lib::error::Result<()> {
11//! use grit_lib::gitbutler::GitButlerWorkspace;
12//! use grit_lib::prelude::{Repository, RepositoryOptions};
13//!
14//! let repo = Repository::discover(None, &RepositoryOptions::empty())?;
15//! if let Some(workspace) = GitButlerWorkspace::detect(&repo)? {
16//!     for head in &workspace.heads {
17//!         println!("{} {}", head.commit, head.branches.join(", "));
18//!     }
19//! }
20//! # Ok(())
21//! # }
22//! ```
23//!
24//! The branch and the commit marker it looks for are constants:
25//!
26//! ```rust
27//! use grit_lib::gitbutler::{WORKSPACE_BRANCH, WORKSPACE_COMMIT_MARKER};
28//!
29//! assert_eq!(WORKSPACE_BRANCH, "refs/heads/gitbutler/workspace");
30//! assert!("GitButler Workspace Commit\n\nbranches: a, b".starts_with(WORKSPACE_COMMIT_MARKER));
31//! ```
32
33use crate::error::Result;
34use crate::objects::ObjectId;
35use crate::refs::list_refs_for_repository;
36use crate::repo::Repository;
37use crate::state::{resolve_head_for_repository, HeadState};
38
39/// The branch GitButler checks out to apply several branches at once.
40pub const WORKSPACE_BRANCH: &str = "refs/heads/gitbutler/workspace";
41
42/// Text GitButler puts in the message of the workspace branch's tip commit.
43pub const WORKSPACE_COMMIT_MARKER: &str = "GitButler Workspace Commit";
44
45/// Local branches under this prefix are GitButler's own, not applied work.
46const GITBUTLER_BRANCH_PREFIX: &str = "refs/heads/gitbutler/";
47
48/// A checked-out GitButler workspace: `HEAD` is on [`WORKSPACE_BRANCH`] and its tip is the
49/// workspace commit ([`WORKSPACE_COMMIT_MARKER`] in its message).
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct GitButlerWorkspace {
52    /// The workspace commit at the tip of [`WORKSPACE_BRANCH`].
53    pub commit: ObjectId,
54    /// The heads merged into the workspace commit, in its parent order.
55    pub heads: Vec<WorkspaceHead>,
56}
57
58/// One head merged into a [`GitButlerWorkspace`].
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct WorkspaceHead {
61    /// The merged commit (a parent of the workspace commit).
62    pub commit: ObjectId,
63    /// Short names of the local branches that point at [`Self::commit`], sorted, leaving out
64    /// GitButler's own `gitbutler/*` branches. Empty when no branch points there.
65    pub branches: Vec<String>,
66}
67
68impl GitButlerWorkspace {
69    /// The workspace `repo` has checked out, or `None` when `HEAD` is not on one.
70    ///
71    /// `HEAD` must be on [`WORKSPACE_BRANCH`] and that branch's tip must be a commit whose
72    /// message contains [`WORKSPACE_COMMIT_MARKER`]; a `gitbutler/workspace` branch with any
73    /// other tip is treated as an ordinary branch.
74    ///
75    /// # Errors
76    ///
77    /// Returns ref store and object database errors from reading `HEAD`, the tip commit or the
78    /// local branches.
79    pub fn detect(repo: &Repository) -> Result<Option<Self>> {
80        let HeadState::Branch {
81            refname,
82            oid: Some(tip),
83            ..
84        } = resolve_head_for_repository(repo)?
85        else {
86            return Ok(None);
87        };
88        if refname != WORKSPACE_BRANCH {
89            return Ok(None);
90        }
91        let commit = repo.odb().read_commit(&tip)?;
92        if !commit.message.contains(WORKSPACE_COMMIT_MARKER) {
93            return Ok(None);
94        }
95        let branches = list_refs_for_repository(repo, "refs/heads/")?;
96        let heads = commit
97            .parents
98            .iter()
99            .map(|&parent| {
100                let mut names: Vec<String> = branches
101                    .iter()
102                    .filter(|(name, oid)| {
103                        *oid == parent && !name.starts_with(GITBUTLER_BRANCH_PREFIX)
104                    })
105                    .filter_map(|(name, _)| name.strip_prefix("refs/heads/"))
106                    .map(str::to_owned)
107                    .collect();
108                names.sort();
109                WorkspaceHead {
110                    commit: parent,
111                    branches: names,
112                }
113            })
114            .collect();
115        Ok(Some(Self { commit: tip, heads }))
116    }
117
118    /// The merged heads' commits, in parent order: where history of the applied work starts.
119    #[must_use]
120    pub fn head_commits(&self) -> Vec<ObjectId> {
121        self.heads.iter().map(|head| head.commit).collect()
122    }
123}