GitoxideLabs/gitoxide · warning

we have read non-zero bytes before

Error message

we have read non-zero bytes before

What it means

This is a Rust `expect()` panic in the `Reverse` reflog iterator's `next()` (gix-ref/store/file/log/iter.rs). After seeking backwards and calling `read_exact` for `n` bytes, the code calls `buf.last()`; the `expect` asserts `n` is non-zero because `n == 0` was returned early above. It can only panic if `n` was >0 but `read_exact` left the buffer logically empty relative to expectations — i.e. an arithmetic/seek invariant broke, typically from a corrupted reflog file size or a race where the file shrank between size query and read.

Solutions

  1. Do not iterate reflogs concurrently with operations that rewrite them; take a stable snapshot (copy the log file) first.
  2. Check the reflog file for truncation/corruption and repair it (e.g. restore via `git reflog` on a healthy clone, or delete the corrupt log).
  3. Retry the iteration after the concurrent writer finishes (reflog rewrite is transient).
  4. If it happens on static files, report upstream with the reflog file — internal invariant violation.

Example fix

// before
let iter = reverse(file); // file concurrently truncated mid-iteration -> panic
// after
let snapshot = std::fs::read(log_path)?; // stable copy
let iter = reverse(std::io::Cursor::new(snapshot));
Defensive patterns

Strategy: retry

Validate before calling

// verify the reflog file is stable before reverse iteration
let meta1 = std::fs::metadata(log_path)?;
std::thread::sleep(std::time::Duration::from_millis(50));
let meta2 = std::fs::metadata(log_path)?;
if meta1.len() != meta2.len() { /* file is being rewritten; wait */ }

Try / catch

// snapshot first, then iterate; on failure retry once with a fresh snapshot
match iterate_reverse(snapshot()) {
    Ok(lines) => lines,
    Err(_) => iterate_reverse(snapshot())?,
}

Prevention

When it happens

Trigger: Iterating a reflog in reverse (`gix_ref::store::file::log::iter::reverse`) while the reflog file is truncated/concurrently modified (e.g. `git gc`, ref deletion) between the stat that computed `pos` and the `read_exact`; or a reflog whose reported size is inconsistent.

Common situations: Reading reflogs of repositories being actively rewritten by other processes; file systems with unreliable size reporting; corrupted `.git/logs/**` files.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08). Data as JSON: /api/errors/305d85f8bb94ec28. Report an issue: GitHub.

Appendix: source

Thrown at gix-ref/src/store/file/log/iter.rs:183

    fn next(&mut self) -> Option<Self::Item> {
        match (self.last_nl_pos.take(), self.read_and_pos.take()) {
            // Initial state - load first data block
            (None, Some((mut read, pos))) => {
                let npos = pos.saturating_sub(self.buf.len() as u64);
                if let Err(err) = read.seek(std::io::SeekFrom::Start(npos)) {
                    return Some(Err(err.into()));
                }

                let n = (pos - npos) as usize;
                if n == 0 {
                    return None;
                }
                let buf = &mut self.buf[..n];
                if let Err(err) = read.read_exact(buf) {
                    return Some(Err(err.into()));
                }

                let last_byte = *buf.last().expect("we have read non-zero bytes before");
                self.last_nl_pos = Some(if last_byte != b'\n' { buf.len() } else { buf.len() - 1 });
                self.read_and_pos = Some((read, npos));
                self.next()
            }
            // Has data block and can extract lines from it, load new blocks as needed
            (Some(end), Some(read_and_pos)) => match self.buf[..end].rfind_byte(b'\n') {
                Some(start) => {
                    self.read_and_pos = Some(read_and_pos);
                    self.last_nl_pos = Some(start);
                    let buf = &self.buf[start + 1..end];
                    let res = Some(
                        log::LineRef::from_bytes(buf)
                            .map_err(|err| {
                                reverse::Error::Decode(decode::Error::new(err, LineNumber::FromEnd(self.count)))
                            })
                            .map(Into::into),
                    );
                    self.count += 1;

View on GitHub (pinned to e73179060b)