1use 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#[derive(Getters, Clone, Debug, Eq, PartialEq)]
32pub struct SubtreeConfig {
33 #[getset(get = "pub")]
35 id: String,
36 #[getset(get = "pub")]
38 follow: Option<String>,
39 #[getset(get = "pub")]
41 origin: Option<String>,
42 #[getset(get = "pub")]
44 upstream: Option<String>,
45 #[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 #[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 #[must_use]
85 #[inline]
86 pub const fn is_pullable(&self) -> bool {
87 self.upstream.is_some() || self.origin.is_some()
88 }
89
90 #[must_use]
92 #[inline]
93 pub const fn is_pushable(&self) -> bool {
94 self.origin.is_some()
95 }
96
97 #[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 #[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 #[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#[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#[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#[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#[derive(Debug)]
269pub struct Subtrees {
270 repo: Repository,
271 configs: Vec<SubtreeConfig>,
272}
273
274#[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#[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#[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#[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#[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#[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#[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 #[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 #[inline]
521 pub fn from_repo(repo: Repository) -> Result<Self, SubtreesError> {
522 let configs = all(&repo)?;
523 Ok(Self { repo, configs })
524 }
525
526 #[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 #[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 #[inline]
584 pub fn all(&self) -> Result<Vec<SubtreeConfig>, ConfigError> {
585 Ok(self.configs.clone())
586 }
587
588 #[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 #[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 #[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 #[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 #[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 #[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 {
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]
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}