rust: migrate libutil/git.cc

Change-Id: Ic3932fdf3afd3dc09019623516597f1c648f7fc4
This commit is contained in:
rootile
2026-07-08 20:03:10 +00:00
committed by eldritch horrors
parent dd8f1843cc
commit 41bad096e3
17 changed files with 201 additions and 124 deletions
Generated
+32 -2
View File
@@ -189,7 +189,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e82d74e6c83060ec269fe9e0d408d6de4a1645d525f9a0bbbb841ba4efd91ac"
dependencies = [
"hashbrown 0.15.5",
"regex-automata",
"regex-automata 0.3.9",
"serde",
"stacker",
"unicode-ident",
@@ -451,6 +451,7 @@ name = "lix"
version = "0.0.0"
dependencies = [
"lix-doc",
"regex",
"rustyline",
"rustyline-derive",
"zngur",
@@ -604,6 +605,18 @@ dependencies = [
"nibble_vec",
]
[[package]]
name = "regex"
version = "1.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.14",
"regex-syntax 0.8.11",
]
[[package]]
name = "regex-automata"
version = "0.3.9"
@@ -612,7 +625,18 @@ checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
"regex-syntax 0.7.5",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.11",
]
[[package]]
@@ -621,6 +645,12 @@ version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "rnix"
version = "0.12.0"
+1
View File
@@ -11,6 +11,7 @@ edition = "2021"
[workspace.dependencies]
clap = "4"
regex = "1.12.4"
rustyline = "18"
rustyline-derive = "0.12"
syn = "2.0"
+10 -11
View File
@@ -16,9 +16,10 @@
#include "lix/libutil/url-parts.hh"
#include "lix/libstore/pathlocks.hh"
#include "lix/libutil/users.hh"
#include "lix/libutil/git.hh"
#include "lix/libutil/logging.hh"
#include "lix/libutil/finally.hh"
#include "lix/lix-rs/main.gen.hh"
#include "lix/lix-rs/utils.hh"
#include "lix/libfetchers/fetch-settings.hh"
@@ -81,17 +82,15 @@ try {
));
std::string_view line = output;
line = line.substr(0, line.find("\n"));
if (const auto parseResult = git::parseLsRemoteLine(line)) {
switch (parseResult->kind) {
case git::LsRemoteRefLine::Kind::Symbolic:
debug("resolved HEAD ref '%s' for repo '%s'", parseResult->target, path);
break;
case git::LsRemoteRefLine::Kind::Object:
debug("resolved HEAD rev '%s' for repo '%s'", parseResult->target, path);
break;
auto line_rs = rust::to_string(line.substr(0, line.find("\n")));
if (const auto parseResult = to_std(rust::lix::fetchers::git::parse_ls_remote_line(line_rs.as_str()))) {
auto target = to_std_string(parseResult->target.as_str());
if (parseResult->kind.matches_Object()) {
debug("resolved HEAD rev '%s' for repo '%s'", target, path);
} else {
debug("resolved HEAD ref '%s' for repo '%s'", target, path);
}
co_return parseResult->target;
co_return to_std_string(parseResult->target.as_str());
}
co_return std::nullopt;
} catch (ExecError &) {
+10 -7
View File
@@ -7,10 +7,11 @@
#include "lix/libutil/result.hh"
#include "lix/libutil/types.hh"
#include "lix/libutil/url-parts.hh"
#include "lix/libutil/git.hh"
#include "lix/libutil/json.hh"
#include "lix/libfetchers/fetchers.hh"
#include "lix/libfetchers/fetch-settings.hh"
#include "lix/lix-rs/main.gen.hh"
#include "lix/lix-rs/utils.hh"
#include <optional>
#include <fstream>
@@ -486,15 +487,15 @@ struct SourceHutInputScheme : GitArchiveInputScheme
std::string line;
getline(is, line);
auto remoteLine = git::parseLsRemoteLine(line);
auto remoteLine =
to_std(rust::lix::fetchers::git::parse_ls_remote_line(rust::to_string(line).as_str()));
if (!remoteLine) {
throw BadURL("in '%d', couldn't resolve HEAD ref '%d'", input.to_string(), ref);
}
refUri = remoteLine->target;
refUri = to_std_string(remoteLine->target.as_str());
} else {
refUri = fmt("refs/(heads|tags)/%s", ref);
}
std::regex refRegex = regex::parse(refUri);
auto file = store->toRealPath(
TRY_AWAIT(downloadFile(store, fmt("%s/info/refs", base_url), "source", false, headers))
@@ -505,9 +506,11 @@ struct SourceHutInputScheme : GitArchiveInputScheme
std::string line;
std::optional<std::string> id;
while(!id && getline(is, line)) {
auto parsedLine = git::parseLsRemoteLine(line);
if (parsedLine && parsedLine->reference && std::regex_match(*parsedLine->reference, refRegex))
id = parsedLine->target;
auto parsedLine =
to_std(rust::lix::fetchers::git::parse_ls_remote_line(rust::to_string(line).as_str()));
if (parsedLine && (*parsedLine).matches_ref_uri(rust::to_string(refUri).as_str())) {
id = to_std_string(parsedLine->target.as_str());
}
}
if(!id)
-26
View File
@@ -1,26 +0,0 @@
#include "lix/libutil/git.hh"
#include "regex.hh"
#include <regex>
namespace nix {
namespace git {
std::optional<LsRemoteRefLine> parseLsRemoteLine(std::string_view line)
{
const static std::regex line_regex = regex::parse("^(ref: *)?([^\\s]+)(?:\\t+(.*))?$");
std::match_results<std::string_view::const_iterator> match;
if (!std::regex_match(line.cbegin(), line.cend(), match, line_regex))
return std::nullopt;
return LsRemoteRefLine {
.kind = match[1].length() == 0
? LsRemoteRefLine::Kind::Object
: LsRemoteRefLine::Kind::Symbolic,
.target = match[2],
.reference = match[3].length() == 0 ? std::nullopt : std::optional<std::string>{ match[3] }
};
}
}
}
-43
View File
@@ -1,43 +0,0 @@
#pragma once
///@file
#include <string>
#include <string_view>
#include <optional>
namespace nix {
namespace git {
/**
* A line from the output of `git ls-remote --symref`.
*
* These can be of two kinds:
*
* - Symbolic references of the form
*
* ref: {target} {reference}
*
* where {target} is itself a reference and {reference} is optional
*
* - Object references of the form
*
* {target} {reference}
*
* where {target} is a commit id and {reference} is mandatory
*/
struct LsRemoteRefLine {
enum struct Kind {
Symbolic,
Object
};
Kind kind;
std::string target;
std::optional<std::string> reference;
};
std::optional<LsRemoteRefLine> parseLsRemoteLine(std::string_view line);
}
}
-2
View File
@@ -22,7 +22,6 @@ liblix_sources += files(
'file-descriptor.cc',
'file-system.cc',
'fmt.cc',
'git.cc',
'hash.cc',
'hilite.cc',
'io-buffer.cc',
@@ -103,7 +102,6 @@ libutil_headers = files(
'finally.hh',
'fmt.hh',
'generator.hh',
'git.hh',
'hash.hh',
'hilite.hh',
'input-accessor.hh',
+1
View File
@@ -12,6 +12,7 @@ default = []
[dependencies]
lix-doc.path = "../lix-doc"
regex.workspace=true
rustyline.workspace = true
rustyline-derive.workspace = true
+2
View File
@@ -1,6 +1,8 @@
lix_rs_files += files(
'Cargo.toml',
# keep-sorted start
'src/fetchers/git.rs',
'src/fetchers/mod.rs',
'src/ffi_test.rs',
'src/lib.rs',
'src/repl.rs',
+29
View File
@@ -0,0 +1,29 @@
use ::std::string::String as String;
mod crate::fetchers::git {
type Kind {
#layout_conservative(size=16, align=8);
constructor Symbolic;
constructor Object;
}
type LsRemoteRefLine {
#layout_conservative(size=56, align=8);
field kind (offset=auto, type=Kind);
field target (offset=auto, type=String);
field reference (offset=auto, type=::std::option::Option<String>);
fn matches_ref_uri(&self, &str) -> bool;
}
fn parse_ls_remote_line(&str) -> ::std::option::Option<LsRemoteRefLine>;
type ::std::option::Option<LsRemoteRefLine> {
#layout_conservative(size=56, align=8);
constructor None;
constructor Some(LsRemoteRefLine);
fn unwrap(self) -> LsRemoteRefLine;
}
}
+104
View File
@@ -0,0 +1,104 @@
use std::sync::LazyLock;
use regex::Regex;
#[derive(Debug, Eq, PartialEq)]
pub enum Kind {
Symbolic,
Object,
}
///
/// A line from the output of `git ls-remote --symref`
///
/// These can be of two kinds:
///
/// - Symbolic references of the form
/// ref: {target} {references}
/// where {target} is itself a reference and {reference} is optional
///
/// - Object reference of the form
/// {target} {reference}
/// where {target} is a commit id and {reference} is mandatory
pub struct LsRemoteRefLine {
pub kind: Kind,
pub target: String,
pub reference: Option<String>,
}
impl LsRemoteRefLine {
pub fn matches_ref_uri(&self, uri: &str) -> bool {
let uri_regex = Regex::new(uri).unwrap();
match &self.reference {
None => false,
Some(r) => uri_regex.is_match(r.as_str()),
}
}
}
static LS_REMOTE_REF_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^(ref: *)?([^\s]+)(?:\t+(.*))?$").unwrap());
pub fn parse_ls_remote_line(line: &str) -> Option<LsRemoteRefLine> {
LS_REMOTE_REF_REGEX.captures(line).map(|m| {
let kind = m
.get(1)
.filter(|ma| !ma.is_empty())
.map_or(Kind::Object, |_| Kind::Symbolic);
let re = m.get(3).map(|ma| ma.as_str()).map(|ma| {
if ma.is_empty() {
None
} else {
Some(ma.to_string())
}
});
LsRemoteRefLine {
kind: kind,
target: m
.get(2)
.expect("regex for matching remotes has two capture groups defined, so this should not error")
.as_str()
.to_string(),
reference: re.flatten(),
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_symref_line_with_reference() {
let line = "ref: refs/head/main\tHEAD";
let res = parse_ls_remote_line(line);
assert!(res.is_some());
let res = res.unwrap();
assert_eq!(res.kind, Kind::Symbolic);
assert_eq!(res.target, "refs/head/main");
assert_eq!(res.reference, Some("HEAD".to_string()));
}
#[test]
fn parse_symref_line_with_no_reference() {
let line = "ref: refs/head/main";
let res = parse_ls_remote_line(line);
assert!(res.is_some());
let res = res.unwrap();
assert_eq!(res.kind, Kind::Symbolic);
assert_eq!(res.target, "refs/head/main");
assert_eq!(res.reference, None);
}
#[test]
fn parse_object_ref_line() {
let line = "abc123 refs/head/main";
let res = parse_ls_remote_line(line);
assert!(res.is_some());
let res = res.unwrap();
assert_eq!(res.kind, Kind::Object);
assert_eq!(res.target, "abc123");
assert_eq!(res.reference, Some("refs/head/main".to_string()));
}
}
+1
View File
@@ -0,0 +1 @@
pub mod git;
+1
View File
@@ -145,5 +145,6 @@ pub(crate) mod log {
}
}
pub mod fetchers;
mod ffi_test;
mod repl;
+1
View File
@@ -1,5 +1,6 @@
merge "./ffi_test.zng";
merge "./repl.zng";
merge "./fetchers.zng";
mod crate::ffi {
use ::std::result::Result as Result;
+9
View File
@@ -35,6 +35,15 @@ mod ::std::option {
fn unwrap(self) -> u64;
}
type Option<String> {
#layout_conservative(size=24, align=8);
constructor Some(String);
constructor None;
fn unwrap(self) -> String;
}
}
mod ::std::result {
-32
View File
@@ -1,32 +0,0 @@
#include "lix/libutil/git.hh"
#include <gtest/gtest.h>
namespace nix {
TEST(GitLsRemote, parseSymrefLineWithReference) {
auto line = "ref: refs/head/main HEAD";
auto res = git::parseLsRemoteLine(line);
ASSERT_TRUE(res.has_value());
ASSERT_EQ(res->kind, git::LsRemoteRefLine::Kind::Symbolic);
ASSERT_EQ(res->target, "refs/head/main");
ASSERT_EQ(res->reference, "HEAD");
}
TEST(GitLsRemote, parseSymrefLineWithNoReference) {
auto line = "ref: refs/head/main";
auto res = git::parseLsRemoteLine(line);
ASSERT_TRUE(res.has_value());
ASSERT_EQ(res->kind, git::LsRemoteRefLine::Kind::Symbolic);
ASSERT_EQ(res->target, "refs/head/main");
ASSERT_EQ(res->reference, std::nullopt);
}
TEST(GitLsRemote, parseObjectRefLine) {
auto line = "abc123 refs/head/main";
auto res = git::parseLsRemoteLine(line);
ASSERT_TRUE(res.has_value());
ASSERT_EQ(res->kind, git::LsRemoteRefLine::Kind::Object);
ASSERT_EQ(res->target, "abc123");
ASSERT_EQ(res->reference, "refs/head/main");
}
}
-1
View File
@@ -55,7 +55,6 @@ libutil_tests_sources = files(
'libutil/escape-string.cc',
'libutil/fmt.cc',
'libutil/generator.cc',
'libutil/git.cc',
'libutil/hash.cc',
'libutil/hilite.cc',
'libutil/io-buffer.cc',