jj_lib/
evolution.rs

1// Copyright 2025 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Utility for commit evolution history.
16
17use std::collections::VecDeque;
18use std::slice;
19use std::sync::Arc;
20
21use itertools::Itertools as _;
22use thiserror::Error;
23
24use crate::backend::BackendError;
25use crate::backend::BackendResult;
26use crate::backend::CommitId;
27use crate::commit::Commit;
28use crate::dag_walk;
29use crate::op_store::OpStoreError;
30use crate::op_store::OpStoreResult;
31use crate::op_store::OperationId;
32use crate::op_walk;
33use crate::operation::Operation;
34use crate::repo::ReadonlyRepo;
35use crate::repo::Repo as _;
36use crate::store::Store;
37
38/// Commit with predecessor information.
39#[derive(Clone, Debug)]
40pub struct CommitEvolutionEntry {
41    /// Commit id and metadata.
42    pub commit: Commit,
43    /// Operation where the commit was created or rewritten.
44    pub operation: Option<Operation>,
45}
46
47impl CommitEvolutionEntry {
48    /// Predecessor ids of this commit.
49    pub fn predecessor_ids(&self) -> &[CommitId] {
50        match &self.operation {
51            Some(op) => op.predecessors_for_commit(self.commit.id()).unwrap(),
52            None => &self.commit.store_commit().predecessors,
53        }
54    }
55
56    /// Predecessor commit objects of this commit.
57    pub fn predecessors(&self) -> impl ExactSizeIterator<Item = BackendResult<Commit>> + use<'_> {
58        let store = self.commit.store();
59        self.predecessor_ids().iter().map(|id| store.get_commit(id))
60    }
61}
62
63#[allow(missing_docs)]
64#[derive(Debug, Error)]
65pub enum WalkPredecessorsError {
66    #[error(transparent)]
67    Backend(#[from] BackendError),
68    #[error(transparent)]
69    OpStore(#[from] OpStoreError),
70    #[error("Predecessors cycle detected at operation {0}")]
71    CycleDetected(OperationId),
72}
73
74/// Walks operations to emit commit predecessors in reverse topological order.
75pub fn walk_predecessors(
76    repo: &ReadonlyRepo,
77    start_commits: &[CommitId],
78) -> impl Iterator<Item = Result<CommitEvolutionEntry, WalkPredecessorsError>> {
79    WalkPredecessors {
80        store: repo.store().clone(),
81        op_ancestors: op_walk::walk_ancestors(slice::from_ref(repo.operation())),
82        to_visit: start_commits.to_vec(),
83        queued: VecDeque::new(),
84    }
85}
86
87struct WalkPredecessors<I> {
88    store: Arc<Store>,
89    op_ancestors: I,
90    to_visit: Vec<CommitId>,
91    queued: VecDeque<CommitEvolutionEntry>,
92}
93
94impl<I> WalkPredecessors<I>
95where
96    I: Iterator<Item = OpStoreResult<Operation>>,
97{
98    fn try_next(&mut self) -> Result<Option<CommitEvolutionEntry>, WalkPredecessorsError> {
99        while !self.to_visit.is_empty() && self.queued.is_empty() {
100            let Some(op) = self.op_ancestors.next().transpose()? else {
101                // Scanned all operations, no fallback needed.
102                self.flush_commits()?;
103                break;
104            };
105            if !op.stores_commit_predecessors() {
106                // There may be concurrent ops, but let's simply switch to the
107                // legacy commit traversal. Operation history should be mostly
108                // linear.
109                self.scan_commits()?;
110                break;
111            }
112            self.visit_op(&op)?;
113        }
114        Ok(self.queued.pop_front())
115    }
116
117    /// Looks for predecessors within the given operation.
118    fn visit_op(&mut self, op: &Operation) -> Result<(), WalkPredecessorsError> {
119        let mut visited = Vec::new(); // transitive edges should be short
120        let mut i = 0;
121        while let Some(cur_id) = self.to_visit.get(i) {
122            if let Some(next_ids) = op.predecessors_for_commit(cur_id) {
123                if visited.contains(cur_id) {
124                    return Err(WalkPredecessorsError::CycleDetected(op.id().clone()));
125                }
126                let commit = self.store.get_commit(cur_id)?;
127                // Predecessors will be visited in reverse order if they appear
128                // in the same operation. See scan_commits() for why.
129                visited.extend(self.to_visit.splice(i..=i, next_ids.iter().rev().cloned()));
130                self.queued.push_back(CommitEvolutionEntry {
131                    commit,
132                    operation: Some(op.clone()),
133                });
134            } else {
135                i += 1;
136            }
137        }
138        Ok(())
139    }
140
141    /// Traverses predecessors from remainder commits.
142    fn scan_commits(&mut self) -> BackendResult<()> {
143        // TODO: commits to visit might be gc-ed if we make index not preserve
144        // commit.predecessor_ids.
145        let commits = dag_walk::topo_order_reverse_ok(
146            self.to_visit.drain(..).map(|id| self.store.get_commit(&id)),
147            |commit: &Commit| commit.id().clone(),
148            |commit: &Commit| {
149                // Predecessors don't need to follow any defined order. However
150                // in practice, if there are multiple predecessors, then usually
151                // the first predecessor is the previous version of the same
152                // change, and the other predecessors are commits that were
153                // squashed into it. If multiple commits are squashed at once,
154                // then they are usually recorded in chronological order. We
155                // want to show squashed commits in reverse chronological order,
156                // and we also want to show squashed commits before the squash
157                // destination (since the destination's subgraph may contain
158                // earlier squashed commits as well), so we visit the
159                // predecessors in reverse order.
160                let ids = &commit.store_commit().predecessors;
161                ids.iter()
162                    .rev()
163                    .map(|id| self.store.get_commit(id))
164                    .collect_vec()
165            },
166        )?;
167        self.queued
168            .extend(commits.into_iter().map(|commit| CommitEvolutionEntry {
169                commit,
170                operation: None,
171            }));
172        Ok(())
173    }
174
175    /// Moves remainder commits to output queue.
176    fn flush_commits(&mut self) -> BackendResult<()> {
177        // TODO: commits to visit might be gc-ed if we make index not preserve
178        // commit.predecessor_ids.
179        self.queued.reserve(self.to_visit.len());
180        for id in self.to_visit.drain(..) {
181            let commit = self.store.get_commit(&id)?;
182            self.queued.push_back(CommitEvolutionEntry {
183                commit,
184                operation: None,
185            });
186        }
187        Ok(())
188    }
189}
190
191impl<I> Iterator for WalkPredecessors<I>
192where
193    I: Iterator<Item = OpStoreResult<Operation>>,
194{
195    type Item = Result<CommitEvolutionEntry, WalkPredecessorsError>;
196
197    fn next(&mut self) -> Option<Self::Item> {
198        self.try_next().transpose()
199    }
200}