# HG changeset patch
# User Georges Racinet <georges.racinet@octobus.net>
# Date 1686781350 -7200
#      Thu Jun 15 00:22:30 2023 +0200
# Branch stable
# Node ID 725e8c524ffaa9ce06e4c8d8a10c86787b929352
# Parent  bcab70280a335c0e4132a155107aad7229486efa
RHGitaly: reading keep-arounds state file

This time, because there is no arbitrary name to consider, hence
no bytes string, Tokio's `LinesStream` is perfectly suitable for
our needs.

diff --git a/rust/rhgitaly/src/gitlab/state.rs b/rust/rhgitaly/src/gitlab/state.rs
--- a/rust/rhgitaly/src/gitlab/state.rs
+++ b/rust/rhgitaly/src/gitlab/state.rs
@@ -19,7 +19,11 @@
 //! As of this writing, the GitLab state is made of
 //!
 //! - refs, believed by other components to actually be Git refs. These are kept in several
-//!   files, due to vastly different read/write frequencies.
+//!   files, due to vastly different read/write frequencies. Among them are typed refs (branches,
+//!   tags etc), and keep-arounds (just hashes). These two cases are represented with different
+//!   Rust types in this implementation, whereas the Python reference would consider them all to be
+//!   typed refs with a different serialization format for keep-arounds (name would just be
+//!   repeating the hash, which is a waste).
 //! - default branch, as ultimately defined by end users
 //!
 //! For a full documentation of the concepts, related file formats and atomicity properties, see the
@@ -32,7 +36,8 @@
 use std::io;
 use std::path::Path;
 use tokio::fs::File;
-use tokio::io::{AsyncBufRead, AsyncReadExt, BufReader};
+use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt, BufReader};
+use tokio_stream::wrappers::LinesStream;
 use tokio_stream::StreamExt;
 use tokio_util::codec::{Decoder, FramedRead};
 
@@ -95,6 +100,7 @@
 
 /// Convenience alias for actual type returned by streaming functions
 pub type TypedRefsFileStream = Option<FramedRead<BufReader<File>, TypedRefDecoder>>;
+pub type KeepAroundsFileStream = Option<LinesStream<BufReader<File>>>;
 
 type TypedRefsFileStreamResult = Result<TypedRefsFileStream, StateFileError>;
 
@@ -223,6 +229,47 @@
     }
 }
 
+async fn stream_keep_arounds<R: AsyncBufRead + Unpin>(
+    mut buf: R,
+) -> Result<LinesStream<R>, StateFileError> {
+    check_version_preamble(&mut buf).await?;
+    Ok(LinesStream::new(buf.lines()))
+}
+
+/// Open the keep-arounds state file and stream its content as a [`LinesStream`]
+///
+/// Returns `None` if the state file does not exist.
+/// In particular, iterating on the stream yields [`String`] objects, not [`Vec<u8>`].
+pub async fn stream_keep_arounds_file(
+    store_vfs: &Path,
+) -> Result<KeepAroundsFileStream, StateFileError> {
+    Ok(
+        match io_error_not_found_as_none(File::open(store_vfs.join("gitlab.keep-arounds")).await)? {
+            None => None,
+            Some(f) => Some(stream_keep_arounds(BufReader::new(f)).await?),
+        },
+    )
+}
+
+/// Tell whether the repository has a keep-around, given in hexadecimal form
+///
+/// As other functions in this module, the repository is given just by its
+/// `store` subdirectory.
+pub async fn has_keep_around(store_vfs: &Path, ka: &[u8]) -> Result<bool, StateFileError> {
+    if let Some(mut stream) = stream_keep_arounds_file(store_vfs).await? {
+        // cannot use stream.any() and propagate error as it needs the closure to return `bool`,
+        // not some Result
+        while let Some(res) = stream.next().await {
+            if res?.as_bytes() == ka {
+                return Ok(true);
+            }
+        }
+        Ok(false)
+    } else {
+        Ok(false)
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -368,4 +415,45 @@
         assert_eq!(map_lookup_typed_ref(tags, b"v1.3", |tr| tr).await?, None);
         Ok(())
     }
+
+    #[tokio::test]
+    async fn test_stream_keep_arounds_file() -> Result<(), StateFileError> {
+        let tmp_dir = tempdir().unwrap(); // not async, but it doesn't matter much in tests
+        let store_vfs = tmp_dir.path();
+
+        assert!(stream_keep_arounds_file(store_vfs).await?.is_none());
+
+        write_test_file(
+            store_vfs,
+            "gitlab.keep-arounds",
+            b"001\n437bd1bf68ac037eb6956490444e2d7f9a5655c9",
+        )
+        .await?;
+        let stream = stream_keep_arounds_file(store_vfs).await?.unwrap();
+        let res: Vec<String> = stream.map(|r| r.unwrap()).collect().await;
+        assert_eq!(
+            res,
+            vec!["437bd1bf68ac037eb6956490444e2d7f9a5655c9".to_owned()]
+        );
+        Ok(())
+    }
+
+    #[tokio::test]
+    async fn test_has_keep_around() -> Result<(), StateFileError> {
+        let tmp_dir = tempdir().unwrap(); // not async, but it doesn't matter much in tests
+        let store_vfs = tmp_dir.path();
+
+        assert!(!has_keep_around(store_vfs, b"437bd1bf68ac037eb6956490444e2d7f9a5655c9").await?);
+
+        write_test_file(
+            store_vfs,
+            "gitlab.keep-arounds",
+            b"001\n437bd1bf68ac037eb6956490444e2d7f9a5655c9",
+        )
+        .await?;
+
+        assert!(has_keep_around(store_vfs, b"437bd1bf68ac037eb6956490444e2d7f9a5655c9").await?);
+        assert!(!has_keep_around(store_vfs, b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").await?);
+        Ok(())
+    }
 }