Skip to main content

veryl_metadata/
lockfile.rs

1use crate::git::Git;
2use crate::lockfile_compat;
3use crate::metadata::{Dependency, Metadata, UrlPath};
4use crate::metadata_error::MetadataError;
5use crate::pubfile::{Pubfile, Release};
6use log::info;
7use pathdiff::diff_paths;
8use semver::{Version, VersionReq};
9use serde::{Deserialize, Serialize};
10use std::collections::{HashMap, HashSet};
11use std::fmt;
12use std::fs;
13use std::path::{Path, PathBuf};
14use std::str::FromStr;
15use url::Url;
16use uuid::Uuid;
17use veryl_path::{PathSet, ignore_already_exists};
18use walkdir::WalkDir;
19
20const LOCKFILE_VERSION: usize = 1;
21
22#[derive(Clone, Debug, Default, Serialize, Deserialize)]
23#[serde(deny_unknown_fields)]
24pub struct Lockfile {
25    version: usize,
26    projects: Vec<Lock>,
27    #[serde(skip)]
28    pub lock_table: HashMap<UrlPath, Vec<Lock>>,
29    #[serde(skip)]
30    force_update: bool,
31    #[serde(skip)]
32    pub metadata_path: PathBuf,
33}
34
35#[derive(Clone, Debug, Serialize, Deserialize)]
36#[serde(deny_unknown_fields)]
37pub struct Lock {
38    pub name: String,
39    pub source: LockSource,
40    pub dependencies: Vec<LockDependency>,
41    #[serde(skip)]
42    pub visible: bool,
43}
44
45#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
46#[serde(deny_unknown_fields)]
47#[serde(untagged)]
48pub enum LockSource {
49    Repository(Box<LockSourceRepository>),
50    Path(PathBuf),
51    // TODO
52    // Registory
53}
54
55impl LockSource {
56    pub fn to_url(&self) -> UrlPath {
57        match self {
58            LockSource::Repository(x) => x.url.clone(),
59            LockSource::Path(x) => UrlPath::Path(x.clone()),
60        }
61    }
62
63    pub fn get_version(&self) -> Option<&Version> {
64        match self {
65            LockSource::Repository(x) => Some(&x.version),
66            LockSource::Path(_) => None,
67        }
68    }
69
70    pub fn get_revision(&self) -> Option<&str> {
71        match self {
72            LockSource::Repository(x) => Some(&x.revision),
73            LockSource::Path(_) => None,
74        }
75    }
76}
77
78#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
79#[serde(deny_unknown_fields)]
80pub struct LockSourceRepository {
81    uuid: Uuid,
82    url: UrlPath,
83    path: PathBuf,
84    project: String,
85    version: Version,
86    revision: String,
87    r#override: Option<PathBuf>,
88}
89
90impl PartialOrd for LockSource {
91    fn partial_cmp(&self, other: &LockSource) -> Option<std::cmp::Ordering> {
92        Some(self.cmp(other))
93    }
94}
95
96impl Ord for LockSource {
97    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
98        match (self, other) {
99            (LockSource::Repository(x), LockSource::Repository(y)) => x
100                .url
101                .cmp(&y.url)
102                .then(x.project.cmp(&y.project))
103                .then(x.version.cmp(&y.version)),
104            (LockSource::Path(x), LockSource::Path(y)) => x.cmp(y),
105            (LockSource::Repository(_), LockSource::Path(_)) => std::cmp::Ordering::Less,
106            (LockSource::Path(_), LockSource::Repository(_)) => std::cmp::Ordering::Greater,
107        }
108    }
109}
110
111impl fmt::Display for LockSource {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        let mut ret = String::new();
114        match self {
115            LockSource::Repository(x) => {
116                ret.push_str(&format!("{} : {} @ {}", x.project, x.url, x.version));
117            }
118            LockSource::Path(x) => {
119                ret.push_str(&format!("{}", x.to_string_lossy()));
120            }
121        }
122        ret.fmt(f)
123    }
124}
125
126#[derive(Clone, Debug, Serialize, Deserialize)]
127#[serde(deny_unknown_fields)]
128pub struct LockDependency {
129    pub name: String,
130    pub source: LockSource,
131}
132
133impl Lockfile {
134    pub fn load(metadata: &Metadata) -> Result<Self, MetadataError> {
135        let path = metadata
136            .lockfile_path
137            .canonicalize()
138            .map_err(|x| MetadataError::file_io(x, &metadata.lockfile_path))?;
139        let text = fs::read_to_string(&path).map_err(|x| MetadataError::file_io(x, &path))?;
140        let mut ret = LockfileCompat::load(&text, &path, metadata)?;
141        ret.metadata_path = metadata.metadata_path.clone();
142
143        let mut locks = Vec::new();
144        locks.append(&mut ret.projects);
145
146        ret.lock_table.clear();
147        for lock in locks {
148            ret.lock_table
149                .entry(lock.source.to_url())
150                .and_modify(|x| x.push(lock.clone()))
151                .or_insert(vec![lock]);
152        }
153        ret.sort_table();
154
155        Ok(ret)
156    }
157
158    pub fn save<T: AsRef<Path>>(&mut self, path: T) -> Result<(), MetadataError> {
159        self.projects.clear();
160        for locks in self.lock_table.values() {
161            for lock in locks {
162                self.projects.push(lock.clone());
163            }
164        }
165        self.projects.sort_by(|x, y| x.source.cmp(&y.source));
166
167        let mut text = String::new();
168        text.push_str("# This file is automatically @generated by Veryl.\n");
169        text.push_str("# It is not intended for manual editing.\n");
170        text.push_str(&toml::to_string_pretty(&self)?);
171        fs::write(&path, text.as_bytes()).map_err(|x| MetadataError::file_io(x, path.as_ref()))?;
172        Ok(())
173    }
174
175    pub fn new(metadata: &Metadata) -> Result<Self, MetadataError> {
176        let mut ret = Lockfile {
177            version: LOCKFILE_VERSION,
178            metadata_path: metadata.metadata_path.clone(),
179            ..Default::default()
180        };
181
182        let mut name_table = HashSet::new();
183        let mut src_table = HashMap::new();
184        let locks = ret.gen_locks(metadata, &mut name_table, &mut src_table, true, metadata)?;
185
186        for lock in locks {
187            info!("Adding dependency ({})", lock.source);
188            ret.lock_table
189                .entry(lock.source.to_url())
190                .and_modify(|x| x.push(lock.clone()))
191                .or_insert(vec![lock]);
192        }
193        ret.sort_table();
194
195        Ok(ret)
196    }
197
198    pub fn update(
199        &mut self,
200        metadata: &Metadata,
201        force_update: bool,
202    ) -> Result<bool, MetadataError> {
203        self.force_update = force_update;
204
205        let mut name_table = HashSet::new();
206        let mut src_table = HashMap::new();
207        let locks = self.gen_locks(metadata, &mut name_table, &mut src_table, true, metadata)?;
208
209        let old_table = self.lock_table.clone();
210        self.lock_table.clear();
211
212        let mut modified = false;
213
214        for lock in &locks {
215            let add = if let Some(old_locks) = old_table.get(&lock.source.to_url()) {
216                !old_locks.iter().any(|x| x.source == lock.source)
217            } else {
218                true
219            };
220
221            if add {
222                info!("Adding dependency ({})", lock.source);
223                modified = true;
224            }
225
226            self.lock_table
227                .entry(lock.source.to_url())
228                .and_modify(|x| x.push(lock.clone()))
229                .or_insert(vec![lock.clone()]);
230        }
231        self.sort_table();
232
233        for old_locks in old_table.values() {
234            for old_lock in old_locks {
235                if !locks.iter().any(|x| x.source == old_lock.source) {
236                    info!("Removing dependency ({})", old_lock.source);
237                    modified = true;
238                }
239            }
240        }
241
242        Ok(modified)
243    }
244
245    pub fn paths(&self, base_dst: &Path) -> Result<Vec<PathSet>, MetadataError> {
246        let mut ret = Vec::new();
247
248        for locks in self.lock_table.values() {
249            for lock in locks {
250                let metadata = self.get_metadata(&lock.source)?;
251                let path = metadata.project_path();
252
253                for src in &veryl_path::gather_files_with_extension(&path, "veryl", false)? {
254                    let Ok(rel) = src.strip_prefix(&path) else {
255                        return Err(MetadataError::InvalidSourceLocation(src.clone()));
256                    };
257                    let mut dst = base_dst.join(&lock.name);
258                    dst.push(rel);
259                    dst.set_extension("sv");
260                    let mut map = dst.clone();
261                    map.set_extension("sv.map");
262                    ret.push(PathSet {
263                        prj: lock.name.clone(),
264                        src: src.to_path_buf(),
265                        dst,
266                        map,
267                    });
268                }
269            }
270        }
271
272        Ok(ret)
273    }
274
275    pub fn clear_cache(&self) -> Result<(), MetadataError> {
276        let lock_resolve = veryl_path::lock_dir("resolve")?;
277        let lock_dependencies = veryl_path::lock_dir("dependencies")?;
278
279        for locks in self.lock_table.values() {
280            for lock in locks {
281                if let LockSource::Repository(x) = &lock.source {
282                    let resolve_path = Self::resolve_path(&x.url)?;
283                    let dependency_path = Self::dependency_path(&x.url, &x.path, &x.revision)?;
284                    if resolve_path.exists() {
285                        fs::remove_dir_all(&resolve_path)
286                            .map_err(|x| MetadataError::file_io(x, &resolve_path))?;
287                    }
288                    if dependency_path.exists() {
289                        fs::remove_dir_all(&dependency_path)
290                            .map_err(|x| MetadataError::file_io(x, &dependency_path))?;
291                    }
292                }
293            }
294        }
295
296        veryl_path::unlock_dir(lock_resolve)?;
297        veryl_path::unlock_dir(lock_dependencies)?;
298
299        Ok(())
300    }
301
302    fn git_clone(&self, url: &UrlPath, path: &Path) -> Result<Git, MetadataError> {
303        let url = match url {
304            UrlPath::Url(x) => UrlPath::Url(x.clone()),
305            UrlPath::Path(x) => {
306                if x.is_relative() {
307                    let path = self.metadata_path.parent().unwrap().join(x);
308                    UrlPath::Path(path)
309                } else {
310                    UrlPath::Path(x.clone())
311                }
312            }
313        };
314
315        Git::clone(&url, path)
316    }
317
318    fn sort_table(&mut self) {
319        for locks in self.lock_table.values_mut() {
320            locks.sort_by(|a, b| b.source.cmp(&a.source));
321        }
322    }
323
324    fn gen_uuid(url: &UrlPath, path: &Path, revision: &str) -> Result<Uuid, MetadataError> {
325        let mut url = url.to_string();
326        url.push_str(&path.to_string_lossy());
327        url.push_str(revision);
328        Ok(Uuid::new_v5(&Uuid::NAMESPACE_URL, url.as_bytes()))
329    }
330
331    fn gen_locks(
332        &mut self,
333        metadata: &Metadata,
334        name_table: &mut HashSet<String>,
335        src_table: &mut HashMap<LockSource, String>,
336        root: bool,
337        root_metadata: &Metadata,
338    ) -> Result<Vec<Lock>, MetadataError> {
339        let mut ret = Vec::new();
340
341        // breadth first search because root has top priority of name
342        let mut dependencies_metadata = Vec::new();
343        for (name, dep) in &metadata.dependencies {
344            let dependency = self.resolve_dependency(metadata, name, dep, root, root_metadata)?;
345            let metadata = self.get_metadata(&dependency.source)?;
346            let mut name = dependency.name.clone();
347
348            // avoid name conflict by adding suffix
349            if name_table.contains(&name) {
350                if root {
351                    return Err(MetadataError::NameConflict(name));
352                }
353                let mut suffix = 0;
354                loop {
355                    let new_name = format!("{name}_{suffix}");
356                    if !name_table.contains(&new_name) {
357                        name = new_name;
358                        break;
359                    }
360                    suffix += 1;
361                }
362            }
363            name_table.insert(name.clone());
364
365            let mut dependencies = Vec::new();
366            for (name, dep) in &metadata.dependencies {
367                let dependency =
368                    self.resolve_dependency(&metadata, name, dep, root, root_metadata)?;
369                // project local name is not required to check name_table
370                dependencies.push(dependency);
371            }
372
373            if let Some(x) = src_table.get(&dependency.source) {
374                if root {
375                    return Err(MetadataError::InvalidDependency {
376                        name: dependency.name.clone(),
377                        cause: format!("it conflicts with {x}"),
378                    });
379                }
380            } else {
381                let lock = Lock {
382                    name: name.clone(),
383                    source: dependency.source.clone(),
384                    dependencies,
385                    visible: root,
386                };
387
388                ret.push(lock);
389                src_table.insert(dependency.source.clone(), name.clone());
390                dependencies_metadata.push(metadata);
391            }
392        }
393
394        for metadata in dependencies_metadata {
395            let mut dependency_locks =
396                self.gen_locks(&metadata, name_table, src_table, false, root_metadata)?;
397            ret.append(&mut dependency_locks);
398        }
399
400        Ok(ret)
401    }
402
403    fn resolve_dependency(
404        &mut self,
405        metadata: &Metadata,
406        name: &str,
407        dep: &Dependency,
408        root: bool,
409        root_metadata: &Metadata,
410    ) -> Result<LockDependency, MetadataError> {
411        Ok(match dep {
412            Dependency::Version(_) => {
413                unimplemented!();
414            }
415            Dependency::Entry(x) => {
416                let url = if let Some(git) = &x.git {
417                    Some(git.clone())
418                } else if let Some(github) = &x.github {
419                    let url = format!("https://github.com/{github}");
420                    let url = Url::parse(&url).unwrap();
421                    Some(UrlPath::Url(url))
422                } else {
423                    None
424                };
425                let project = x.project.clone().unwrap_or(name.to_string());
426                let source = if let Some(url) = &url {
427                    let Some(version) = &x.version else {
428                        return Err(MetadataError::InvalidDependency {
429                            name: name.to_string(),
430                            cause: "version is not specified".to_string(),
431                        });
432                    };
433                    let (release, path) = self.resolve_version(url, &project, version)?;
434                    let uuid = Self::gen_uuid(url, &path, &release.revision)?;
435
436                    // Path override is disabled if it is not root
437                    let r#override = if root { x.path.clone() } else { None };
438
439                    LockSource::Repository(Box::new(LockSourceRepository {
440                        uuid,
441                        url: url.clone(),
442                        path,
443                        project,
444                        version: release.version,
445                        revision: release.revision,
446                        r#override,
447                    }))
448                } else if let Some(path) = &x.path {
449                    let path = if path.is_absolute() {
450                        path.clone()
451                    } else {
452                        let base = root_metadata.project_path();
453                        let path = base.join(metadata.project_path()).join(path);
454                        if !path.exists() {
455                            let project = x.project.clone().unwrap_or(name.to_string());
456                            return Err(MetadataError::ProjectNotFound {
457                                url: UrlPath::Path(path),
458                                project,
459                            });
460                        }
461                        diff_paths(path.canonicalize().unwrap(), base).unwrap()
462                    };
463                    LockSource::Path(path)
464                } else {
465                    return Err(MetadataError::InvalidDependency {
466                        name: name.to_string(),
467                        cause: "[git|github|path] are not specified".to_string(),
468                    });
469                };
470                LockDependency {
471                    name: name.to_string(),
472                    source,
473                }
474            }
475        })
476    }
477
478    fn resolve_version(
479        &mut self,
480        url: &UrlPath,
481        project: &str,
482        version_req: &VersionReq,
483    ) -> Result<(Release, PathBuf), MetadataError> {
484        if let Some(release) = self.resolve_version_from_lockfile(url, project, version_req)? {
485            if self.force_update {
486                let latest = self.resolve_version_from_latest(url, project, version_req)?;
487                Ok(latest)
488            } else {
489                Ok(release)
490            }
491        } else {
492            let latest = self.resolve_version_from_latest(url, project, version_req)?;
493            Ok(latest)
494        }
495    }
496
497    fn resolve_version_from_lockfile(
498        &mut self,
499        url: &UrlPath,
500        project: &str,
501        version_req: &VersionReq,
502    ) -> Result<Option<(Release, PathBuf)>, MetadataError> {
503        if let Some(locks) = self.lock_table.get_mut(url) {
504            for lock in locks {
505                if let LockSource::Repository(x) = &lock.source
506                    && x.project == project
507                    && version_req.matches(&x.version)
508                {
509                    let release = Release {
510                        version: x.version.clone(),
511                        revision: x.revision.clone(),
512                    };
513                    let path = x.path.clone();
514                    return Ok(Some((release, path)));
515                }
516            }
517        }
518        Ok(None)
519    }
520
521    fn resolve_path(url: &UrlPath) -> Result<PathBuf, MetadataError> {
522        let resolve_dir = veryl_path::cache_path().join("resolve");
523        let uuid = Self::gen_uuid(url, &PathBuf::new(), "")?;
524        Ok(resolve_dir.join(uuid.simple().encode_lower(&mut Uuid::encode_buffer())))
525    }
526
527    fn search_project(path: &Path, project: &str) -> Option<PathBuf> {
528        for entry in WalkDir::new(path).into_iter().flatten() {
529            if entry.file_name() == "Veryl.toml"
530                && let Ok(metadata) = Metadata::load(entry.path())
531                && metadata.project.name == project
532            {
533                let ret = entry.path();
534                let ret = ret.parent().unwrap().strip_prefix(path).unwrap();
535                return Some(ret.to_path_buf());
536            }
537        }
538        None
539    }
540
541    fn resolve_version_from_latest(
542        &mut self,
543        url: &UrlPath,
544        project: &str,
545        version_req: &VersionReq,
546    ) -> Result<(Release, PathBuf), MetadataError> {
547        let resolve_dir = veryl_path::cache_path().join("resolve");
548
549        if !resolve_dir.exists() {
550            ignore_already_exists(fs::create_dir_all(&resolve_dir))
551                .map_err(|x| MetadataError::file_io(x, &resolve_dir))?;
552        }
553
554        let path = Self::resolve_path(url)?;
555        let lock = veryl_path::lock_dir("resolve")?;
556        let git = self.git_clone(url, &path)?;
557        git.fetch()?;
558        git.checkout(None)?;
559        veryl_path::unlock_dir(lock)?;
560
561        let Some(prj_path) = Self::search_project(&path, project) else {
562            return Err(MetadataError::ProjectNotFound {
563                url: url.clone(),
564                project: project.to_string(),
565            });
566        };
567
568        let toml = path.join(&prj_path).join("Veryl.pub");
569        let mut pubfile = Pubfile::load(toml)?;
570
571        pubfile.releases.sort_by(|a, b| b.version.cmp(&a.version));
572
573        for release in &pubfile.releases {
574            if version_req.matches(&release.version) {
575                return Ok((release.clone(), prj_path));
576            }
577        }
578
579        Err(MetadataError::VersionNotFound {
580            url: url.clone(),
581            version: version_req.to_string(),
582        })
583    }
584
585    fn dependency_path(
586        url: &UrlPath,
587        path: &Path,
588        revision: &str,
589    ) -> Result<PathBuf, MetadataError> {
590        let dependencies_dir = veryl_path::cache_path().join("dependencies");
591        let uuid = Self::gen_uuid(url, path, revision)?;
592        Ok(dependencies_dir.join(uuid.simple().encode_lower(&mut Uuid::encode_buffer())))
593    }
594
595    fn get_metadata(&self, source: &LockSource) -> Result<Metadata, MetadataError> {
596        // try to load from local path
597        let path = match source {
598            LockSource::Path(x) => Some(x.clone()),
599            LockSource::Repository(x) => x.r#override.clone(),
600        };
601        let path_metadata = if let Some(x) = path {
602            let path = self.metadata_path.parent().unwrap().join(x);
603            let path = path.join("Veryl.toml");
604            if path.exists() {
605                Some(Metadata::load(path)?)
606            } else {
607                None
608            }
609        } else {
610            None
611        };
612
613        match source {
614            LockSource::Path(_) => {
615                if let Some(x) = path_metadata {
616                    Ok(x)
617                } else {
618                    Err(MetadataError::FileNotFound)
619                }
620            }
621            LockSource::Repository(x) => {
622                if let Some(x) = path_metadata {
623                    Ok(x)
624                } else {
625                    let dependencies_dir = veryl_path::cache_path().join("dependencies");
626
627                    if !dependencies_dir.exists() {
628                        ignore_already_exists(fs::create_dir_all(&dependencies_dir))
629                            .map_err(|x| MetadataError::file_io(x, &dependencies_dir))?;
630                    }
631
632                    let path = Self::dependency_path(&x.url, &x.path, &x.revision)?;
633                    let toml = path.join("Veryl.toml");
634
635                    // Acquire the lock before checking path existence to prevent
636                    // race conditions where gix::prepare_clone creates an
637                    // incomplete directory that other threads may observe.
638                    let lock = veryl_path::lock_dir("dependencies")?;
639                    if !path.exists() {
640                        let git = self.git_clone(&x.url, &path)?;
641                        git.fetch()?;
642                        git.checkout(Some(&x.revision))?;
643                    } else {
644                        let git = Git::open(&path)?;
645                        let ret = git.is_clean().is_ok_and(|x| x);
646
647                        // If the existing path is not git repository, cleanup and re-try
648                        if !ret || !toml.exists() {
649                            veryl_path::ignore_directory_not_empty(fs::remove_dir_all(&path))
650                                .map_err(|x| MetadataError::file_io(x, &path))?;
651                            let git = self.git_clone(&x.url, &path)?;
652                            git.fetch()?;
653                            git.checkout(Some(&x.revision))?;
654                        }
655                    }
656                    veryl_path::unlock_dir(lock)?;
657
658                    Metadata::load(toml)
659                }
660            }
661        }
662    }
663}
664
665impl FromStr for Lockfile {
666    type Err = MetadataError;
667
668    fn from_str(s: &str) -> Result<Self, Self::Err> {
669        let lockfile: Lockfile = toml::from_str(s)?;
670        Ok(lockfile)
671    }
672}
673
674#[derive(Clone, Debug, Default, Serialize, Deserialize)]
675pub struct LockfileCompat {
676    version: Option<usize>,
677}
678
679impl LockfileCompat {
680    pub fn load(
681        text: &str,
682        lockfile_path: &Path,
683        metadata: &Metadata,
684    ) -> Result<Lockfile, MetadataError> {
685        let compat: LockfileCompat = toml::from_str(text)?;
686        let version = compat.version.unwrap_or(0);
687        let mut lockfile = match version {
688            0 => {
689                info!(
690                    "Migrating lockfile to v1 ({})",
691                    lockfile_path.to_string_lossy()
692                );
693                let lockfile: lockfile_compat::v0::Lockfile = toml::from_str(text)?;
694                let mut lockfile = Lockfile::from_v0(lockfile, &metadata.metadata_path)?;
695                lockfile.save(lockfile_path)?;
696                lockfile
697            }
698            1 => toml::from_str(text)?,
699            _ => unreachable!(),
700        };
701
702        for lock in lockfile.projects.iter_mut() {
703            lock.visible = metadata.dependencies.contains_key(&lock.name);
704        }
705
706        Ok(lockfile)
707    }
708}
709
710impl Lockfile {
711    fn set_project(
712        mut source: LockSource,
713        metadata_path: &Path,
714    ) -> Result<LockSource, MetadataError> {
715        let lockfile = Lockfile {
716            metadata_path: metadata_path.to_path_buf(),
717            ..Default::default()
718        };
719        let metadata = lockfile.get_metadata(&source)?;
720        if let LockSource::Repository(x) = &mut source {
721            x.project = metadata.project.name;
722        }
723        Ok(source)
724    }
725
726    pub fn from_v0(
727        x: lockfile_compat::v0::Lockfile,
728        metadata_path: &Path,
729    ) -> Result<Self, MetadataError> {
730        let mut projects = Vec::new();
731        for lock in x.projects {
732            let mut dependencies = Vec::new();
733            for dep in lock.dependencies {
734                let uuid = Lockfile::gen_uuid(&dep.url, &PathBuf::new(), &dep.revision)?;
735                let source = LockSource::Repository(Box::new(LockSourceRepository {
736                    uuid,
737                    url: dep.url,
738                    path: PathBuf::new(),
739                    project: String::new(),
740                    version: dep.version,
741                    revision: dep.revision,
742                    r#override: None,
743                }));
744                let source = Self::set_project(source, metadata_path)?;
745                let new_dep = LockDependency {
746                    name: dep.name,
747                    source,
748                };
749                dependencies.push(new_dep);
750            }
751
752            let uuid = Lockfile::gen_uuid(&lock.url, &PathBuf::new(), &lock.revision).unwrap();
753            let source = LockSource::Repository(Box::new(LockSourceRepository {
754                uuid,
755                url: lock.url,
756                path: PathBuf::new(),
757                project: String::new(),
758                version: lock.version,
759                revision: lock.revision,
760                r#override: lock.path,
761            }));
762            let source = Self::set_project(source, metadata_path)?;
763
764            let new_lock = Lock {
765                name: lock.name,
766                source,
767                dependencies,
768                visible: false,
769            };
770
771            projects.push(new_lock);
772        }
773
774        Ok(Lockfile {
775            version: LOCKFILE_VERSION,
776            projects,
777            ..Default::default()
778        })
779    }
780}