git_stree/
lib.rs

1//! Library for working with my improved git subtree schema.
2//!
3//! The subtrees with their `<prefix>`, `<repository>` and a target to follow are tracked in
4//! `.gitsubtrees` files. Each `.gitsubtrees` file contains information about tracked subtrees in
5//! the same directory.
6//!
7//! ## `.gitsubtrees` Format
8//!
9//! ```ini
10//! [example]
11//!       version = 1 ; opional normally, required if no other key specified
12//!       upstream = https://example.com/ORIGINAL/example
13//!       origin = https://example.com/FORKED/example
14//!       follow = master ; some ref or a semver range
15//!       pre-releases = false ; if allow pulling pre-releases
16//! ```
17use getset::Getters;
18use std::collections::HashMap;
19
20use configparser::ini::Ini;
21use git_wrapper::ConfigSetError;
22use git_wrapper::{
23    RefSearchError, RepoError, Repository, StagingError, SubtreeAddError, SubtreePullError,
24    SubtreePushError, SubtreeSplitError,
25};
26use std::path::{Path, PathBuf};
27
28use posix_errors::{PosixError, EAGAIN, EINVAL, ENOENT, ENOTRECOVERABLE, ENOTSUP};
29
30/// Configuration for a subtree
31#[derive(Getters, Clone, Debug, Eq, PartialEq)]
32pub struct SubtreeConfig {
33    /// subtree id
34    #[getset(get = "pub")]
35    id: String,
36    /// Follow schema for subtree
37    #[getset(get = "pub")]
38    follow: Option<String>,
39    /// Origin remote for subtree
40    #[getset(get = "pub")]
41    origin: Option<String>,
42    /// Upstream remote for subtree
43    #[getset(get = "pub")]
44    upstream: Option<String>,
45    /// `true` if this subtree should also pull pre release tags e.g. “1.0.3-23-alpah”
46    #[getset(get = "pub")]
47    pull_pre_releases: bool,
48}
49
50impl Ord for SubtreeConfig {
51    #[inline]
52    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
53        self.id.cmp(&other.id)
54    }
55}
56
57impl PartialOrd for SubtreeConfig {
58    #[inline]
59    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
60        Some(self.cmp(other))
61    }
62}
63
64impl SubtreeConfig {
65    /// Return a new instance
66    #[must_use]
67    #[inline]
68    pub const fn new(
69        id: String,
70        follow: Option<String>,
71        origin: Option<String>,
72        upstream: Option<String>,
73        pull_pre_releases: bool,
74    ) -> Self {
75        Self {
76            id,
77            follow,
78            origin,
79            upstream,
80            pull_pre_releases,
81        }
82    }
83    /// Return `true` if upstream is set
84    #[must_use]
85    #[inline]
86    pub const fn is_pullable(&self) -> bool {
87        self.upstream.is_some() || self.origin.is_some()
88    }
89
90    /// Return `true` if origin is set
91    #[must_use]
92    #[inline]
93    pub const fn is_pushable(&self) -> bool {
94        self.origin.is_some()
95    }
96
97    /// Return path to config file
98    #[must_use]
99    #[inline]
100    pub fn config_file(&self) -> String {
101        let mut result = self
102            .id
103            .rsplit_once('/')
104            .map_or_else(|| "".to_owned(), |x| x.1.to_owned());
105        result.push_str(".gitsubtrees");
106        result
107    }
108
109    /// Subtree name
110    #[must_use]
111    #[inline]
112    pub fn name(&self) -> String {
113        self.id()
114            .rsplit_once('/')
115            .map_or_else(|| self.id.clone(), |x| x.1.to_owned())
116    }
117
118    fn parse_remote_version_req(input: &str) -> Result<semver::VersionReq, PosixError> {
119        let tmp = input
120            .strip_suffix('}')
121            .ok_or_else(|| PosixError::new(EINVAL, format!("Illegal upstream value {}", input)))?;
122
123        let tmp2 = tmp
124            .strip_prefix("@{")
125            .ok_or_else(|| PosixError::new(EINVAL, format!("Illegal upstream value {}", input)))?;
126
127        semver::VersionReq::parse(tmp2).map_err(|e| PosixError::new(EINVAL, format!("{}", e)))
128    }
129
130    /// Figure out which named ref to pull from.
131    ///
132    /// # Panics
133    ///
134    /// Will panic if `&self` has no upstream remote defined
135    ///
136    /// # Errors
137    ///
138    /// Will return a [`PosixError`] when fails to find a ref to pull
139    #[inline]
140    pub fn ref_to_pull(&self) -> Result<String, PosixError> {
141        if !self.is_pullable() {
142            return Err(PosixError::new(
143                ENOENT,
144                "Subtree does not have upstream remote defined".to_owned(),
145            ));
146        }
147        let candidate = self.follow.clone().unwrap_or_else(|| "HEAD".to_owned());
148        let remote = &self
149            .upstream
150            .clone()
151            .or_else(|| self.origin.clone())
152            .ok_or_else(|| PosixError::new(ENOENT, "No origin or upstream set".to_owned()))?;
153        let follow = if candidate == *"@{tags}" {
154            find_latest_version(remote)?
155        } else if candidate.starts_with("@{") {
156            let range = Self::parse_remote_version_req(&candidate)?;
157            return find_latest_version_matching(remote, &range, *self.pull_pre_releases());
158        } else if candidate == *"HEAD" {
159            git_wrapper::resolve_head(remote)?
160        } else {
161            candidate
162        };
163        Ok(follow)
164    }
165}
166
167/// Aliases some well known urls to their initials.
168#[must_use]
169#[inline]
170pub fn alias_url(url: &str) -> String {
171    let github = regex::Regex::new(r"^(git@github.com:|.+://github.com/)").expect("Valid RegEx");
172    let gitlab = regex::Regex::new(r"^(git@gitlab.com:|.+://gitlab.com/)").expect("Valid RegEx");
173    let bitbucket =
174        regex::Regex::new(r"^(git@bitbucket.com:|.+://bitbucket.com/)").expect("Valid RegEx");
175    if github.is_match(url) {
176        return github.replace(url, "GH:").to_string();
177    }
178    if gitlab.is_match(url) {
179        return gitlab.replace(url, "GL:").to_string();
180    }
181    if bitbucket.is_match(url) {
182        return bitbucket.replace(url, "BB:").to_string();
183    }
184    url.to_owned()
185}
186
187fn versions_from_remote(url: &str) -> Result<HashMap<semver::Version, String>, PosixError> {
188    let mut result = HashMap::new();
189
190    let tmp = git_wrapper::tags_from_remote(url)?;
191    for s in tmp {
192        let version_result = lenient_semver::parse(&s);
193        if let Ok(version) = version_result {
194            result.insert(version, s);
195        }
196    }
197
198    Ok(result)
199}
200
201/// Return the latest version from remote
202///
203/// # Errors
204///
205/// Will return [`PosixError`] if command exits if no versions found.
206#[inline]
207pub fn find_latest_version(remote: &str) -> Result<String, PosixError> {
208    let versions = versions_from_remote(remote)?;
209    if versions.is_empty() {
210        let message = "Failed to find any valid tags".to_owned();
211        return Err(PosixError::new(ENOENT, message));
212    }
213
214    let mut keys: Vec<&semver::Version> = Vec::new();
215    for v in versions.keys() {
216        keys.push(v);
217    }
218    keys.sort();
219    let key = keys.pop().expect("Keys should not be empty");
220
221    Ok(versions.get(key).expect("Keys should exist").clone())
222}
223
224/// Return the latest version from remote matching [`semver::VersionReq`]
225/// # Errors
226///
227/// Will return [`PosixError`] if command exits if fails to find matching version.
228#[inline]
229pub fn find_latest_version_matching(
230    remote: &str,
231    range: &semver::VersionReq,
232    pre_releases: bool,
233) -> Result<String, PosixError> {
234    let versions_map = versions_from_remote(remote)?;
235    let mut keys: Vec<&semver::Version> = Vec::new();
236    for v in versions_map.keys() {
237        keys.push(v);
238    }
239    keys.sort();
240
241    let mut latest: Option<&semver::Version> = None;
242    let mut versions: Vec<&semver::Version> = versions_map.keys().collect();
243    versions.sort();
244    for v in versions {
245        if range.matches(v) {
246            latest.replace(v);
247        } else if pre_releases {
248            let tmp = semver::Version::new(v.major, v.minor, v.patch);
249            if range.matches(&tmp) {
250                latest.replace(v);
251            }
252        } else {
253        }
254    }
255    latest.map_or_else(
256        || {
257            let msg = format!("Failed to find a tag matching {}", range);
258            Err(PosixError::new(ENOENT, msg))
259        },
260        |v| {
261            let result = versions_map.get(v);
262            Ok(result.expect("Version is in version map").clone())
263        },
264    )
265}
266
267/// Manages subtrees in a repository
268#[derive(Debug)]
269pub struct Subtrees {
270    repo: Repository,
271    configs: Vec<SubtreeConfig>,
272}
273
274/// Failed to initialize `Subtrees`
275#[allow(missing_docs)]
276#[derive(thiserror::Error, Debug)]
277pub enum SubtreesError {
278    #[error("{0}")]
279    RepoError(#[from] RepoError),
280    #[error("{0}")]
281    InvalidConfig(#[from] ConfigError),
282}
283
284impl From<SubtreesError> for PosixError {
285    #[inline]
286    fn from(err: SubtreesError) -> Self {
287        match err {
288            SubtreesError::InvalidConfig(e) => Self::new(EINVAL, format!("{}", e)),
289            SubtreesError::RepoError(e) => e.into(),
290        }
291    }
292}
293
294/// Failed reading or parsing a `.gitsubtrees` file.
295#[allow(missing_docs)]
296#[derive(thiserror::Error, Debug)]
297pub enum ConfigError {
298    #[error("{0}")]
299    ReadFailed(#[from] std::io::Error),
300    #[error("Failed to parse config {0:?}")]
301    ParseFailed(PathBuf),
302}
303
304impl From<ConfigError> for PosixError {
305    #[inline]
306    fn from(err: ConfigError) -> Self {
307        match err {
308            ConfigError::ReadFailed(e) => e.into(),
309            ConfigError::ParseFailed(p) => Self::new(1, format!("Failed to parse config {:?}", p)),
310        }
311    }
312}
313
314/// Failed adding a new subtree to a repository fails
315#[allow(missing_docs)]
316#[derive(thiserror::Error, Debug, PartialEq, Eq)]
317pub enum AdditionError {
318    #[error("{0}")]
319    AddError(#[from] SubtreeAddError),
320    #[error("Work tree is dirty")]
321    WorkTreeDirty,
322    #[error("Failed to write config {0:?}")]
323    WriteConfig(String),
324    #[error("No upstream remote defined")]
325    NoUpstream,
326    #[error("{0}")]
327    StagingError(#[from] StagingError),
328    #[error("Invalid version {0}")]
329    InvalidVersion(String),
330    #[error("{0}")]
331    Failure(String, i32),
332}
333
334impl From<AdditionError> for PosixError {
335    #[inline]
336    fn from(err: AdditionError) -> Self {
337        match err {
338            AdditionError::AddError(e) => e.into(),
339            AdditionError::StagingError(e) => e.into(),
340            AdditionError::WorkTreeDirty => {
341                let msg = "Working tree is dirty".to_owned();
342                Self::new(ENOTSUP, msg)
343            }
344            AdditionError::NoUpstream => Self::new(1, format!("{}", err)),
345            AdditionError::InvalidVersion(version) => {
346                let msg = format!("Invalid version {}", version);
347                Self::new(EINVAL, msg)
348            }
349            AdditionError::Failure(msg, _) | AdditionError::WriteConfig(msg) => Self::new(1, msg),
350        }
351    }
352}
353impl From<ConfigSetError> for AdditionError {
354    #[inline]
355    fn from(err: ConfigSetError) -> Self {
356        match err {
357            ConfigSetError::InvalidConfigFile(f) => {
358                let msg = format!("Invalid config file: {}", f);
359                Self::WriteConfig(msg)
360            }
361            ConfigSetError::WriteFailed(f) => {
362                let msg = format!("Failed to write config file: {}", f);
363                Self::WriteConfig(msg)
364            }
365            ConfigSetError::InvalidSectionOrKey(msg) => Self::WriteConfig(msg),
366            ConfigSetError::Failure(msg, code) => Self::Failure(msg, code),
367        }
368    }
369}
370
371/// Failed to find specified subtree
372#[allow(missing_docs)]
373#[derive(thiserror::Error, Debug)]
374pub enum FindError {
375    #[error("Bare repository")]
376    BareRepository,
377    #[error("{0}")]
378    ConfigError(#[from] ConfigError),
379    #[error("Not found subtree {0}")]
380    NotFound(String),
381}
382
383impl From<FindError> for PosixError {
384    #[inline]
385    fn from(err: FindError) -> Self {
386        Self::new(EINVAL, format!("{}", err))
387    }
388}
389
390/// Failed to update a subtree from remote
391#[allow(missing_docs)]
392#[derive(thiserror::Error, Debug)]
393pub enum PullError {
394    #[error("{0}")]
395    Failure(String),
396    #[error("{0}")]
397    IOError(#[from] std::io::Error),
398    #[error("No changes to pull")]
399    NoChanges,
400    #[error("No upstream remote defined")]
401    NoUpstream,
402    #[error("{0}")]
403    ReferenceNotFound(#[from] RefSearchError),
404    #[error("Work tree is dirty")]
405    WorkTreeDirty,
406}
407
408impl From<PullError> for PosixError {
409    #[inline]
410    fn from(err: PullError) -> Self {
411        match err {
412            PullError::WorkTreeDirty => {
413                let msg = "Can not execute pull operation in a dirty repository".to_owned();
414                Self::new(ENOENT, msg)
415            }
416            PullError::ReferenceNotFound(e) => e.into(),
417            PullError::NoChanges => {
418                let msg = "Upstream does not have any new changes".to_owned();
419                Self::new(EAGAIN, msg)
420            }
421            PullError::NoUpstream => {
422                let msg = "Subtree does not have a upstream defined".to_owned();
423                Self::new(ENOTRECOVERABLE, msg)
424            }
425            PullError::Failure(msg) => Self::new(1, msg),
426            PullError::IOError(e) => Self::from(e),
427        }
428    }
429}
430
431impl From<SubtreePullError> for PullError {
432    #[inline]
433    fn from(prev: SubtreePullError) -> Self {
434        match prev {
435            SubtreePullError::Failure(msg, _) => Self::Failure(msg),
436            SubtreePullError::WorkTreeDirty => Self::WorkTreeDirty,
437        }
438    }
439}
440
441/// Failed to push subtree to remote
442#[allow(missing_docs)]
443#[derive(thiserror::Error, Debug)]
444pub enum PushError {
445    #[error("No upstream remote defined")]
446    NoUpstream,
447    #[error("{0}")]
448    Failure(String),
449}
450
451impl From<PushError> for PosixError {
452    #[inline]
453    fn from(err: PushError) -> Self {
454        match err {
455            PushError::NoUpstream => {
456                let msg = "Subtree does not have a upstream defined".to_owned();
457                Self::new(ENOTRECOVERABLE, msg)
458            }
459
460            PushError::Failure(msg) => Self::new(1, msg),
461        }
462    }
463}
464
465impl From<SubtreePushError> for PushError {
466    #[inline]
467    fn from(prev: SubtreePushError) -> Self {
468        match prev {
469            SubtreePushError::Failure(msg, _) => Self::Failure(msg),
470        }
471    }
472}
473
474/// Failed to split subtree
475#[allow(missing_docs)]
476#[derive(thiserror::Error, Debug)]
477pub enum SplitError {
478    #[error("Work tree is dirty")]
479    WorkTreeDirty,
480    #[error("{0}")]
481    Failure(String),
482}
483
484impl From<SplitError> for PosixError {
485    #[inline]
486    fn from(err: SplitError) -> Self {
487        match err {
488            SplitError::WorkTreeDirty => {
489                let msg = "Can not execute push operation in a dirty repository".to_owned();
490                Self::new(ENOENT, msg)
491            }
492            SplitError::Failure(msg) => Self::new(1, msg),
493        }
494    }
495}
496
497impl From<SubtreeSplitError> for SplitError {
498    #[inline]
499    fn from(prev: SubtreeSplitError) -> Self {
500        match prev {
501            SubtreeSplitError::Failure(msg, _) => Self::Failure(msg),
502            SubtreeSplitError::WorkTreeDirty => Self::WorkTreeDirty,
503        }
504    }
505}
506
507#[allow(clippy::missing_errors_doc)]
508impl Subtrees {
509    /// # Errors
510    ///
511    /// Throws [`SubtreesError`] if fails to find or access repository.
512    #[inline]
513    pub fn new() -> Result<Self, SubtreesError> {
514        let repo = Repository::default()?;
515        let configs = all(&repo)?;
516        Ok(Self { repo, configs })
517    }
518
519    /// Discover subtrees in a git repository
520    #[inline]
521    pub fn from_repo(repo: Repository) -> Result<Self, SubtreesError> {
522        let configs = all(&repo)?;
523        Ok(Self { repo, configs })
524    }
525
526    /// Discover subtrees in specified directory
527    #[inline]
528    pub fn from_dir(path: &Path) -> Result<Self, SubtreesError> {
529        let repo = Repository::discover(path)?;
530        let configs = all(&repo)?;
531        Ok(Self { repo, configs })
532    }
533
534    /// Add subtree to repository
535    ///
536    /// # Errors
537    ///
538    /// Throws errors when there are errors
539    ///
540    /// # Panics
541    ///
542    /// Panics when something unexpected happens
543    #[inline]
544    pub fn add(
545        &self,
546        subtree: &SubtreeConfig,
547        revision: Option<&str>,
548        subject: Option<&str>,
549    ) -> Result<(), AdditionError> {
550        if let Some(rev) = revision {
551            let remote = subtree.upstream.as_ref().ok_or(AdditionError::NoUpstream)?;
552            let target = subtree.id();
553
554            let title = subject.map_or_else(
555                || format!(":{} Import {}", target, alias_url(remote)),
556                |v| format!(":{} {}", target, v),
557            );
558            let msg = format!(
559                "{}
560
561git-subtree-origin: {}
562git-subtree-remote-ref: {}",
563                title, remote, rev
564            );
565            self.repo.subtree_add(remote, target, rev, &msg)?;
566        }
567        self.persist(subtree)?;
568        self.repo.stage(Path::new(&subtree.config_file()))?;
569
570        let mut cmd = self.repo.git();
571        cmd.args(&["commit", "--amend", "--no-edit"]);
572        let out = cmd.output().expect("Failed to execute git-commit(1)");
573        if !out.status.success() {
574            let msg = String::from_utf8_lossy(&out.stderr).to_string();
575            return Err(AdditionError::WriteConfig(msg));
576        }
577        Ok(())
578    }
579
580    /// # Errors
581    ///
582    /// Throws [`ConfigError`] if something goes wrong during parsing
583    #[inline]
584    pub fn all(&self) -> Result<Vec<SubtreeConfig>, ConfigError> {
585        Ok(self.configs.clone())
586    }
587
588    /// Returns the repository head commit id
589    #[must_use]
590    #[inline]
591    pub fn head(&self) -> Option<String> {
592        Some(self.repo.head())
593    }
594
595    fn persist(&self, subtree: &SubtreeConfig) -> Result<(), ConfigSetError> {
596        let root = self.repo.work_tree().expect("Repo without work_tree");
597        let file = root.join(subtree.config_file());
598        let section = subtree.name();
599        let mut has_written = false;
600
601        if let Some(value) = subtree.follow() {
602            let key = format!("{}.follow", section);
603            git_wrapper::config_file_set(&file, &key, value)?;
604            has_written = true;
605        }
606
607        if let Some(value) = subtree.origin() {
608            let key = format!("{}.origin", section);
609            git_wrapper::config_file_set(&file, &key, value)?;
610            has_written = true;
611        }
612
613        if let Some(value) = subtree.upstream() {
614            let key = format!("{}.upstream", section);
615            git_wrapper::config_file_set(&file, &key, value)?;
616            has_written = true;
617        }
618
619        if *subtree.pull_pre_releases() {
620            let key = format!("{}.pull_pre_releases", section);
621            git_wrapper::config_file_set(&file, &key, "true")?;
622            has_written = true;
623        }
624
625        if !has_written {
626            let key = format!("{}.version", section);
627            git_wrapper::config_file_set(&file, &key, "1")?;
628        }
629        Ok(())
630    }
631
632    /// Pull remote changes in the specified subtree
633    #[inline]
634    pub fn pull(&self, subtree: &SubtreeConfig, git_ref: &str) -> Result<String, PullError> {
635        let prefix = subtree.id();
636        let remote = subtree
637            .upstream()
638            .as_ref()
639            .or_else(|| subtree.origin().as_ref())
640            .ok_or(PullError::NoUpstream)?;
641
642        let message = format!("Update :{} to {}", prefix, &git_ref);
643        let head_before = self.repo.head();
644        self.repo.subtree_pull(remote, prefix, git_ref, &message)?;
645        let head_after = self.repo.head();
646        if head_before == head_after {
647            return Err(PullError::NoChanges);
648        }
649        let mut cmd = self.repo.git();
650        let out = cmd
651            .arg("rev-parse")
652            .arg("--short")
653            .arg("HEAD^2")
654            .output()
655            .expect("Got second parent");
656        if out.status.success() {
657            Ok(String::from_utf8(out.stdout)
658                .expect("UTF-8 encoding")
659                .trim()
660                .to_owned())
661        } else {
662            Err(PullError::Failure(
663                "Failed to execute git rev-parse".to_owned(),
664            ))
665        }
666    }
667
668    /// Split changes in a subtree to own artificial history and merge it back into HEAD
669    #[inline]
670    pub fn split(&self, subtree: &SubtreeConfig) -> Result<(), SplitError> {
671        let prefix = subtree.id();
672        Ok(self.repo.subtree_split(prefix)?)
673    }
674
675    /// Push subtree changes to remote
676    #[inline]
677    pub fn push(&self, subtree: &SubtreeConfig, git_ref: &str) -> Result<(), PushError> {
678        let prefix = subtree.id();
679        let remote = subtree.origin().as_ref().ok_or(PushError::NoUpstream)?;
680
681        if git_ref == "HEAD" {
682            let head = git_wrapper::resolve_head(remote).expect("asd");
683            Ok(self.repo.subtree_push(remote, prefix, &head)?)
684        } else {
685            Ok(self.repo.subtree_push(remote, prefix, git_ref)?)
686        }
687    }
688
689    /// List modules with changes since specified git commit id
690    #[inline]
691    pub fn changed_modules(&self, id: &str) -> Result<Vec<SubtreeConfig>, ConfigError> {
692        let subtree_modules = self.all()?;
693        if subtree_modules.is_empty() {
694            return Ok(vec![]);
695        }
696        let revision = format!("{}~1..{}", id, id);
697        let mut args = vec![
698            "diff",
699            &revision,
700            "--name-only",
701            "--no-renames",
702            "--no-color",
703            "--",
704        ];
705        for s in &subtree_modules {
706            args.push(&s.id);
707        }
708        let proc = self
709            .repo
710            .git()
711            .args(args)
712            .output()
713            .expect("Failed running git diff");
714        if !proc.status.success() {
715            return Ok(vec![]);
716        }
717
718        let mut result = Vec::new();
719        let text = String::from_utf8_lossy(&proc.stdout);
720        let changed: Vec<&str> = text.lines().collect();
721        for f in &changed {
722            for d in subtree_modules.iter().rev() {
723                if f.starts_with(d.id.as_str()) {
724                    result.push(d.clone());
725                    break;
726                }
727            }
728        }
729
730        result.dedup();
731        Ok(result)
732    }
733
734    /// Find subtree by name
735    #[allow(clippy::missing_panics_doc)]
736    #[inline]
737    pub fn find_subtree(&self, needle: &str) -> Result<SubtreeConfig, FindError> {
738        let configs = self.all()?;
739        for c in configs {
740            if c.id() == needle {
741                return Ok(c);
742            }
743        }
744        Err(FindError::NotFound(needle.to_owned()))
745    }
746}
747
748fn configs_from_path(
749    repo: &Repository,
750    parser: &mut Ini,
751    path: &Path,
752) -> Result<Vec<SubtreeConfig>, ConfigError> {
753    let content = repo
754        .hack_read_file(path)
755        .map(|vec| String::from_utf8_lossy(&vec).to_string())?;
756    let msg = &format!("Failed to parse {:?}", path);
757    let config_map = parser.read(content).expect(msg);
758    let parent_dir = path.parent();
759    let mut result = Vec::with_capacity(config_map.keys().len());
760    for name in config_map.keys() {
761        let id: String = parent_dir.map_or_else(
762            || name.clone(),
763            |parent| {
764                parent
765                    .join(name)
766                    .to_str()
767                    .expect("Convertable to str")
768                    .to_owned()
769            },
770        );
771        result.push(SubtreeConfig {
772            id,
773            follow: parser.get(name, "follow"),
774            origin: parser.get(name, "origin"),
775            upstream: parser.get(name, "upstream"),
776            pull_pre_releases: parser
777                .getbool(name, "pull-pre-releases")
778                .unwrap_or_default()
779                .unwrap_or(false),
780        });
781    }
782    Ok(result)
783}
784
785fn config_files(repo: &Repository) -> Vec<PathBuf> {
786    let mut cmd = repo.git();
787    cmd.arg("ls-files").args(&[
788        "-z",
789        "--cached",
790        "--deleted",
791        "--",
792        ".gitsubtrees",
793        "**/.gitsubtrees",
794    ]);
795    let out = cmd.output().expect("Successful git-ls-files(1) invocation");
796    let tmp = String::from_utf8(out.stdout).expect("UTF-8 encoding");
797    let files: Vec<&str> = tmp.split('\0').filter(|e| !e.is_empty()).collect();
798    let mut result: Vec<PathBuf> = Vec::with_capacity(files.len());
799    for line in files {
800        result.push(PathBuf::from(line));
801    }
802
803    result
804}
805
806fn all(repo: &Repository) -> Result<Vec<SubtreeConfig>, ConfigError> {
807    let config_paths = config_files(repo);
808    let mut result = vec![];
809    let mut config_parser = Ini::new_cs();
810    for path in config_paths {
811        let mut tmp = configs_from_path(repo, &mut config_parser, &path)?;
812        result.append(&mut tmp);
813    }
814
815    Ok(result)
816}
817
818#[cfg(test)]
819mod test {
820    use crate::SubtreeConfig;
821    use crate::Subtrees;
822    use git_wrapper::Repository;
823
824    use tempfile::TempDir;
825
826    #[test]
827    fn bkg_monorepo() {
828        let subtrees = Subtrees::new().unwrap();
829        {
830            let result = subtrees.all();
831            assert!(result.is_ok(), "Found subtree configs");
832            let all_configs = result.unwrap();
833            assert!(
834                all_configs.len() > 50,
835                "Sould find at least 50 subtrees, found: {}",
836                all_configs.len()
837            );
838        }
839
840        // TODO rewrite it as a test for
841        {
842            let expected = "rust/git-wrapper";
843            let result = subtrees.find_subtree(expected);
844            assert!(result.is_ok(), "Found subtree rust/git-wrapper subtree");
845            let gsi_subtree = result.unwrap();
846            let actual = gsi_subtree.id();
847            assert!(
848                actual == expected,
849                "Expected subtree id {}, got {}",
850                expected,
851                actual
852            );
853        }
854    }
855
856    /*#[test]
857    fn initialization() {
858        let tmp_dir = TempDir::new().unwrap();
859        let repo_path = tmp_dir.path();
860        let _ = BareRepository::create(repo_path).expect("Created bare repository");
861        let actual = Subtrees::from_dir(&repo_path);
862        assert!(actual.is_ok(), "Expected a subtrees instance");
863    }*/
864
865    #[test]
866    fn subtree_add() {
867        let tmp_dir = TempDir::new().unwrap();
868        let repo_path = tmp_dir.path();
869        {
870            git_wrapper::setup_test_author();
871            let repo = Repository::create(repo_path).expect("Created repository");
872            let readme = repo_path.join("README.md");
873            std::fs::File::create(&readme).unwrap();
874            std::fs::write(&readme, "# README").unwrap();
875            repo.stage(&readme).unwrap();
876            repo.commit("Test").unwrap();
877        }
878        let mgr = Subtrees::from_dir(repo_path).unwrap();
879        let config = SubtreeConfig {
880            id: "bar".to_owned(),
881            follow: Some("master".to_owned()),
882            origin: None,
883            upstream: Some("https://github.com/kalkin/file-expert".to_owned()),
884            pull_pre_releases: false,
885        };
886        let actual = mgr.add(&config, Some("master"), None);
887        assert!(actual.is_ok(), "Expected a subtrees instance");
888    }
889
890    #[test]
891    fn subtree_pull() {
892        let tmp_dir = TempDir::new().unwrap();
893        let repo_path = tmp_dir.path();
894        {
895            git_wrapper::setup_test_author();
896            let repo = Repository::create(repo_path).expect("Created repository");
897            let readme = repo_path.join("README.md");
898            std::fs::File::create(&readme).unwrap();
899            std::fs::write(&readme, "# README").unwrap();
900            repo.stage(&readme).unwrap();
901            repo.commit("Test").unwrap();
902        }
903        let mgr = Subtrees::from_dir(repo_path).unwrap();
904        let config = SubtreeConfig {
905            id: "bar".to_owned(),
906            follow: Some("v0.10.1".to_owned()),
907            origin: None,
908            upstream: Some("https://github.com/kalkin/file-expert".to_owned()),
909            pull_pre_releases: false,
910        };
911        mgr.add(&config, Some("v0.10.1"), None).unwrap();
912        let actual = mgr.pull(&config, "v0.13.1");
913        assert!(
914            actual.is_ok(),
915            "Expected successful pull execution, got: {:?}",
916            actual
917        );
918    }
919}