bazaar 3.5.3

Rust implementation of the Bazaar formats and protocols
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
//! The `Transport` trait and its companion types.
//!
//! `DirState` does all of its filesystem I/O through a
//! [`Transport`] implementation: read the dirstate file, write it
//! back, acquire a lock, stat a tracked file, read a symlink, list
//! a directory.  The pure crate never touches `std::fs` directly
//! (CLAUDE.md "Filesystem goes through Transport, not std::fs") so
//! the pyo3 adapter can supply a Python-file-backed transport, and
//! tests can use a `MemoryTransport` that models the filesystem in
//! a `HashMap`.

use super::LockState;

/// Stat result returned by [`Transport::lstat`].  Mirrors the subset of
/// `os.stat_result` fields that dirstate logic actually inspects:
/// mode (for kind + executable), size, mtime/ctime (for the cutoff
/// check), dev/ino (fed into `pack_stat`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StatInfo {
    pub mode: u32,
    pub size: u64,
    pub mtime: i64,
    pub ctime: i64,
    pub dev: u64,
    pub ino: u64,
}

impl StatInfo {
    /// Whether `mode` indicates a regular file (S_IFREG).
    pub fn is_file(&self) -> bool {
        self.mode & 0o170000 == 0o100000
    }
    /// Whether `mode` indicates a directory (S_IFDIR).
    pub fn is_dir(&self) -> bool {
        self.mode & 0o170000 == 0o040000
    }
    /// Whether `mode` indicates a symlink (S_IFLNK).
    pub fn is_symlink(&self) -> bool {
        self.mode & 0o170000 == 0o120000
    }
}

/// One entry yielded by [`Transport::list_dir`] — mirrors the
/// per-child tuple Python's `DirReader.read_dir` returns.
#[derive(Debug, Clone)]
pub struct DirEntryInfo {
    /// The child's utf8 basename (no trailing slash).
    pub basename: Vec<u8>,
    /// Filesystem kind, or `None` for kinds dirstate doesn't track
    /// (block / char / socket / fifo).
    pub kind: Option<crate::osutils::Kind>,
    /// Stat info from `lstat` on the child.
    pub stat: StatInfo,
    /// Absolute path of the child on disk (utf8 bytes).
    pub abspath: Vec<u8>,
}

/// Errors returned by [`Transport`] operations.
///
/// Variants are coarse on purpose: callers generally either propagate
/// the error or match on `NotFound` / `LockContention`. I/O errors are
/// normalised into `(ErrorKind, String)` so the enum stays
/// `Clone + PartialEq + Eq` and tests can compare values directly.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TransportError {
    /// The backing file does not exist. Returned by `read_all` /
    /// `exists` / lock acquisition when there is nothing to open.
    NotFound(String),
    /// A lock was requested but another process already holds it, or
    /// the transport is already locked in an incompatible mode.
    LockContention(String),
    /// The caller tried to operate on an unlocked transport (read,
    /// write, or unlock without a prior `lock_read` / `lock_write`).
    NotLocked,
    /// The caller tried to acquire a second lock while one was still
    /// held. Dirstate's model is that you unlock before relocking;
    /// explicit rather than RAII.
    AlreadyLocked,
    /// Catch-all for I/O errors from the underlying store. The
    /// `(ErrorKind, message)` pair is preserved so callers can branch
    /// on kind without losing the original diagnostic.
    Io {
        kind: std::io::ErrorKind,
        message: String,
    },
    /// Catch-all for backend-specific failures that don't map to any
    /// of the above (typically wrapped Python exceptions on the pyo3
    /// adapter side).
    Other(String),
}

impl From<std::io::Error> for TransportError {
    fn from(e: std::io::Error) -> Self {
        if e.kind() == std::io::ErrorKind::NotFound {
            TransportError::NotFound(e.to_string())
        } else {
            TransportError::Io {
                kind: e.kind(),
                message: e.to_string(),
            }
        }
    }
}

impl std::fmt::Display for TransportError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            TransportError::NotFound(p) => write!(f, "No such file: {}", p),
            TransportError::LockContention(p) => write!(f, "Lock contention: {}", p),
            TransportError::NotLocked => write!(f, "Transport is not locked"),
            TransportError::AlreadyLocked => write!(f, "Transport is already locked"),
            TransportError::Io { kind, message } => {
                write!(f, "I/O error ({:?}): {}", kind, message)
            }
            TransportError::Other(s) => write!(f, "Transport error: {}", s),
        }
    }
}

impl std::error::Error for TransportError {}

/// Decode a utf8-bytes path (as dirstate stores them) into a [`PathBuf`].
///
/// On Unix the bytes are used verbatim via `OsStr`; on other platforms
/// they are decoded as utf8.
#[cfg(unix)]
pub fn bytes_to_path(b: &[u8]) -> Result<std::path::PathBuf, TransportError> {
    use std::os::unix::ffi::OsStrExt;
    Ok(std::path::PathBuf::from(std::ffi::OsStr::from_bytes(b)))
}

#[cfg(not(unix))]
pub fn bytes_to_path(b: &[u8]) -> Result<std::path::PathBuf, TransportError> {
    String::from_utf8(b.to_vec())
        .map(std::path::PathBuf::from)
        .map_err(|e| TransportError::Other(e.to_string()))
}

/// Encode a [`Path`] back into the utf8-bytes form dirstate stores.
#[cfg(unix)]
pub fn path_to_bytes(p: &std::path::Path) -> Vec<u8> {
    use std::os::unix::ffi::OsStrExt;
    p.as_os_str().as_bytes().to_vec()
}

#[cfg(not(unix))]
pub fn path_to_bytes(p: &std::path::Path) -> Vec<u8> {
    p.to_string_lossy().into_owned().into_bytes()
}

/// Build a [`StatInfo`] from filesystem metadata.
pub fn stat_info_from_metadata(m: &std::fs::Metadata) -> Result<StatInfo, TransportError> {
    #[cfg(unix)]
    {
        use std::os::unix::fs::MetadataExt;
        Ok(StatInfo {
            mode: m.mode(),
            size: m.size(),
            mtime: m.mtime(),
            ctime: m.ctime(),
            dev: m.dev(),
            ino: m.ino(),
        })
    }
    #[cfg(not(unix))]
    {
        let _ = m;
        Err(TransportError::Other(
            "lstat unsupported on this platform".to_string(),
        ))
    }
}

fn kind_from_stat(stat: &StatInfo) -> Option<crate::osutils::Kind> {
    if stat.is_file() {
        Some(crate::osutils::Kind::File)
    } else if stat.is_dir() {
        Some(crate::osutils::Kind::Directory)
    } else if stat.is_symlink() {
        Some(crate::osutils::Kind::Symlink)
    } else {
        None
    }
}

/// `lstat(2)` on a dirstate path: stat without following symlinks.
///
/// Path-keyed and stateless, so both the real-filesystem and pyo3
/// transports share this implementation.
pub fn lstat_path(abspath: &[u8]) -> Result<StatInfo, TransportError> {
    let path = bytes_to_path(abspath)?;
    let metadata = std::fs::symlink_metadata(&path)?;
    stat_info_from_metadata(&metadata)
}

/// `readlink(2)` on a dirstate path, returning the target as utf8 bytes.
pub fn read_link_path(abspath: &[u8]) -> Result<Vec<u8>, TransportError> {
    let path = bytes_to_path(abspath)?;
    let target = std::fs::read_link(&path)?;
    Ok(path_to_bytes(&target))
}

/// Whether `abspath` is a directory carrying its own `.bzr/`, i.e. a
/// potential nested tree reference.
pub fn is_tree_reference_dir_path(abspath: &[u8]) -> Result<bool, TransportError> {
    if abspath.is_empty() {
        return Ok(false);
    }
    let path = bytes_to_path(abspath)?;
    Ok(path.join(".bzr").is_dir())
}

/// List a directory, returning one [`DirEntryInfo`] per child with its
/// `lstat` info filled in.
pub fn list_dir_path(abspath: &[u8]) -> Result<Vec<DirEntryInfo>, TransportError> {
    let path = bytes_to_path(abspath)?;
    let entries = std::fs::read_dir(&path)?;
    let mut out = Vec::new();
    for entry in entries {
        let entry = entry?;
        let name = entry.file_name();
        let basename = path_to_bytes(std::path::Path::new(&name));
        let metadata = entry.metadata()?;
        let stat = stat_info_from_metadata(&metadata)?;
        let kind = kind_from_stat(&stat);
        out.push(DirEntryInfo {
            basename,
            kind,
            stat,
            abspath: path_to_bytes(&entry.path()),
        });
    }
    Ok(out)
}

/// Single-file backing store for a [`DirState`].
///
/// Unlike `bazaar::transport::Transport` (the knit-side path-keyed
/// byte store), a dirstate transport represents exactly one file held
/// open across a lock.  The real-filesystem backend is a thin wrapper
/// over `std::fs`; tests use a `MemoryTransport`, and the pyo3 layer
/// uses a `PyFileTransport` that delegates to a Python file-like
/// object.  Operations:
///
/// * [`Transport::exists`] — whether the backing file exists.  Used
///   by `on_file` to decide whether to create a fresh dirstate.
/// * [`Transport::lock_read`] / [`Transport::lock_write`] — acquire
///   a lock on the backing file.  Explicit rather than RAII; the
///   caller must pair each lock with an `unlock`.  Re-locking while
///   already locked returns `AlreadyLocked`.
/// * [`Transport::unlock`] — release the current lock.
/// * [`Transport::lock_state`] — observe the current lock state.
/// * [`Transport::read_all`] — return the full file contents.
///   Requires a read or write lock.  The returned bytes are owned;
///   callers parse in memory (no streaming `readline` — the pure-Rust
///   `read_header` operates on a byte slice).
/// * [`Transport::write_all`] — replace the full file contents,
///   truncating any trailing bytes from the previous version.
///   Requires a write lock.  Implementations are expected to flush
///   before returning, but are not required to fdatasync — call
///   [`Transport::fdatasync`] for that.
/// * [`Transport::fdatasync`] — force the current contents to durable
///   storage.  Optional no-op for stores where fsync has no meaning
///   (e.g. in-memory tests); the trait method exists so
///   `DirState.save` can call it unconditionally.
///
/// The `&mut self` receivers are deliberate: every operation either
/// mutates the lock state, the file contents, or both.  Callers that
/// need shared access should wrap an implementation in their own
/// synchronisation primitive.
pub trait Transport {
    /// Whether the backing file exists. Does not require a lock.
    fn exists(&self) -> Result<bool, TransportError>;

    /// Acquire a read lock on the backing file. Returns
    /// `AlreadyLocked` if any lock is already held.
    fn lock_read(&mut self) -> Result<(), TransportError>;

    /// Acquire a write lock on the backing file. Returns
    /// `AlreadyLocked` if any lock is already held.
    fn lock_write(&mut self) -> Result<(), TransportError>;

    /// Release the current lock. Returns `NotLocked` if no lock was
    /// held.
    fn unlock(&mut self) -> Result<(), TransportError>;

    /// Current lock state, or `None` if no lock is held.
    fn lock_state(&self) -> Option<LockState>;

    /// Read the full contents of the backing file. Requires a read
    /// or write lock; returns `NotLocked` otherwise.
    fn read_all(&mut self) -> Result<Vec<u8>, TransportError>;

    /// Length of the backing file, in bytes. Used by the bisect path
    /// to know where the last record ends without pulling the whole
    /// file into memory. Requires a read or write lock; returns
    /// `NotLocked` otherwise. The default implementation reads the
    /// whole file and reports its length; concrete implementations
    /// should override with `fstat` or equivalent.
    #[allow(clippy::len_without_is_empty)]
    fn len(&mut self) -> Result<u64, TransportError> {
        Ok(self.read_all()?.len() as u64)
    }

    /// Read `len` bytes starting at `offset`. Used by the bisect path
    /// to avoid pulling the whole dirstate into memory just to probe a
    /// handful of locations. The default implementation reads the
    /// whole file via [`Self::read_all`] and slices; concrete
    /// implementations should override with a seek+read when possible.
    /// A read shorter than `len` is acceptable (e.g. when the requested
    /// range extends past EOF) — callers must tolerate short reads.
    fn read_at(&mut self, offset: u64, len: usize) -> Result<Vec<u8>, TransportError> {
        let all = self.read_all()?;
        let start = offset as usize;
        if start >= all.len() {
            return Ok(Vec::new());
        }
        let end = std::cmp::min(start + len, all.len());
        Ok(all[start..end].to_vec())
    }

    /// Replace the full contents of the backing file, truncating any
    /// trailing bytes from the previous version. Requires a write
    /// lock; returns `NotLocked` if no lock is held, and a generic
    /// error if only a read lock is held.
    fn write_all(&mut self, bytes: &[u8]) -> Result<(), TransportError>;

    /// Temporarily upgrade a read lock to a write lock. On success the
    /// transport's [`Transport::lock_state`] becomes
    /// `Some(LockState::Write)`. On failure (another reader holds the
    /// file) the read lock is preserved and the method returns
    /// `Ok(false)`. Returns `NotLocked` if no lock is held, or
    /// `AlreadyLocked` if a write lock is already held.
    ///
    /// The default implementation refuses the upgrade — backends that
    /// can't atomically switch lock modes (e.g. in-memory tests, the
    /// Python-file adapter) keep the read lock.
    fn upgrade_to_write_lock(&mut self) -> Result<bool, TransportError> {
        match self.lock_state() {
            None => Err(TransportError::NotLocked),
            Some(LockState::Write) => Err(TransportError::AlreadyLocked),
            Some(LockState::Read) => Ok(false),
        }
    }

    /// Inverse of [`Self::upgrade_to_write_lock`]. Downgrade a write
    /// lock previously obtained via `upgrade_to_write_lock` back to a
    /// read lock. Returns `NotLocked` if no lock is held, or a
    /// generic error if the current lock is not a write lock.
    ///
    /// The default implementation is a no-op when the lock is already
    /// a read lock and errors otherwise.
    fn downgrade_to_read_lock(&mut self) -> Result<(), TransportError> {
        match self.lock_state() {
            None => Err(TransportError::NotLocked),
            Some(LockState::Read) => Ok(()),
            Some(LockState::Write) => Err(TransportError::Other(
                "downgrade_to_read_lock not supported by this transport".into(),
            )),
        }
    }

    /// Force the current contents to durable storage. Implementations
    /// that have no meaningful fsync (in-memory tests, mocked
    /// backends) are free to make this a no-op; real filesystem
    /// implementations should call `fdatasync(2)` or the platform
    /// equivalent.
    fn fdatasync(&mut self) -> Result<(), TransportError>;

    /// Return the stat info for an absolute path in the working-tree
    /// filesystem that the dirstate is tracking (not the dirstate
    /// file itself).  `NoSuchFile` when the path is gone from disk.
    /// Required by `DirState::update_entry` / `process_entry`, which
    /// otherwise would couple the pure crate to `std::fs`.
    fn lstat(&self, abspath: &[u8]) -> Result<StatInfo, TransportError>;

    /// Return the target of the symlink at `abspath`.  `NoSuchFile`
    /// when the path is gone; a generic error when the path is not a
    /// symlink.
    fn read_link(&self, abspath: &[u8]) -> Result<Vec<u8>, TransportError>;

    /// Whether the directory at `abspath` is a nested tree reference
    /// (i.e. contains a `.bzr/` control directory).  Mirrors the
    /// per-format `_directory_is_tree_reference` hook on breezy's
    /// `WorkingTree`: the file format decides whether tree references
    /// can exist at all, and a concrete directory qualifies iff it
    /// carries its own `.bzr/`.  Consumers use this during
    /// `iter_changes` to flip the on-disk `directory` kind to
    /// `tree-reference` before handing the entry to
    /// [`DirState::process_entry`].
    ///
    /// Formats that don't support tree references should implement
    /// this as an unconditional `Ok(false)`.
    fn is_tree_reference_dir(&self, abspath: &[u8]) -> Result<bool, TransportError>;

    /// List the immediate children of directory `abspath`.  Used by
    /// the pure-crate `iter_changes` walker.  Returns a vector of
    /// per-child entries; the implementation does not guarantee any
    /// particular order — the walker sorts.
    ///
    /// Each entry carries the child's utf8 basename, its kind
    /// (`"file"`, `"directory"`, `"symlink"`, or `"tree-reference"`),
    /// its [`StatInfo`] (from an `lstat`), and the absolute path of
    /// the child on disk.  The walker re-uses the stat to avoid a
    /// second syscall inside `process_entry`.
    ///
    /// `NoSuchFile` when `abspath` does not exist or is not a
    /// directory.
    fn list_dir(&self, abspath: &[u8]) -> Result<Vec<DirEntryInfo>, TransportError>;
}