Skip to content
Snippets Groups Projects
Commit 5ac1a849 authored by Georges Racinet's avatar Georges Racinet
Browse files

RHGitaly: gitlab::revision, only with `map_full_ref()` for now.

Of course it will grow the equivalenbt of Python's `gitlab_revision_changeset`,
which is one of the most common helper functions in HGitaly, making
the `FindCommit` gRPC method almost trivial.
parent 725e8c52
No related branches found
No related tags found
2 merge requests!169Protocol v15.9 and stable branch merge",!166RHGitaly: reading GitLab ref state files, implementing RefService.RefExists
......@@ -3,4 +3,5 @@
// This software may be used and distributed according to the terms of the
// GNU General Public License version 2 or any later version.
// SPDX-License-Identifier: GPL-2.0-or-later
pub mod revision;
pub mod state;
// Copyright 2023 Georges Racinet <georges.racinet@octobus.net>
//
// This software may be used and distributed according to the terms of the
// GNU General Public License version 2 or any later version.
// SPDX-License-Identifier: GPL-2.0-or-later
//! High level support for GitLab revisions.
//!
//! This matches roughly the `hgitaly.revision` Python module.
//!
//! There is no precise definition of revision, it is merely what Gitaly clients
//! can pass legitimately in the various `revision` fields of requests. Often, it is just
//! the subset of of `git log` would accept and treat as a single revision, e.g., by default
//! `main` would be tried first as a tag name, then as a branch name.
//!
//! Refs given in their full path from, e.g., as `refs/heads/tags/v1.2` are valid revisions, hence
//! the hlpers to handle them are in particular provided by this module.
use std::path::Path;
use super::state::{
has_keep_around, map_lookup_typed_ref, stream_gitlab_branches, stream_gitlab_special_refs,
stream_gitlab_tags, StateFileError, TypedRef,
};
#[derive(Debug, derive_more::From)]
pub enum RefError {
/// Returned to indicate that a full ref path does not start with `refs/`
NotAFullRef,
/// Returned to indicate that a given full path starts like a typed ref, yet ends
/// without the "name" part, as does e.g., `refs/heads`
MissingRefName,
/// Returned when a ref is not found for the given repository.
NotFound,
#[from]
/// Errors reading and parsing GitLab state files
GitLabStateFileError(StateFileError),
}
/// From GitLab state files in `store_vfs`, resolve a ref and convert using the given closures
///
/// Parameter `ref_path` must be the full ref path, i.e., starting with `refs/`
/// Parameter `map_ka` is used if the ref is found to be a keep-around
/// Parameter `map_ref` is used in other cases
pub async fn map_full_ref<R, MK, MR>(
store_vfs: &Path,
ref_path: &[u8],
map_ref: MR,
map_ka: MK,
) -> Result<R, RefError>
where
MK: FnOnce(&[u8]) -> R,
MR: FnOnce(TypedRef) -> R + Copy,
{
let mut split = ref_path.splitn(3, |c| *c == b'/');
if split.next() != Some(b"refs") {
return Err(RefError::NotAFullRef);
}
let ref_type = split
.next() // `None` happends when `ref_path` is just `"refs"`
.ok_or(RefError::NotAFullRef)?;
if ref_type == b"keep-around" {
let ka = split.next().ok_or(RefError::NotAFullRef)?;
if has_keep_around(store_vfs, ka).await? {
return Ok(map_ka(ka));
}
}
let (stream, name_is_next) = match ref_type {
b"heads" => (stream_gitlab_branches(store_vfs).await?, true),
b"tags" => (stream_gitlab_tags(store_vfs).await?, true),
_ => (stream_gitlab_special_refs(store_vfs).await?, false),
};
let wanted = if name_is_next {
split.next().ok_or(RefError::MissingRefName)?
} else {
&ref_path[5..]
};
map_lookup_typed_ref(stream, wanted, map_ref)
.await?
.ok_or(RefError::NotFound)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
async fn write_test_file(
// TODO deduplicate with gitlab::state::tests
store_vfs: &Path,
name: &str,
content: &[u8],
) -> Result<(), StateFileError> {
Ok(File::create(store_vfs.join(name))
.await?
.write_all(content)
.await?)
}
/// A simple wrapper that returns the hash for a full ref name
///
/// One of the goals is the error treatment to map [`RefError::NotFound`] to `None` and
/// hence allow the testing code to use the question mark operator
async fn full_ref_hash(store_vfs: &Path, full_ref: &[u8]) -> Result<Option<Vec<u8>>, RefError> {
match map_full_ref(store_vfs, full_ref, |tr| tr.target_sha, |ka| ka.to_vec()).await {
Err(RefError::NotFound) => Ok(None),
Ok(res) => Ok(Some(res)),
Err(e) => Err(e),
}
}
#[tokio::test]
async fn test_map_full_ref() -> Result<(), RefError> {
let tmp_dir = tempdir().unwrap(); // not async, but it doesn't matter much in tests
let store_vfs = tmp_dir.path();
write_test_file(
store_vfs,
"gitlab.branches",
b"001\n437bd1bf68ac037eb6956490444e2d7f9a5655c9 branch/default\n",
)
.await?;
write_test_file(
store_vfs,
"gitlab.tags",
b"001\nb50274b9b1c58fc97c45357a7c901d39bafc264d v6\n",
)
.await?;
write_test_file(
store_vfs,
"gitlab.special-refs",
b"001\nf61a76dc97fb1a58f30e1b74245b957bb8c8d609 merge-requests/35/train\n",
)
.await?;
write_test_file(
store_vfs,
"gitlab.keep-arounds",
b"001\n9787c1a7b9390e3f09babce1506254eb698dfba3\n",
)
.await?;
assert_eq!(
full_ref_hash(store_vfs, b"refs/heads/branch/default").await?,
Some(b"437bd1bf68ac037eb6956490444e2d7f9a5655c9".to_vec())
);
assert_eq!(
full_ref_hash(store_vfs, b"refs/tags/v6").await?,
Some(b"b50274b9b1c58fc97c45357a7c901d39bafc264d".to_vec())
);
assert_eq!(
full_ref_hash(store_vfs, b"refs/merge-requests/35/train").await?,
Some(b"f61a76dc97fb1a58f30e1b74245b957bb8c8d609".to_vec())
);
assert_eq!(
full_ref_hash(
store_vfs,
b"refs/keep-around/9787c1a7b9390e3f09babce1506254eb698dfba3"
)
.await?,
Some(b"9787c1a7b9390e3f09babce1506254eb698dfba3".to_vec())
);
assert_eq!(full_ref_hash(store_vfs, b"refs/heads/other").await?, None);
assert_eq!(full_ref_hash(store_vfs, b"refs/pipelines/54").await?, None);
assert_eq!(full_ref_hash(store_vfs, b"refs/tags/v8").await?, None);
assert_eq!(
full_ref_hash(
store_vfs,
b"refs/keep-around/b50274b9b1c58fc97c45357a7c901d39bafc264d"
)
.await?,
None
);
// input errors
match full_ref_hash(store_vfs, b"REF/foo").await.unwrap_err() {
RefError::NotAFullRef => Ok(()),
e => Err(e),
}?;
match full_ref_hash(store_vfs, b"refs").await.unwrap_err() {
RefError::NotAFullRef => Ok(()),
e => Err(e),
}?;
match full_ref_hash(store_vfs, b"refs/tags").await.unwrap_err() {
RefError::MissingRefName => Ok(()),
e => Err(e),
}?;
Ok(())
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment