Skip to main content

silver_platter/
workspace.rs

1//! Workspace for preparing changes for publication
2use 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                // GenericBranch implements PyBranch, so we can push colocated branches
36                match _controldir.push_branch(
37                    remote_colo_branch.as_ref(),
38                    Some(to_branch_name),
39                    None,        // stop_revision
40                    Some(false), // overwrite
41                    None,        // tag_selector
42                ) {
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)]
72/// An error that can occur when working with a workspace
73pub enum Error {
74    /// An error from the Breezy shim
75    BrzError(BrzError),
76
77    /// An I/O error
78    IOError(std::io::Error),
79
80    /// Unknown format was specified
81    UnknownFormat(String),
82
83    /// Permission denied
84    PermissionDenied(Option<String>),
85
86    /// Other error
87    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)]
135/// A builder for a workspace
136pub 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    /// Set the main branch
149    pub fn main_branch(mut self, main_branch: GenericBranch) -> Self {
150        self.main_branch = Some(main_branch);
151        self
152    }
153
154    /// Set the resume branch
155    pub fn resume_branch(mut self, resume_branch: GenericBranch) -> Self {
156        self.resume_branch = Some(resume_branch);
157        self
158    }
159
160    /// Set the cached branch
161    pub fn cached_branch(mut self, cached_branch: GenericBranch) -> Self {
162        self.cached_branch = Some(cached_branch);
163        self
164    }
165
166    /// Set the additional colocated branches
167    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    /// Set the additional colocated branches for the resume branch
176    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    /// Set the containing directory to use for the workspace
186    pub fn dir(mut self, dir: PathBuf) -> Self {
187        self.dir = Some(dir);
188        self
189    }
190
191    /// Set the path to the workspace
192    pub fn path(mut self, path: PathBuf) -> Self {
193        self.path = Some(path);
194        self
195    }
196
197    /// Set the control dir format to use.
198    ///
199    /// This defaults to the format of the remote branch.
200    pub fn format(mut self, format: impl breezyshim::controldir::AsFormat) -> Self {
201        self.format = format.as_format();
202        self
203    }
204
205    /// Build the workspace
206    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
233/// A place in which changes can be prepared for publication
234pub 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    /// Create a new temporary workspace
248    pub fn temporary() -> Result<Self, Error> {
249        let td = tempfile::tempdir().unwrap();
250        let path = td.path().to_path_buf();
251        let _ = td.keep(); // Keep the temporary directory
252        Self::builder().dir(path).build()
253    }
254
255    /// Create a new workspace from a main branch URL
256    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    /// Start this workspace
262    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        // First, clone the main branch from the most efficient source
268        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 there is a main branch, ensure that revisions match
337        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            // At this point, we're either on the tip of the main branch or the tip of the resume
380            // branch
381            if let Some(resume_branch) = self.resume_branch.as_ref() {
382                // If there's a resume branch at play, make sure it's derived from the main branch
383                // *or* reset back to the main branch.
384                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    /// Return the state of the workspace
469    fn state(&self) -> &WorkspaceState {
470        self.state.as_ref().unwrap()
471    }
472
473    /// Create a new workspace builder
474    pub fn builder() -> WorkspaceBuilder {
475        WorkspaceBuilder::default()
476    }
477
478    /// Return the main branch
479    pub fn main_branch(&self) -> Option<&GenericBranch> {
480        self.main_branch.as_ref()
481    }
482
483    /// Set the main branch
484    pub fn set_main_branch(&mut self, branch: GenericBranch) -> Result<(), Error> {
485        self.main_branch = Some(branch);
486        Ok(())
487    }
488
489    /// Return the cached branch
490    pub fn local_tree(&self) -> &breezyshim::workingtree::GenericWorkingTree {
491        &self.state().local_tree
492    }
493
494    /// Return whether the workspace has been refreshed
495    ///
496    /// In other words, whether the workspace has been reset to the main branch
497    pub fn refreshed(&self) -> bool {
498        self.state().refreshed
499    }
500
501    /// Return the resume branch
502    pub fn resume_branch(&self) -> Option<&GenericBranch> {
503        self.resume_branch.as_ref()
504    }
505
506    /// Return the path to the workspace
507    pub fn path(&self) -> PathBuf {
508        self.local_tree()
509            .abspath(std::path::Path::new("."))
510            .unwrap()
511    }
512
513    /// Return whether there are changes since the main branch
514    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    /// Return whether there are changes since the base revision
520    pub fn changes_since_base(&self) -> bool {
521        self.base_revid() != Some(&self.local_tree().branch().last_revision())
522    }
523
524    /// Return the base revision id
525    pub fn base_revid(&self) -> Option<&RevisionId> {
526        self.state.as_ref().map(|s| &s.base_revid)
527    }
528
529    /// Have any branch changes at all been made?
530    ///
531    /// Includes changes that already existed in the resume branch
532    pub fn any_branch_changes(&self) -> bool {
533        self.changed_branches().iter().any(|(_, br, r)| br != r)
534    }
535
536    /// Return the additional colocated branches
537    pub fn additional_colocated_branches(&self) -> &HashMap<String, String> {
538        &self.additional_colocated_branches
539    }
540
541    /// Return the branches that have changed
542    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    /// Return the main colocated branch revision ids
573    pub fn main_colo_revid(&self) -> &HashMap<String, RevisionId> {
574        &self.state().main_colo_revid
575    }
576
577    /// Return the basis tree
578    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                // Fall back to repository if the working tree doesn't have the revision
584                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    /// Defer destroying the workspace, even if the Workspace is dropped
596    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    /// Publish the changes back to the main branch
603    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    /// Propose the changes against the main branch
650    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            // GenericBranch implements PyBranch, so we can use forge operations
675            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    /// Push a new derived branch
706    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            // GenericBranch implements PyBranch, so we can use forge operations
721            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    /// Push the specified tags to the main branch
740    pub fn push_tags(&self, tags: HashMap<String, RevisionId>) -> Result<(), Error> {
741        self.push(Some(tags))
742    }
743
744    /// Push the changes back to the main branch
745    pub fn push(&self, tags: Option<HashMap<String, RevisionId>>) -> Result<(), Error> {
746        let main_branch = self.main_branch().unwrap();
747
748        // Get forge for the main branch
749        let forge = match breezyshim::forge::get_forge(main_branch) {
750            Ok(forge) => Some(forge),
751            Err(breezyshim::error::Error::UnsupportedForge(e)) => {
752                // We can't figure out what branch to resume from when there's no forge
753                // that can tell us.
754                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(), // This is a hack - need proper GenericBranch
769            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    /// Show the diff between the base tree and the local tree
790    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    /// Destroy this workspace
808    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        // There are changes since the branch is created
836        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        // There are changes since the branch is created
877        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}