Commit

Author:

Hash:

Timestamp:

+190 -35 +/-8 browse

Kevin Schoon [me@kevinschoon.com]

510a5d3c70508fd11d8be2d514d519997dd2c00f

Sun, 05 Oct 2025 21:35:56 +0000 (1 month ago)

add support for cloning bare repositories
add support for cloning bare repositories

It is now possible to build bare repositories by cloning it's source into
the build working directory.
1diff --git a/ayllu-build/src/config.rs b/ayllu-build/src/config.rs
2index eff7909..8fbdccd 100644
3--- a/ayllu-build/src/config.rs
4+++ b/ayllu-build/src/config.rs
5 @@ -8,8 +8,8 @@ use ayllu_config::{data_dir, runtime_dir, Configurable, Database, Error, Reader}
6 pub struct Builder {
7 #[serde(default = "Builder::address_default")]
8 pub address: String,
9- #[serde(default = "Builder::log_path_default")]
10- pub log_path: PathBuf,
11+ #[serde(default = "Builder::work_dir_default")]
12+ pub work_dir: PathBuf,
13 }
14
15 impl Builder {
16 @@ -17,8 +17,8 @@ impl Builder {
17 runtime_dir().to_str().unwrap().to_string() + "/ayllu-build.sock"
18 }
19
20- fn log_path_default() -> PathBuf {
21- data_dir().join("worker_logs")
22+ fn work_dir_default() -> PathBuf {
23+ data_dir().join("work")
24 }
25 }
26
27 @@ -26,7 +26,7 @@ impl Default for Builder {
28 fn default() -> Self {
29 Self {
30 address: Builder::address_default(),
31- log_path: Builder::log_path_default(),
32+ work_dir: Builder::work_dir_default(),
33 }
34 }
35 }
36 diff --git a/ayllu-build/src/error.rs b/ayllu-build/src/error.rs
37index ba64405..e5a5551 100644
38--- a/ayllu-build/src/error.rs
39+++ b/ayllu-build/src/error.rs
40 @@ -6,6 +6,13 @@ pub enum Error {
41 path: PathBuf,
42 io_err: std::io::Error,
43 },
44+ CannotReadManifestFromRepository {
45+ path: PathBuf,
46+ repo_err: ayllu_git::Error,
47+ },
48+ RepositoryDoesNotContainManifest {
49+ path: PathBuf,
50+ },
51 InvalidManifest {
52 path: PathBuf,
53 json_err: serde_json::Error,
54 @@ -33,11 +40,18 @@ impl std::fmt::Display for Error {
55 write!(f, "Manifest is invalid: {path:?}, {json_err}")
56 }
57 Error::EmptyWorkflow { name } => write!(f, "Workflow {name} is empty"),
58- Error::CycleDetected => write!(f, "Cycle Detected!"), // FIXME: Where?
59+ Error::CycleDetected => write!(f, "Cycle Detected!"),
60 Error::DuplicateStepNames { name } => write!(f, "Duplicate step detected: {name}"),
61- Error::DuplicateWorkflows => write!(f, "Duplicate Workflows!"), // FIXME: Which?
62+ Error::DuplicateWorkflows => write!(f, "Duplicate Workflows!"),
63 Error::Io(error) => write!(f, "Io Error: {error}"),
64 Error::Db(error) => write!(f, "SQL Error: {error}"),
65+ Error::CannotReadManifestFromRepository { path, repo_err } => write!(
66+ f,
67+ "Cannot read manifest from repository {path:?}: {repo_err}"
68+ ),
69+ Error::RepositoryDoesNotContainManifest { path } => {
70+ write!(f, "Repository {path:?} does not contain a manifest")
71+ }
72 }
73 }
74 }
75 diff --git a/ayllu-build/src/evaluate.rs b/ayllu-build/src/evaluate.rs
76index 9c9e0eb..42ebd9c 100644
77--- a/ayllu-build/src/evaluate.rs
78+++ b/ayllu-build/src/evaluate.rs
79 @@ -1,6 +1,6 @@
80- use std::collections::HashMap;
81 use std::fs::metadata;
82 use std::path::PathBuf;
83+ use std::{collections::HashMap, path::Path};
84
85 use petgraph::{
86 algo::is_cyclic_directed,
87 @@ -9,11 +9,14 @@ use petgraph::{
88 visit::Topo,
89 };
90
91- use crate::models::{Manifest, Step};
92 use crate::{
93 error::Error,
94 executor::{Context, Executor, Local},
95 };
96+ use crate::{
97+ models::{Manifest, Step},
98+ DEFAULT_BUILD_FILE,
99+ };
100 use ayllu_database::{
101 build::{BuildExt, BuildTx},
102 Tx, Wrapper as Database,
103 @@ -23,7 +26,7 @@ use ayllu_api::build::Unit;
104
105 pub type BuildGraph = Graph<Unit, u8>;
106
107- #[derive(Clone)]
108+ #[derive(Clone, Debug)]
109 #[allow(dead_code)]
110 pub enum Source {
111 // already loaded manifest for testing
112 @@ -34,13 +37,30 @@ pub enum Source {
113 Clone((String, String)),
114 }
115
116+ impl Source {
117+ pub fn url(&self) -> String {
118+ match self {
119+ Source::Manifest(_) => todo!(),
120+ Source::Path(path_buf) => {
121+ if path_buf.ends_with(DEFAULT_BUILD_FILE) {
122+ let path = path_buf.parent().unwrap();
123+ path.to_string_lossy().to_string()
124+ } else {
125+ path_buf.to_string_lossy().to_string()
126+ }
127+ }
128+ Source::Clone(_) => todo!(),
129+ }
130+ }
131+ }
132+
133 #[allow(dead_code)]
134 pub(crate) struct Runtime {
135 // parallelism: bool,
136 pub tee_output: bool,
137 pub db: Database,
138 pub source: Source,
139- pub log_dir: PathBuf,
140+ pub work_dir: PathBuf,
141 }
142
143 impl Runtime {
144 @@ -53,7 +73,40 @@ impl Runtime {
145 if info.is_file() {
146 Manifest::from_file(path)
147 } else {
148- Manifest::from_dir(path)
149+ // detect if the path is a git dir and read from it's HEAD instead.
150+ // TODO: We can't just consider it's HEAD but need to accept flags
151+ // for a branch and or commit hash.
152+ if ayllu_git::git_dir(path.as_path())? {
153+ let repository =
154+ ayllu_git::Wrapper::new(path.as_path()).map_err(|e| {
155+ Error::CannotReadManifestFromRepository {
156+ path: path.to_path_buf(),
157+ repo_err: e,
158+ }
159+ })?;
160+ let manifest_str = repository
161+ .read_string(Path::new(DEFAULT_BUILD_FILE), None)
162+ .map_err(|e| Error::CannotReadManifestFromRepository {
163+ path: path.to_path_buf(),
164+ repo_err: e,
165+ })?;
166+ if let Some(manifest_str) = manifest_str {
167+ let manifest: Manifest =
168+ serde_json::de::from_str(&manifest_str).map_err(|e| {
169+ Error::InvalidManifest {
170+ path: path.to_path_buf(),
171+ json_err: e,
172+ }
173+ })?;
174+ Ok(manifest)
175+ } else {
176+ Err(Error::RepositoryDoesNotContainManifest {
177+ path: path.to_path_buf(),
178+ })
179+ }
180+ } else {
181+ Manifest::from_dir(path)
182+ }
183 }
184 }
185 Err(err) => Err(Error::CannotReadManifest {
186 @@ -61,7 +114,6 @@ impl Runtime {
187 io_err: err,
188 }),
189 }
190- // let manifest = Manifest::from_path(path)?;
191 }
192 Source::Clone((_url, _git_hash)) => todo!(),
193 }
194 @@ -74,7 +126,11 @@ impl Runtime {
195 Source::Path(path_buf) => std::fs::canonicalize(path_buf).unwrap(),
196 Source::Clone(_) => todo!(),
197 };
198- let repo_path = manifest_path.parent().unwrap();
199+ let repo_path = if manifest_path.is_dir() {
200+ manifest_path.as_path()
201+ } else {
202+ manifest_path.parent().unwrap()
203+ };
204 let (collection, name) = ayllu_git::collection_and_name(repo_path);
205 let repository = ayllu_git::Wrapper::new(repo_path)?;
206 let latest_hash = repository.latest_hash()?.unwrap();
207 @@ -214,7 +270,7 @@ impl Runtime {
208 tracing::info!("starting step: {}", next_step.name);
209 self.db.update_step_start(next_step.id).await?;
210 let executor = Local {
211- temp_dir: self.log_dir.clone(),
212+ temp_dir: self.work_dir.clone(), // FIXME: Eliminate file based logging all together
213 tee_output: self.tee_output,
214 };
215
216 @@ -222,6 +278,8 @@ impl Runtime {
217 let ctx = Context {
218 manifest_id,
219 workflow_id: current_workflow.unwrap(),
220+ step_id: *next_step_id,
221+ repo_url: self.source.url(),
222 ..Default::default()
223 };
224
225 diff --git a/ayllu-build/src/executor.rs b/ayllu-build/src/executor.rs
226index ff8c100..f5c3319 100644
227--- a/ayllu-build/src/executor.rs
228+++ b/ayllu-build/src/executor.rs
229 @@ -1,6 +1,6 @@
230 use std::collections::HashMap;
231 use std::env;
232- use std::fs::{create_dir_all, metadata, read_to_string, File};
233+ use std::fs::{read_to_string, File};
234 use std::mem::take;
235 use std::path::{Path, PathBuf};
236 use std::process::{Command, ExitStatus, Stdio};
237 @@ -40,6 +40,7 @@ impl Output {
238 pub struct Context {
239 pub manifest_id: i64,
240 pub workflow_id: i64,
241+ pub step_id: i64,
242 pub git_hash: String,
243 pub repo_url: String,
244 pub environment: HashMap<String, Option<String>>,
245 @@ -99,22 +100,38 @@ impl Executor for Local {
246 step: &Step,
247 context: Context,
248 ) -> Result<(String, String, ExitStatus), Error> {
249- let temp_dir = self
250+ let work_dir = self
251+ .temp_dir
252+ .as_path()
253+ .join(context.manifest_id.to_string());
254+ let src_dir = work_dir.join("src");
255+ std::fs::create_dir_all(&src_dir)?;
256+ let log_dir = work_dir.join("logs");
257+ std::fs::create_dir_all(&log_dir)?;
258+ if !ayllu_git::git_dir(&src_dir)? {
259+ tracing::info!("Cloning repository {} to {work_dir:?}", context.repo_url);
260+ ayllu_git::clone(&context.repo_url, src_dir.as_path(), None).unwrap();
261+ }
262+ let log_dir = self
263 .temp_dir
264 .as_path()
265 .join(context.manifest_id.to_string())
266- .join(context.workflow_id.to_string());
267+ .join(context.workflow_id.to_string())
268+ .join(context.step_id.to_string());
269+
270+ std::fs::create_dir_all(&log_dir)?;
271
272- match metadata(temp_dir.clone()) {
273- Ok(_) => {}
274- Err(_) => create_dir_all(temp_dir.as_path())?,
275- }
276 // TODO: add env filter to config
277 let mut filtered_env: HashMap<String, String> = env::vars()
278 .filter(|(k, _)| k == "TERM" || k == "TZ" || k == "LANG" || k == "PATH")
279 .collect();
280 // FIXME: If user specified None for an env it should override the defaults
281- filtered_env.extend(context.environment.iter().filter_map(|env| env.1.as_ref().map(|value| (env.0.clone(), value.clone()))));
282+ filtered_env.extend(
283+ context
284+ .environment
285+ .iter()
286+ .filter_map(|env| env.1.as_ref().map(|value| (env.0.clone(), value.clone()))),
287+ );
288 filtered_env.extend([
289 (String::from("AYLLU_GIT_HASH"), context.git_hash.clone()),
290 (String::from("AYLLU_REPO_URL"), context.repo_url.clone()),
291 @@ -126,11 +143,12 @@ impl Executor for Local {
292 .arg("-c")
293 .arg(step.input.clone())
294 .envs(filtered_env)
295+ .current_dir(&src_dir)
296 .stdout(Stdio::piped())
297 .stderr(Stdio::piped())
298 .spawn()?;
299
300- let stdout_log_path = temp_dir.clone().join("stdout.log");
301+ let stdout_log_path = log_dir.clone().join("stdout.log");
302 info!("writing stdout to: {:?}", stdout_log_path);
303 let proc_stdout = take(&mut proc.stdout).expect("cannot get stdout");
304 let tee_output = self.tee_output;
305 @@ -142,7 +160,7 @@ impl Executor for Local {
306 )
307 .unwrap();
308 });
309- let stderr_log_path = temp_dir.clone().join("stderr.log");
310+ let stderr_log_path = log_dir.clone().join("stderr.log");
311 info!("writing stderr to: {:?}", stderr_log_path);
312 let proc_stderr = take(&mut proc.stderr).expect("cannot get stderr");
313 let stderr_thd = thread::spawn(move || {
314 @@ -157,8 +175,8 @@ impl Executor for Local {
315 stdout_thd.join().unwrap();
316 stderr_thd.join().unwrap();
317
318- let stdout_str = read_to_string(Path::new(&temp_dir).join("stdout.log"))?;
319- let stderr_str = read_to_string(Path::new(&temp_dir).join("stderr.log"))?;
320+ let stdout_str = read_to_string(Path::new(&log_dir).join("stdout.log"))?;
321+ let stderr_str = read_to_string(Path::new(&log_dir).join("stderr.log"))?;
322
323 debug!("waiting for process to complete");
324 let status_code = proc.wait().expect("failed to wait on process");
325 diff --git a/ayllu-build/src/main.rs b/ayllu-build/src/main.rs
326index a8247ea..70d2486 100644
327--- a/ayllu-build/src/main.rs
328+++ b/ayllu-build/src/main.rs
329 @@ -43,7 +43,7 @@ enum Commands {
330 /// evaluate a local build script
331 Evaluate {
332 /// Path to your configuration file
333- #[arg(short, long, value_name = "FILE")]
334+ #[arg(value_name = "FILE")]
335 alternate_path: Option<PathBuf>,
336 /// "Tee" output from the executor to your local stdout/stderr
337 #[arg(short, long, action)]
338 @@ -75,13 +75,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
339 .log_queries(false) // FIXME
340 .build()
341 .await?;
342+ let source_path = alternate_path.unwrap_or(Path::new(DEFAULT_BUILD_FILE).to_path_buf());
343+ let source_path = std::fs::canonicalize(source_path)?;
344 let rt = Runtime {
345 db,
346- source: Source::Path(
347- alternate_path.unwrap_or(Path::new(DEFAULT_BUILD_FILE).to_path_buf()),
348- ),
349+ source: Source::Path(source_path),
350 tee_output,
351- log_dir: cfg.builder.log_path,
352+ work_dir: cfg.builder.work_dir,
353 };
354 rt.evaluate().await?;
355 Ok(())
356 diff --git a/ayllu-shell/src/ui.rs b/ayllu-shell/src/ui.rs
357index 9593096..7a9fa74 100644
358--- a/ayllu-shell/src/ui.rs
359+++ b/ayllu-shell/src/ui.rs
360 @@ -52,6 +52,8 @@ fn maybe_string(input: &str) -> Option<String> {
361
362 mod helpers {
363
364+ use std::process::ExitStatus;
365+
366 use super::*;
367
368 pub struct Repository<'a> {
369 @@ -120,6 +122,16 @@ mod helpers {
370 println!("Moved {src_path:?} --> {dst_path:?}");
371 Ok(())
372 }
373+
374+ pub fn build(_branch: &str, path: &Path) -> Result<ExitStatus, Error> {
375+ let path = std::fs::canonicalize(path)?;
376+ let path_str = path.to_string_lossy();
377+ let mut cmd = std::process::Command::new("ayllu-build")
378+ .args(["evaluate", &path_str])
379+ .spawn()?;
380+ let status = cmd.wait()?;
381+ Ok(status)
382+ }
383 }
384
385 mod menu {
386 @@ -150,6 +162,10 @@ mod menu {
387 collection: &'a Collection,
388 name: String,
389 },
390+ Build {
391+ collection: &'a Collection,
392+ name: String,
393+ },
394 /// Select a repoisitory
395 Repository {
396 collection: &'a Collection,
397 @@ -195,6 +211,12 @@ mod menu {
398 write!(f, "Move")
399 }
400 Item::Collection(collection) => write!(f, "{}", collection.name),
401+ Item::Build {
402+ collection: _,
403+ name: _,
404+ } => {
405+ write!(f, "Build")
406+ }
407 }
408 }
409 }
410 @@ -378,6 +400,10 @@ impl Prompt<'_> {
411 collection,
412 name: name.clone(),
413 },
414+ menu::Item::Build {
415+ collection,
416+ name: name.clone(),
417+ },
418 menu::Item::Edit {
419 collection,
420 name: name.clone(),
421 @@ -395,6 +421,29 @@ impl Prompt<'_> {
422 None => self.execute(previous, None),
423 }
424 }
425+ Some(menu::Item::Build { collection, name }) => {
426+ let repository = helpers::open(collection, name)?;
427+ let branches: Vec<String> = repository
428+ .branches()?
429+ .iter()
430+ .map(|branch| branch.name.clone())
431+ .collect();
432+ match default_menu!("Select a Branch", branches.as_slice()) {
433+ Some(i) => {
434+ let branch_name = branches.get(i).unwrap();
435+ let path = collection.path.join(name);
436+ println!("Building repository in {path:?} @ {branch_name}");
437+ let status = helpers::build(branch_name, &path)?;
438+ if status.success() {
439+ println!("Build completed successfully")
440+ } else {
441+ println!("Build has failed!")
442+ };
443+ self.execute(previous, None)
444+ }
445+ None => self.execute(previous, None),
446+ }
447+ }
448 None => match default_menu!("What would you like to do?", menu::INITIAL_ITEMS) {
449 Some(choice) => self.execute(menu::INITIAL_ITEMS.get(choice), None),
450 None => Ok(()),
451 diff --git a/crates/git/src/clone.rs b/crates/git/src/clone.rs
452index 0af2c86..4a66cc4 100644
453--- a/crates/git/src/clone.rs
454+++ b/crates/git/src/clone.rs
455 @@ -1,7 +1,22 @@
456+ use std::path::Path;
457+
458 use crate::error::Error;
459+
460 /// clone a remote repository into the given path with the specified depth.
461 /// this is only used as a part of ayllu-build
462- #[allow(dead_code)]
463- pub fn clone(_url: &str, _path: &str, _depth: Option<i64>) -> Result<(), Error> {
464- todo!()
465+ pub fn clone(url: &str, path: &Path, _depth: Option<i64>) -> Result<(), Error> {
466+ // TODO: Rewrite with libgit2
467+ let mut cmd = std::process::Command::new("git");
468+ cmd.args(["clone", url, path.to_str().unwrap()]);
469+ let output = cmd.output()?;
470+ tracing::info!(
471+ "Stdout:\n{}",
472+ String::from_utf8_lossy(output.stdout.as_slice())
473+ );
474+ tracing::info!(
475+ "Stderr:\n{}",
476+ String::from_utf8_lossy(output.stderr.as_slice())
477+ );
478+ assert!(output.status.success()); // FIXME
479+ Ok(())
480 }
481 diff --git a/crates/git/src/lib.rs b/crates/git/src/lib.rs
482index 58b8240..dda3bd8 100644
483--- a/crates/git/src/lib.rs
484+++ b/crates/git/src/lib.rs
485 @@ -1,3 +1,4 @@
486+ pub use clone::clone;
487 pub use config::{ChatKind, ChatLink, Config, Sites};
488 pub use error::Error;
489 pub use lite::{Blob, Branch, Commit, Kind, Note, Stats, Tag, TreeEntry};