1use crate::publish::{DescriptionFormat, Error as PublishError, PublishResult};
3use breezyshim::branch::{Branch, GenericBranch};
4use breezyshim::controldir::ControlDirFormat;
5use breezyshim::error::Error as BrzError;
6use breezyshim::forge::{Forge, MergeProposal};
7use breezyshim::repository::Repository;
8use breezyshim::tree::{MutableTree, RevisionTree, WorkingTree};
9use breezyshim::ControlDir;
10use breezyshim::RevisionId;
11use std::collections::HashMap;
12use std::path::PathBuf;
13
14fn fetch_colocated(
15 _controldir: &dyn ControlDir<
16 Branch = GenericBranch,
17 Repository = breezyshim::repository::GenericRepository,
18 WorkingTree = breezyshim::workingtree::GenericWorkingTree,
19 >,
20 from_controldir: &dyn ControlDir<
21 Branch = GenericBranch,
22 Repository = breezyshim::repository::GenericRepository,
23 WorkingTree = breezyshim::workingtree::GenericWorkingTree,
24 >,
25 additional_colocated_branches: &HashMap<&str, &str>,
26) -> Result<(), BrzError> {
27 log::debug!(
28 "Fetching colocated branches: {:?}",
29 additional_colocated_branches
30 );
31
32 for (from_branch_name, to_branch_name) in additional_colocated_branches.iter() {
33 match from_controldir.open_branch(Some(from_branch_name)) {
34 Ok(remote_colo_branch) => {
35 match _controldir.push_branch(
37 remote_colo_branch.as_ref(),
38 Some(to_branch_name),
39 None, Some(false), None, ) {
43 Ok(_) => {
44 log::debug!(
45 "Successfully fetched colocated branch {} -> {}",
46 from_branch_name,
47 to_branch_name
48 );
49 }
50 Err(e) => {
51 log::warn!(
52 "Failed to fetch colocated branch {} -> {}: {}",
53 from_branch_name,
54 to_branch_name,
55 e
56 );
57 }
58 }
59 }
60 Err(BrzError::NotBranchError(..)) | Err(BrzError::NoColocatedBranchSupport) => {
61 continue;
62 }
63 Err(e) => {
64 return Err(e);
65 }
66 }
67 }
68 Ok(())
69}
70
71#[derive(Debug)]
72pub enum Error {
74 BrzError(BrzError),
76
77 IOError(std::io::Error),
79
80 UnknownFormat(String),
82
83 PermissionDenied(Option<String>),
85
86 Other(String),
88}
89
90impl From<BrzError> for Error {
91 fn from(e: BrzError) -> Self {
92 match e {
93 BrzError::UnknownFormat(n) => Error::UnknownFormat(n),
94 BrzError::AlreadyControlDir(_) => unreachable!(),
95 BrzError::PermissionDenied(_, m) => Error::PermissionDenied(m),
96 e => Error::BrzError(e),
97 }
98 }
99}
100
101impl From<std::io::Error> for Error {
102 fn from(e: std::io::Error) -> Self {
103 Error::IOError(e)
104 }
105}
106
107impl From<PublishError> for Error {
108 fn from(e: PublishError) -> Self {
109 match e {
110 PublishError::Other(e) => Error::BrzError(e),
111 e => Error::Other(format!("{:?}", e)),
112 }
113 }
114}
115
116impl From<crate::vcs::BranchOpenError> for Error {
117 fn from(e: crate::vcs::BranchOpenError) -> Self {
118 Error::Other(e.to_string())
119 }
120}
121
122impl std::fmt::Display for Error {
123 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
124 match self {
125 Error::IOError(e) => write!(f, "{}", e),
126 Error::UnknownFormat(n) => write!(f, "Unknown format: {}", n),
127 Error::BrzError(e) => write!(f, "{}", e),
128 Error::PermissionDenied(m) => write!(f, "Permission denied: {:?}", m),
129 Error::Other(e) => write!(f, "{}", e),
130 }
131 }
132}
133
134#[derive(Default)]
135pub struct WorkspaceBuilder {
137 main_branch: Option<GenericBranch>,
138 resume_branch: Option<GenericBranch>,
139 cached_branch: Option<GenericBranch>,
140 additional_colocated_branches: HashMap<String, String>,
141 resume_branch_additional_colocated_branches: HashMap<String, String>,
142 dir: Option<PathBuf>,
143 path: Option<PathBuf>,
144 format: Option<ControlDirFormat>,
145}
146
147impl WorkspaceBuilder {
148 pub fn main_branch(mut self, main_branch: GenericBranch) -> Self {
150 self.main_branch = Some(main_branch);
151 self
152 }
153
154 pub fn resume_branch(mut self, resume_branch: GenericBranch) -> Self {
156 self.resume_branch = Some(resume_branch);
157 self
158 }
159
160 pub fn cached_branch(mut self, cached_branch: GenericBranch) -> Self {
162 self.cached_branch = Some(cached_branch);
163 self
164 }
165
166 pub fn additional_colocated_branches(
168 mut self,
169 additional_colocated_branches: HashMap<String, String>,
170 ) -> Self {
171 self.additional_colocated_branches = additional_colocated_branches;
172 self
173 }
174
175 pub fn resume_branch_additional_colocated_branches(
177 mut self,
178 resume_branch_additional_colocated_branches: HashMap<String, String>,
179 ) -> Self {
180 self.resume_branch_additional_colocated_branches =
181 resume_branch_additional_colocated_branches;
182 self
183 }
184
185 pub fn dir(mut self, dir: PathBuf) -> Self {
187 self.dir = Some(dir);
188 self
189 }
190
191 pub fn path(mut self, path: PathBuf) -> Self {
193 self.path = Some(path);
194 self
195 }
196
197 pub fn format(mut self, format: impl breezyshim::controldir::AsFormat) -> Self {
201 self.format = format.as_format();
202 self
203 }
204
205 pub fn build(self) -> Result<Workspace, Error> {
207 let mut ws = Workspace {
208 main_branch: self.main_branch,
209 resume_branch: self.resume_branch,
210 cached_branch: self.cached_branch,
211 additional_colocated_branches: self.additional_colocated_branches,
212 resume_branch_additional_colocated_branches: self
213 .resume_branch_additional_colocated_branches,
214 path: self.path,
215 dir: self.dir,
216 format: self.format,
217 state: None,
218 };
219
220 ws.start()?;
221 Ok(ws)
222 }
223}
224
225struct WorkspaceState {
226 base_revid: RevisionId,
227 local_tree: breezyshim::workingtree::GenericWorkingTree,
228 refreshed: bool,
229 tempdir: Option<tempfile::TempDir>,
230 main_colo_revid: HashMap<String, RevisionId>,
231}
232
233pub struct Workspace {
235 main_branch: Option<GenericBranch>,
236 cached_branch: Option<GenericBranch>,
237 resume_branch: Option<GenericBranch>,
238 additional_colocated_branches: HashMap<String, String>,
239 resume_branch_additional_colocated_branches: HashMap<String, String>,
240 dir: Option<PathBuf>,
241 path: Option<PathBuf>,
242 state: Option<WorkspaceState>,
243 format: Option<breezyshim::controldir::ControlDirFormat>,
244}
245
246impl Workspace {
247 pub fn temporary() -> Result<Self, Error> {
249 let td = tempfile::tempdir().unwrap();
250 let path = td.path().to_path_buf();
251 let _ = td.keep(); Self::builder().dir(path).build()
253 }
254
255 pub fn from_url(url: &url::Url) -> Result<Self, Error> {
257 let branch = crate::vcs::open_branch(url, None, None, None)?;
258 Self::builder().main_branch(branch).build()
259 }
260
261 fn start(&mut self) -> Result<(), Error> {
263 if self.state.is_some() {
264 panic!("Workspace already started");
265 }
266 let mut td: Option<tempfile::TempDir> = None;
267 let (sprout_base, sprout_coloc) = if let Some(cache_branch) = self.cached_branch.as_ref() {
269 (Some(cache_branch), &self.additional_colocated_branches)
270 } else if let Some(resume_branch) = self.resume_branch.as_ref() {
271 (
272 Some(resume_branch),
273 &self.resume_branch_additional_colocated_branches,
274 )
275 } else {
276 (
277 self.main_branch.as_ref(),
278 &self.additional_colocated_branches,
279 )
280 };
281
282 let (local_tree, td) = if let Some(sprout_base) = sprout_base {
283 log::debug!("Creating sprout from {}", sprout_base.get_user_url());
284 let (wt, td) = crate::utils::create_temp_sprout(
285 sprout_base,
286 Some(
287 sprout_coloc
288 .iter()
289 .map(|(k, v)| (k.clone(), v.clone()))
290 .collect(),
291 ),
292 self.dir.as_deref(),
293 self.path.as_deref(),
294 )?;
295 (wt, td)
296 } else {
297 if let Some(format) = self.format.as_ref() {
298 log::debug!(
299 "Creating new empty tree with format {}",
300 format.get_format_description()
301 );
302 } else {
303 log::debug!("Creating new empty tree");
304 };
305
306 let tp = if let Some(path) = self.path.as_deref() {
307 std::fs::create_dir_all(path)?;
308 path.to_path_buf()
309 } else {
310 td = Some(if let Some(dir) = self.dir.as_ref() {
311 tempfile::tempdir_in(dir)?
312 } else {
313 tempfile::tempdir()?
314 });
315 td.as_ref().unwrap().path().to_path_buf()
316 };
317 (
318 breezyshim::controldir::create_standalone_workingtree(
319 tp.as_path(),
320 self.format
321 .as_ref()
322 .unwrap_or(&breezyshim::controldir::ControlDirFormat::default()),
323 )?,
324 td,
325 )
326 };
327
328 if let Some(path) = self.path.as_ref() {
329 breezyshim::clean_tree::clean_tree(path, true, true, true, false, true)?;
330 }
331
332 let mut main_colo_revid = std::collections::HashMap::new();
333
334 let mut refreshed = false;
335
336 if let Some(main_branch) = self.main_branch.as_ref() {
338 for (from_name, _to_name) in self.additional_colocated_branches.iter() {
339 match main_branch.controldir().open_branch(Some(from_name)) {
340 Ok(branch) => {
341 main_colo_revid.insert(from_name.to_string(), branch.last_revision());
342 }
343 Err(BrzError::NotBranchError(..)) => {}
344 Err(BrzError::NoColocatedBranchSupport) => {}
345 Err(e) => {
346 log::warn!("Failed to open colocated branch {}: {}", from_name, e);
347 }
348 }
349 }
350
351 if let Some(cached_branch) = self.cached_branch.as_ref() {
352 log::debug!(
353 "Pulling in missing revisions from resume/main branch {:?}",
354 cached_branch.get_user_url()
355 );
356
357 let from_branch = if let Some(resume_branch) = self.resume_branch.as_ref() {
358 resume_branch
359 } else {
360 main_branch
361 };
362
363 match local_tree.pull(from_branch, Some(true), None, None) {
364 Ok(_) => {}
365 Err(BrzError::DivergedBranches) => {
366 unreachable!();
367 }
368 Err(e) => {
369 return Err(e.into());
370 }
371 }
372
373 assert_eq!(
374 local_tree.last_revision().unwrap(),
375 main_branch.last_revision()
376 );
377 }
378
379 if let Some(resume_branch) = self.resume_branch.as_ref() {
382 log::debug!(
385 "Pulling in missing revisions from main branch {:?}",
386 main_branch.get_user_url()
387 );
388
389 match local_tree.pull(main_branch, Some(false), None, None) {
390 Err(BrzError::DivergedBranches) => {
391 log::info!("restarting branch");
392 refreshed = true;
393 self.resume_branch = None;
394 self.resume_branch_additional_colocated_branches.clear();
395 match local_tree.pull(main_branch, Some(true), None, None) {
396 Ok(_) => {}
397 Err(BrzError::DivergedBranches) => {
398 unreachable!();
399 }
400 Err(e) => {
401 return Err(e.into());
402 }
403 }
404 fetch_colocated(
405 local_tree.branch().controldir().as_ref(),
406 main_branch.controldir().as_ref(),
407 &self
408 .additional_colocated_branches
409 .iter()
410 .map(|(k, v)| (k.as_str(), v.as_str()))
411 .collect(),
412 )?;
413 }
414 Ok(_) => {
415 fetch_colocated(
416 local_tree.branch().controldir().as_ref(),
417 main_branch.controldir().as_ref(),
418 &self
419 .additional_colocated_branches
420 .iter()
421 .map(|(k, v)| (k.as_str(), v.as_str()))
422 .collect(),
423 )?;
424
425 if !self.resume_branch_additional_colocated_branches.is_empty() {
426 fetch_colocated(
427 local_tree.branch().controldir().as_ref(),
428 resume_branch.controldir().as_ref(),
429 &self
430 .resume_branch_additional_colocated_branches
431 .iter()
432 .map(|(k, v)| (k.as_str(), v.as_str()))
433 .collect(),
434 )?;
435
436 self.additional_colocated_branches
437 .extend(self.resume_branch_additional_colocated_branches.drain());
438 }
439 }
440 Err(e) => {
441 log::warn!("Failed to pull from main branch: {}", e);
442 }
443 }
444 } else {
445 fetch_colocated(
446 local_tree.branch().controldir().as_ref(),
447 main_branch.controldir().as_ref(),
448 &self
449 .additional_colocated_branches
450 .iter()
451 .map(|(k, v)| (k.as_str(), v.as_str()))
452 .collect(),
453 )?;
454 }
455 }
456
457 self.state = Some(WorkspaceState {
458 base_revid: local_tree.last_revision().unwrap(),
459 local_tree,
460 refreshed,
461 main_colo_revid,
462 tempdir: td,
463 });
464
465 Ok(())
466 }
467
468 fn state(&self) -> &WorkspaceState {
470 self.state.as_ref().unwrap()
471 }
472
473 pub fn builder() -> WorkspaceBuilder {
475 WorkspaceBuilder::default()
476 }
477
478 pub fn main_branch(&self) -> Option<&GenericBranch> {
480 self.main_branch.as_ref()
481 }
482
483 pub fn set_main_branch(&mut self, branch: GenericBranch) -> Result<(), Error> {
485 self.main_branch = Some(branch);
486 Ok(())
487 }
488
489 pub fn local_tree(&self) -> &breezyshim::workingtree::GenericWorkingTree {
491 &self.state().local_tree
492 }
493
494 pub fn refreshed(&self) -> bool {
498 self.state().refreshed
499 }
500
501 pub fn resume_branch(&self) -> Option<&GenericBranch> {
503 self.resume_branch.as_ref()
504 }
505
506 pub fn path(&self) -> PathBuf {
508 self.local_tree()
509 .abspath(std::path::Path::new("."))
510 .unwrap()
511 }
512
513 pub fn changes_since_main(&self) -> bool {
515 Some(self.local_tree().branch().last_revision())
516 != self.main_branch().map(|b| b.last_revision())
517 }
518
519 pub fn changes_since_base(&self) -> bool {
521 self.base_revid() != Some(&self.local_tree().branch().last_revision())
522 }
523
524 pub fn base_revid(&self) -> Option<&RevisionId> {
526 self.state.as_ref().map(|s| &s.base_revid)
527 }
528
529 pub fn any_branch_changes(&self) -> bool {
533 self.changed_branches().iter().any(|(_, br, r)| br != r)
534 }
535
536 pub fn additional_colocated_branches(&self) -> &HashMap<String, String> {
538 &self.additional_colocated_branches
539 }
540
541 pub fn changed_branches(&self) -> Vec<(String, Option<RevisionId>, Option<RevisionId>)> {
543 let main_branch = self.main_branch();
544 let mut branches = vec![(
545 main_branch
546 .as_ref()
547 .map_or_else(|| "".to_string(), |b| b.name().unwrap()),
548 main_branch.map(|b| b.last_revision()),
549 Some(self.local_tree().last_revision().unwrap()),
550 )];
551
552 let local_controldir = self.local_tree().controldir();
553
554 for (from_name, to_name) in self.additional_colocated_branches().iter() {
555 let to_revision = match local_controldir.open_branch(Some(to_name)) {
556 Ok(b) => Some(b.last_revision()),
557 Err(BrzError::NoColocatedBranchSupport) => continue,
558 Err(BrzError::NotBranchError(..)) => None,
559 Err(e) => {
560 panic!("Unexpected error opening branch {}: {}", to_name, e);
561 }
562 };
563
564 let from_revision = self.main_colo_revid().get(from_name).cloned();
565
566 branches.push((from_name.to_string(), from_revision, to_revision));
567 }
568
569 branches
570 }
571
572 pub fn main_colo_revid(&self) -> &HashMap<String, RevisionId> {
574 &self.state().main_colo_revid
575 }
576
577 pub fn base_tree(&self) -> Result<Box<RevisionTree>, BrzError> {
579 let base_revid = &self.state().base_revid;
580 match self.state().local_tree.revision_tree(base_revid) {
581 Ok(t) => Ok(t),
582 Err(BrzError::NoSuchRevisionInTree(revid)) => {
583 Ok(Box::new(
585 self.local_tree()
586 .branch()
587 .repository()
588 .revision_tree(&revid)?,
589 ))
590 }
591 Err(e) => Err(e),
592 }
593 }
594
595 pub fn defer_destroy(&mut self) -> std::path::PathBuf {
597 let tempdir = self.state.as_mut().unwrap().tempdir.take().unwrap();
598
599 tempdir.keep()
600 }
601
602 pub fn publish_changes(
604 &self,
605 target_branch: Option<&GenericBranch>,
606 mode: crate::Mode,
607 name: &str,
608 get_proposal_description: impl FnOnce(DescriptionFormat, Option<&MergeProposal>) -> String,
609 get_proposal_commit_message: Option<impl FnOnce(Option<&MergeProposal>) -> Option<String>>,
610 get_proposal_title: Option<impl FnOnce(Option<&MergeProposal>) -> Option<String>>,
611 forge: Option<&Forge>,
612 allow_create_proposal: Option<bool>,
613 labels: Option<Vec<String>>,
614 overwrite_existing: Option<bool>,
615 existing_proposal: Option<MergeProposal>,
616 reviewers: Option<Vec<String>>,
617 tags: Option<HashMap<String, RevisionId>>,
618 derived_owner: Option<&str>,
619 allow_collaboration: Option<bool>,
620 stop_revision: Option<&RevisionId>,
621 auto_merge: Option<bool>,
622 work_in_progress: Option<bool>,
623 ) -> Result<PublishResult, PublishError> {
624 let main_branch = self.main_branch();
625 crate::publish::publish_changes(
626 &self.local_tree().branch(),
627 target_branch.or(main_branch).unwrap(),
628 self.resume_branch(),
629 mode,
630 name,
631 get_proposal_description,
632 get_proposal_commit_message,
633 get_proposal_title,
634 forge,
635 allow_create_proposal,
636 labels,
637 overwrite_existing,
638 existing_proposal,
639 reviewers,
640 tags,
641 derived_owner,
642 allow_collaboration,
643 stop_revision,
644 auto_merge,
645 work_in_progress,
646 )
647 }
648
649 pub fn propose(
651 &self,
652 name: &str,
653 description: &str,
654 target_branch: Option<&GenericBranch>,
655 forge: Option<Forge>,
656 existing_proposal: Option<MergeProposal>,
657 tags: Option<HashMap<String, RevisionId>>,
658 labels: Option<Vec<String>>,
659 overwrite_existing: Option<bool>,
660 commit_message: Option<&str>,
661 allow_collaboration: Option<bool>,
662 title: Option<&str>,
663 allow_empty: Option<bool>,
664 reviewers: Option<Vec<String>>,
665 owner: Option<&str>,
666 auto_merge: Option<bool>,
667 work_in_progress: Option<bool>,
668 ) -> Result<(MergeProposal, bool), Error> {
669 let main_branch = self.main_branch();
670 let target_branch = target_branch.or(main_branch).unwrap();
671 let forge = if let Some(forge) = forge {
672 forge
673 } else {
674 match breezyshim::forge::get_forge(target_branch) {
676 Ok(forge) => forge,
677 Err(e) => return Err(Error::BrzError(e)),
678 }
679 };
680 crate::publish::propose_changes(
681 &self.local_tree().branch(),
682 target_branch,
683 &forge,
684 name,
685 description,
686 self.resume_branch(),
687 existing_proposal,
688 overwrite_existing,
689 labels,
690 commit_message,
691 title,
692 Some(self.inverse_additional_colocated_branches()),
693 allow_empty,
694 reviewers,
695 tags,
696 owner,
697 None,
698 allow_collaboration,
699 auto_merge,
700 work_in_progress,
701 )
702 .map_err(|e| e.into())
703 }
704
705 pub fn push_derived(
707 &self,
708 name: &str,
709 target_branch: Option<&GenericBranch>,
710 forge: Option<Forge>,
711 tags: Option<HashMap<String, RevisionId>>,
712 overwrite_existing: Option<bool>,
713 owner: Option<&str>,
714 ) -> Result<(Box<dyn Branch>, url::Url), Error> {
715 let main_branch = self.main_branch();
716 let target_branch = target_branch.or(main_branch).unwrap();
717 let forge = if let Some(forge) = forge {
718 forge
719 } else {
720 match breezyshim::forge::get_forge(target_branch) {
722 Ok(forge) => forge,
723 Err(e) => return Err(Error::BrzError(e)),
724 }
725 };
726 crate::publish::push_derived_changes(
727 &self.local_tree().branch(),
728 target_branch,
729 &forge,
730 name,
731 overwrite_existing,
732 owner,
733 tags,
734 None,
735 )
736 .map_err(|e| e.into())
737 }
738
739 pub fn push_tags(&self, tags: HashMap<String, RevisionId>) -> Result<(), Error> {
741 self.push(Some(tags))
742 }
743
744 pub fn push(&self, tags: Option<HashMap<String, RevisionId>>) -> Result<(), Error> {
746 let main_branch = self.main_branch().unwrap();
747
748 let forge = match breezyshim::forge::get_forge(main_branch) {
750 Ok(forge) => Some(forge),
751 Err(breezyshim::error::Error::UnsupportedForge(e)) => {
752 log::warn!(
755 "Unsupported forge ({}), will attempt to push to {}",
756 e,
757 crate::vcs::full_branch_url(main_branch),
758 );
759 None
760 }
761 Err(e) => {
762 return Err(e.into());
763 }
764 };
765
766 crate::publish::push_changes(
767 &self.local_tree().branch(),
768 &self.local_tree().branch(), forge.as_ref(),
770 None,
771 Some(
772 self.inverse_additional_colocated_branches()
773 .into_iter()
774 .collect(),
775 ),
776 tags,
777 None,
778 )
779 .map_err(Into::into)
780 }
781
782 fn inverse_additional_colocated_branches(&self) -> Vec<(String, String)> {
783 self.additional_colocated_branches()
784 .iter()
785 .map(|(k, v)| (v.clone(), k.clone()))
786 .collect()
787 }
788
789 pub fn show_diff(
791 &self,
792 outf: Box<dyn std::io::Write + Send>,
793 old_label: Option<&str>,
794 new_label: Option<&str>,
795 ) -> Result<(), BrzError> {
796 let base_tree = self.base_tree()?;
797 let basis_tree = self.local_tree().basis_tree()?;
798 breezyshim::diff::show_diff_trees(
799 base_tree.as_ref(),
800 &basis_tree,
801 outf,
802 old_label,
803 new_label,
804 )
805 }
806
807 pub fn destroy(&mut self) -> Result<(), Error> {
809 self.state = None;
810 Ok(())
811 }
812}
813
814#[cfg(test)]
815mod tests {
816 use super::*;
817
818 use breezyshim::controldir::ControlDirFormat;
819 use breezyshim::testing::TestEnv;
820 use serial_test::serial;
821
822 #[test]
823 #[serial]
824 fn test_create_workspace() {
825 let _test_env = TestEnv::new();
826 let mut ws = Workspace::builder().build().unwrap();
827
828 assert_eq!(ws.local_tree().branch().name().as_ref().unwrap(), "");
829
830 assert_eq!(
831 ws.base_revid(),
832 Some(&breezyshim::revisionid::RevisionId::null())
833 );
834
835 assert!(ws.changes_since_main());
837 assert!(!ws.changes_since_base());
838 assert_eq!(
839 ws.changed_branches(),
840 vec![(
841 "".to_string(),
842 None,
843 Some(breezyshim::revisionid::RevisionId::null())
844 )]
845 );
846
847 let revid = ws
848 .local_tree()
849 .build_commit()
850 .message("test commit")
851 .allow_pointless(true)
852 .commit()
853 .unwrap();
854
855 assert!(ws.changes_since_main());
856 assert!(ws.changes_since_base());
857 assert_eq!(
858 ws.changed_branches(),
859 vec![("".to_string(), None, Some(revid))]
860 );
861
862 ws.destroy().unwrap();
863 }
864
865 #[test]
866 fn test_temporary() {
867 let ws = Workspace::temporary().unwrap();
868
869 assert_eq!(ws.local_tree().branch().name().as_ref().unwrap(), "");
870
871 assert_eq!(
872 ws.base_revid(),
873 Some(&breezyshim::revisionid::RevisionId::null())
874 );
875
876 assert!(ws.changes_since_main());
878 assert!(!ws.changes_since_base());
879 assert_eq!(
880 ws.changed_branches(),
881 vec![(
882 "".to_string(),
883 None,
884 Some(breezyshim::revisionid::RevisionId::null())
885 )]
886 );
887 }
888
889 #[test]
890 #[serial]
891 fn test_nascent() {
892 let _test_env = TestEnv::new();
893 let td = tempfile::tempdir().unwrap();
894 let origin = breezyshim::controldir::create_standalone_workingtree(
895 &td.path().join("origin"),
896 &ControlDirFormat::default(),
897 )
898 .unwrap();
899
900 let ws_dir = td.path().join("ws");
901 std::fs::create_dir(&ws_dir).unwrap();
902
903 let ws = Workspace::builder()
904 .main_branch(origin.branch())
905 .dir(ws_dir)
906 .build()
907 .unwrap();
908
909 assert!(!ws.changes_since_main());
910 assert!(!ws.any_branch_changes());
911 assert!(!ws.changes_since_base());
912 ws.local_tree()
913 .build_commit()
914 .message("A change")
915 .commit()
916 .unwrap();
917
918 assert_eq!(ws.path(), ws.local_tree().basedir().join("."));
919
920 assert!(ws.changes_since_main());
921 assert!(ws.changes_since_base());
922 assert!(ws.any_branch_changes());
923 assert_eq!(
924 vec![(
925 "".to_string(),
926 Some(breezyshim::revisionid::RevisionId::null()),
927 Some(ws.local_tree().last_revision().unwrap())
928 )],
929 ws.changed_branches()
930 );
931
932 std::mem::drop(td);
933 }
934
935 #[test]
936 #[serial]
937 fn test_without_main() {
938 let _test_env = TestEnv::new();
939 let td = tempfile::tempdir().unwrap();
940
941 let ws = Workspace::builder()
942 .dir(td.path().to_path_buf())
943 .build()
944 .unwrap();
945
946 assert!(ws.changes_since_main());
947 assert!(ws.any_branch_changes());
948 assert!(!ws.changes_since_base());
949 ws.local_tree()
950 .build_commit()
951 .message("A change")
952 .commit()
953 .unwrap();
954 assert!(ws.changes_since_main());
955 assert!(ws.changes_since_base());
956 assert!(ws.any_branch_changes());
957 assert_eq!(
958 vec![(
959 "".to_string(),
960 None,
961 Some(ws.local_tree().last_revision().unwrap())
962 )],
963 ws.changed_branches()
964 );
965 std::mem::drop(ws);
966 std::mem::drop(td);
967 }
968
969 #[test]
970 #[serial]
971 fn test_basic() {
972 let _test_env = TestEnv::new();
973 let td = tempfile::tempdir().unwrap();
974
975 let origin = breezyshim::controldir::create_standalone_workingtree(
976 &td.path().join("origin"),
977 &ControlDirFormat::default(),
978 )
979 .unwrap();
980
981 let revid1 = origin
982 .build_commit()
983 .message("first commit")
984 .commit()
985 .unwrap();
986
987 let ws_dir = td.path().join("ws");
988 std::fs::create_dir(&ws_dir).unwrap();
989
990 let ws = Workspace::builder()
991 .main_branch(origin.branch())
992 .dir(ws_dir)
993 .build()
994 .unwrap();
995
996 assert!(!ws.changes_since_main());
997 assert!(!ws.any_branch_changes());
998 assert!(!ws.changes_since_base());
999
1000 ws.local_tree()
1001 .build_commit()
1002 .message("A change")
1003 .commit()
1004 .unwrap();
1005
1006 assert!(ws.changes_since_main());
1007 assert!(ws.changes_since_base());
1008 assert!(ws.any_branch_changes());
1009 assert_eq!(
1010 vec![(
1011 "".to_string(),
1012 Some(revid1),
1013 Some(ws.local_tree().last_revision().unwrap())
1014 )],
1015 ws.changed_branches()
1016 );
1017 std::mem::drop(td);
1018 }
1019
1020 #[test]
1021 #[serial]
1022 fn test_cached_branch_up_to_date() {
1023 let _test_env = TestEnv::new();
1024 let td = tempfile::tempdir().unwrap();
1025
1026 let origin = breezyshim::controldir::create_standalone_workingtree(
1027 &td.path().join("origin"),
1028 &ControlDirFormat::default(),
1029 )
1030 .unwrap();
1031 let revid1 = origin
1032 .build_commit()
1033 .message("first commit")
1034 .commit()
1035 .unwrap();
1036
1037 let cached = origin
1038 .branch()
1039 .controldir()
1040 .sprout(
1041 url::Url::from_directory_path(td.path().join("cached")).unwrap(),
1042 None,
1043 None,
1044 None,
1045 None,
1046 )
1047 .unwrap();
1048
1049 let ws_dir = td.path().join("ws");
1050 std::fs::create_dir(&ws_dir).unwrap();
1051
1052 let ws = Workspace::builder()
1053 .main_branch(origin.branch())
1054 .cached_branch(*cached.open_branch(None).unwrap())
1055 .dir(ws_dir)
1056 .build()
1057 .unwrap();
1058
1059 assert!(!ws.changes_since_main());
1060 assert!(!ws.any_branch_changes());
1061 assert!(!ws.changes_since_base());
1062 assert_eq!(ws.local_tree().last_revision().unwrap(), revid1);
1063
1064 std::mem::drop(td);
1065 }
1066
1067 #[test]
1068 #[serial]
1069 fn test_cached_branch_out_of_date() {
1070 let _test_env = TestEnv::new();
1071 let td = tempfile::tempdir().unwrap();
1072
1073 let origin = breezyshim::controldir::create_standalone_workingtree(
1074 &td.path().join("origin"),
1075 &ControlDirFormat::default(),
1076 )
1077 .unwrap();
1078 origin
1079 .build_commit()
1080 .message("first commit")
1081 .commit()
1082 .unwrap();
1083
1084 let cached = origin
1085 .branch()
1086 .controldir()
1087 .sprout(
1088 url::Url::from_directory_path(td.path().join("cached")).unwrap(),
1089 None,
1090 None,
1091 None,
1092 None,
1093 )
1094 .unwrap();
1095
1096 let revid2 = origin
1097 .build_commit()
1098 .message("second commit")
1099 .commit()
1100 .unwrap();
1101
1102 let ws_dir = td.path().join("ws");
1103 std::fs::create_dir(&ws_dir).unwrap();
1104
1105 let ws = Workspace::builder()
1106 .main_branch(origin.branch())
1107 .cached_branch(*cached.open_branch(None).unwrap())
1108 .dir(ws_dir)
1109 .build()
1110 .unwrap();
1111
1112 assert!(!ws.changes_since_main());
1113 assert!(!ws.any_branch_changes());
1114 assert!(!ws.changes_since_base());
1115 assert_eq!(ws.local_tree().last_revision().unwrap(), revid2);
1116
1117 std::mem::drop(td);
1118 }
1119
1120 fn commit_on_colo<C: ControlDir + ?Sized>(
1121 controldir: &C,
1122 to_location: &std::path::Path,
1123 message: &str,
1124 ) -> RevisionId {
1125 let colo_branch = controldir.create_branch(Some("colo")).unwrap();
1126 let colo_checkout = colo_branch.create_checkout(to_location).unwrap();
1127
1128 colo_checkout
1129 .build_commit()
1130 .message(message)
1131 .commit()
1132 .unwrap()
1133 }
1134
1135 #[test]
1136 #[serial]
1137 fn test_colocated() {
1138 let _test_env = TestEnv::new();
1139 let td = tempfile::tempdir().unwrap();
1140
1141 let origin = breezyshim::controldir::create_standalone_workingtree(
1142 &td.path().join("origin"),
1143 &ControlDirFormat::default(),
1144 )
1145 .unwrap();
1146 let revid1 = origin.build_commit().message("main").commit().unwrap();
1147
1148 let colo_revid1 = commit_on_colo(
1149 &*origin.branch().controldir(),
1150 &td.path().join("colo"),
1151 "Another",
1152 );
1153
1154 assert_eq!(origin.branch().last_revision(), revid1);
1155
1156 let ws_dir = td.path().join("ws");
1157 std::fs::create_dir(&ws_dir).unwrap();
1158
1159 let ws = Workspace::builder()
1160 .main_branch(origin.branch())
1161 .dir(ws_dir)
1162 .additional_colocated_branches(
1163 vec![("colo".to_string(), "colo".to_string())]
1164 .into_iter()
1165 .collect(),
1166 )
1167 .build()
1168 .unwrap();
1169
1170 assert!(!ws.changes_since_main());
1171 assert!(!ws.any_branch_changes());
1172 assert!(!ws.changes_since_base());
1173
1174 ws.local_tree()
1175 .build_commit()
1176 .message("A change")
1177 .commit()
1178 .unwrap();
1179
1180 assert!(ws.changes_since_main());
1181 assert!(ws.changes_since_base());
1182 assert!(ws.any_branch_changes());
1183 assert_eq!(
1184 vec![
1185 (
1186 "".to_string(),
1187 Some(revid1),
1188 Some(ws.local_tree().last_revision().unwrap())
1189 ),
1190 (
1191 "colo".to_string(),
1192 Some(colo_revid1.clone()),
1193 Some(colo_revid1.clone())
1194 ),
1195 ],
1196 ws.changed_branches()
1197 );
1198 std::mem::drop(td);
1199 }
1200
1201 #[test]
1202 #[serial]
1203 fn test_resume_continue() {
1204 let _test_env = TestEnv::new();
1205 let td = tempfile::tempdir().unwrap();
1206
1207 let origin = breezyshim::controldir::create_standalone_workingtree(
1208 &td.path().join("origin"),
1209 &ControlDirFormat::default(),
1210 )
1211 .unwrap();
1212
1213 let revid1 = origin
1214 .build_commit()
1215 .message("first commit")
1216 .commit()
1217 .unwrap();
1218
1219 let resume = origin
1220 .branch()
1221 .controldir()
1222 .sprout(
1223 url::Url::from_directory_path(td.path().join("resume")).unwrap(),
1224 None,
1225 None,
1226 None,
1227 None,
1228 )
1229 .unwrap();
1230
1231 let resume_tree = resume.open_workingtree().unwrap();
1232
1233 let resume_revid1 = resume_tree
1234 .build_commit()
1235 .message("resume")
1236 .commit()
1237 .unwrap();
1238
1239 let ws_dir = td.path().join("ws");
1240 std::fs::create_dir(&ws_dir).unwrap();
1241
1242 let ws = Workspace::builder()
1243 .main_branch(origin.branch())
1244 .resume_branch(resume_tree.branch())
1245 .dir(ws_dir)
1246 .build()
1247 .unwrap();
1248
1249 assert!(ws.changes_since_main());
1250 assert!(ws.any_branch_changes());
1251 assert!(!ws.refreshed());
1252 assert!(!ws.changes_since_base());
1253
1254 assert_eq!(ws.local_tree().last_revision().unwrap(), resume_revid1);
1255 assert_eq!(
1256 vec![("".to_string(), Some(revid1), Some(resume_revid1))],
1257 ws.changed_branches()
1258 );
1259
1260 std::mem::drop(td);
1261 }
1262
1263 #[test]
1264 #[serial]
1265 fn test_resume_discard() {
1266 let _test_env = TestEnv::new();
1267 let td = tempfile::tempdir().unwrap();
1268
1269 let origin = breezyshim::controldir::create_standalone_workingtree(
1270 &td.path().join("origin"),
1271 &ControlDirFormat::default(),
1272 )
1273 .unwrap();
1274 origin
1275 .build_commit()
1276 .message("first commit")
1277 .commit()
1278 .unwrap();
1279
1280 let resume = origin
1281 .branch()
1282 .controldir()
1283 .sprout(
1284 url::Url::from_directory_path(td.path().join("resume")).unwrap(),
1285 None,
1286 None,
1287 None,
1288 None,
1289 )
1290 .unwrap();
1291 let revid2 = origin
1292 .build_commit()
1293 .message("second commit")
1294 .commit()
1295 .unwrap();
1296
1297 let resume_tree = resume.open_workingtree().unwrap();
1298 resume_tree
1299 .build_commit()
1300 .message("resume")
1301 .commit()
1302 .unwrap();
1303
1304 let ws_dir = td.path().join("ws");
1305 std::fs::create_dir(&ws_dir).unwrap();
1306
1307 let ws = Workspace::builder()
1308 .main_branch(origin.branch())
1309 .resume_branch(resume_tree.branch())
1310 .dir(ws_dir)
1311 .build()
1312 .unwrap();
1313
1314 assert!(!ws.changes_since_main());
1315 assert!(!ws.any_branch_changes());
1316 assert!(ws.refreshed());
1317
1318 assert!(!ws.changes_since_base());
1319 assert_eq!(ws.local_tree().last_revision().unwrap(), revid2);
1320
1321 assert_eq!(
1322 vec![("".to_string(), Some(revid2.clone()), Some(revid2.clone()))],
1323 ws.changed_branches()
1324 );
1325 std::mem::drop(td);
1326 }
1327
1328 #[test]
1329 #[serial]
1330 fn test_resume_continue_with_unchanged_colocated() {
1331 let _test_env = TestEnv::new();
1332 let td = tempfile::tempdir().unwrap();
1333
1334 let origin = breezyshim::controldir::create_standalone_workingtree(
1335 &td.path().join("origin"),
1336 &ControlDirFormat::default(),
1337 )
1338 .unwrap();
1339
1340 let revid1 = origin
1341 .build_commit()
1342 .message("first commit")
1343 .commit()
1344 .unwrap();
1345
1346 let colo_revid1 = commit_on_colo(
1347 &*origin.branch().controldir(),
1348 &td.path().join("colo"),
1349 "First colo",
1350 );
1351
1352 let resume = origin
1353 .branch()
1354 .controldir()
1355 .sprout(
1356 url::Url::from_directory_path(td.path().join("resume")).unwrap(),
1357 None,
1358 None,
1359 None,
1360 None,
1361 )
1362 .unwrap();
1363
1364 let resume_tree = resume.open_workingtree().unwrap();
1365
1366 let resume_revid1 = resume_tree
1367 .build_commit()
1368 .message("resume")
1369 .commit()
1370 .unwrap();
1371
1372 let ws_dir = td.path().join("ws");
1373 std::fs::create_dir(&ws_dir).unwrap();
1374
1375 let ws = Workspace::builder()
1376 .main_branch(origin.branch())
1377 .resume_branch(resume_tree.branch())
1378 .dir(ws_dir)
1379 .additional_colocated_branches(
1380 vec![("colo".to_string(), "colo".to_string())]
1381 .into_iter()
1382 .collect(),
1383 )
1384 .build()
1385 .unwrap();
1386
1387 assert!(ws.changes_since_main());
1388 assert!(ws.any_branch_changes());
1389 assert!(!ws.refreshed());
1390 assert!(!ws.changes_since_base());
1391 assert_eq!(ws.local_tree().last_revision().unwrap(), resume_revid1);
1392 assert_eq!(
1393 vec![
1394 ("".to_string(), Some(revid1), Some(resume_revid1)),
1395 (
1396 "colo".to_string(),
1397 Some(colo_revid1.clone()),
1398 Some(colo_revid1.clone())
1399 ),
1400 ],
1401 ws.changed_branches()
1402 );
1403 std::mem::drop(td);
1404 }
1405
1406 #[test]
1407 #[serial]
1408 fn test_resume_discard_with_unchanged_colocated() {
1409 let _test_env = TestEnv::new();
1410 let td = tempfile::tempdir().unwrap();
1411
1412 let origin = breezyshim::controldir::create_standalone_workingtree(
1413 &td.path().join("origin"),
1414 &ControlDirFormat::default(),
1415 )
1416 .unwrap();
1417
1418 origin
1419 .build_commit()
1420 .message("first commit")
1421 .commit()
1422 .unwrap();
1423
1424 let colo_revid1 = commit_on_colo(
1425 &*origin.branch().controldir(),
1426 &td.path().join("colo"),
1427 "First colo",
1428 );
1429
1430 let resume = origin
1431 .branch()
1432 .controldir()
1433 .sprout(
1434 url::Url::from_directory_path(td.path().join("resume")).unwrap(),
1435 None,
1436 None,
1437 None,
1438 None,
1439 )
1440 .unwrap();
1441
1442 commit_on_colo(
1443 resume.as_ref(),
1444 &td.path().join("resume-colo"),
1445 "First colo on resume",
1446 );
1447
1448 let revid2 = origin
1449 .build_commit()
1450 .message("second commit")
1451 .commit()
1452 .unwrap();
1453 let resume_tree = resume.open_workingtree().unwrap();
1454 resume_tree
1455 .build_commit()
1456 .message("resume")
1457 .commit()
1458 .unwrap();
1459
1460 let ws_dir = td.path().join("ws");
1461 std::fs::create_dir(&ws_dir).unwrap();
1462
1463 let ws = Workspace::builder()
1464 .main_branch(origin.branch())
1465 .resume_branch(resume_tree.branch())
1466 .dir(ws_dir)
1467 .additional_colocated_branches(
1468 vec![("colo".to_string(), "colo".to_string())]
1469 .into_iter()
1470 .collect(),
1471 )
1472 .build()
1473 .unwrap();
1474
1475 assert!(!ws.changes_since_main());
1476 assert!(!ws.any_branch_changes());
1477 assert!(ws.refreshed());
1478 assert!(!ws.changes_since_base());
1479 assert_eq!(ws.local_tree().last_revision().unwrap(), revid2);
1480 assert_eq!(
1481 vec![
1482 ("".to_string(), Some(revid2.clone()), Some(revid2.clone())),
1483 (
1484 "colo".to_string(),
1485 Some(colo_revid1.clone()),
1486 Some(colo_revid1.clone())
1487 ),
1488 ],
1489 ws.changed_branches()
1490 );
1491 std::mem::drop(td);
1492 }
1493
1494 #[test]
1495 #[serial]
1496 fn test_defer_destroy() {
1497 let _test_env = TestEnv::new();
1498 let td = tempfile::tempdir().unwrap();
1499
1500 let origin = breezyshim::controldir::create_standalone_workingtree(
1501 &td.path().join("origin"),
1502 &ControlDirFormat::default(),
1503 )
1504 .unwrap();
1505 origin
1506 .build_commit()
1507 .message("first commit")
1508 .commit()
1509 .unwrap();
1510
1511 let ws_dir = td.path().join("ws");
1512 std::fs::create_dir(&ws_dir).unwrap();
1513
1514 let mut ws = Workspace::builder()
1515 .main_branch(origin.branch())
1516 .dir(ws_dir)
1517 .build()
1518 .unwrap();
1519
1520 let tempdir = ws.defer_destroy();
1521
1522 assert!(tempdir.exists());
1523
1524 std::mem::drop(ws);
1525
1526 assert!(tempdir.exists());
1527 std::mem::drop(td);
1528 }
1529}