1pub use crate::proposal::DescriptionFormat;
3use crate::vcs::open_branch;
4use crate::Mode;
5use breezyshim::branch::{GenericBranch, MemoryBranch, PyBranch};
6
7use breezyshim::error::Error as BrzError;
8use breezyshim::forge::MergeProposalStatus;
9use breezyshim::merge::{MergeType, Merger};
10use breezyshim::repository::Repository;
11use breezyshim::{Branch, Forge, MergeProposal, RevisionId, Transport};
12
13use std::collections::HashMap;
14
15fn _tag_selector_from_tags(
16 tags: std::collections::HashMap<String, RevisionId>,
17) -> impl Fn(String) -> bool {
18 move |tag| tags.contains_key(tag.as_str())
19}
20
21fn _tag_selector_from_tags_ref(
22 tags: &std::collections::HashMap<String, RevisionId>,
23) -> Box<dyn Fn(String) -> bool + '_> {
24 Box::new(move |tag| tags.contains_key(tag.as_str()))
25}
26
27pub fn push_derived_changes(
29 local_branch: &dyn PyBranch,
30 main_branch: &dyn PyBranch,
31 forge: &Forge,
32 name: &str,
33 overwrite_existing: Option<bool>,
34 owner: Option<&str>,
35 tags: Option<std::collections::HashMap<String, RevisionId>>,
36 stop_revision: Option<&RevisionId>,
37) -> Result<(Box<dyn Branch>, url::Url), BrzError> {
38 let tags = tags.unwrap_or_default();
39 let (remote_branch, public_branch_url) = forge.publish_derived(
40 local_branch,
41 main_branch,
42 name,
43 overwrite_existing,
44 owner,
45 stop_revision,
46 Some(Box::new(_tag_selector_from_tags(tags))),
47 )?;
48 Ok((remote_branch, public_branch_url))
49}
50
51pub fn push_result(
53 local_branch: &GenericBranch,
54 remote_branch: &GenericBranch,
55 additional_colocated_branches: Option<&[(String, String)]>,
56 tags: Option<&std::collections::HashMap<String, RevisionId>>,
57 stop_revision: Option<&RevisionId>,
58) -> Result<(), BrzError> {
59 let tag_selector = if let Some(tags) = tags {
60 _tag_selector_from_tags(tags.clone())
61 } else {
62 _tag_selector_from_tags(std::collections::HashMap::new())
63 };
64 local_branch.push(
65 remote_branch,
66 false,
67 stop_revision,
68 Some(Box::new(tag_selector)),
69 )?;
70
71 if let Some(branches) = additional_colocated_branches {
72 for (from_branch_name, to_branch_name) in branches {
73 match local_branch
74 .controldir()
75 .open_branch(Some(from_branch_name.as_str()))
76 {
77 Ok(branch) => {
78 let tag_selector = if let Some(tags) = tags {
79 Box::new(_tag_selector_from_tags(tags.clone()))
80 } else {
81 Box::new(_tag_selector_from_tags(std::collections::HashMap::new()))
82 };
83
84 let target_controldir = remote_branch.controldir();
87 target_controldir.push_branch(
88 branch.as_ref(),
89 Some(to_branch_name.as_str()),
90 None,
91 Some(false),
92 Some(tag_selector),
93 )?;
94 }
95 Err(BrzError::NotBranchError(..)) => {}
96 Err(e) => return Err(e),
97 };
98 }
99 }
100 Ok(())
101}
102
103pub fn push_changes(
114 local_branch: &GenericBranch,
115 main_branch: &GenericBranch,
116 forge: Option<&Forge>,
117 possible_transports: Option<&mut Vec<Transport>>,
118 additional_colocated_branches: Option<Vec<(String, String)>>,
119 tags: Option<std::collections::HashMap<String, RevisionId>>,
120 stop_revision: Option<&RevisionId>,
121) -> Result<(), Error> {
122 let push_url = if let Some(forge) = forge {
123 forge.get_push_url(main_branch)
124 } else {
125 main_branch.get_user_url()
126 };
127 log::info!("pushing to {}", push_url);
128 let target_branch = open_branch(&push_url, possible_transports, None, None)?;
129 push_result(
130 local_branch,
131 &target_branch,
132 additional_colocated_branches.as_deref(),
133 tags.as_ref(),
134 stop_revision,
135 )
136 .map_err(Into::into)
137}
138
139#[allow(dead_code)]
155pub fn find_existing_proposed(
156 main_branch: &GenericBranch,
157 forge: &Forge,
158 name: &str,
159 overwrite_unrelated: bool,
160 owner: Option<&str>,
161 _preferred_schemes: Option<&[&str]>,
162) -> Result<
163 (
164 Option<GenericBranch>,
165 Option<bool>,
166 Option<Vec<MergeProposal>>,
167 ),
168 BrzError,
169> {
170 match forge.get_derived_branch_as_generic(main_branch, name, owner, None) {
171 Ok(derived_branch) => {
172 let proposals =
174 forge.iter_proposals(main_branch, main_branch, MergeProposalStatus::Open)?;
175
176 let derived_url = derived_branch.get_user_url();
178 let matching_proposals: Vec<MergeProposal> = proposals
179 .into_iter()
180 .filter(|proposal| {
181 if let Ok(Some(source_url)) = proposal.get_source_branch_url() {
182 source_url == derived_url
183 } else {
184 false
185 }
186 })
187 .collect();
188
189 Ok((
190 Some(derived_branch),
191 Some(!overwrite_unrelated),
192 Some(matching_proposals),
193 ))
194 }
195 Err(BrzError::NotBranchError(..)) => {
196 Ok((None, None, None))
198 }
199 Err(e) => Err(e),
200 }
201}
202
203pub fn propose_changes(
232 local_branch: &GenericBranch,
233 main_branch: &GenericBranch,
234 forge: &Forge,
235 name: &str,
236 mp_description: &str,
237 resume_branch: Option<&GenericBranch>,
238 mut resume_proposal: Option<MergeProposal>,
239 overwrite_existing: Option<bool>,
240 labels: Option<Vec<String>>,
241 commit_message: Option<&str>,
242 title: Option<&str>,
243 additional_colocated_branches: Option<Vec<(String, String)>>,
244 allow_empty: Option<bool>,
245 _reviewers: Option<Vec<String>>,
246 tags: Option<HashMap<String, RevisionId>>,
247 owner: Option<&str>,
248 stop_revision: Option<&RevisionId>,
249 _allow_collaboration: Option<bool>,
250 auto_merge: Option<bool>,
251 work_in_progress: Option<bool>,
252) -> Result<(MergeProposal, bool), Error> {
253 if !allow_empty.unwrap_or(false)
254 && check_proposal_diff_empty(local_branch, main_branch, stop_revision)?
255 {
256 return Err(Error::EmptyMergeProposal);
257 }
258 let overwrite_existing = overwrite_existing.unwrap_or(true);
259
260 if let Some(resume_branch) = resume_branch {
262 let tag_selector = tags.as_ref().map(|tag_map| {
264 Box::new(_tag_selector_from_tags(tag_map.clone())) as Box<dyn Fn(String) -> bool>
265 });
266 local_branch.push(
267 resume_branch,
268 overwrite_existing,
269 stop_revision,
270 tag_selector,
271 )?;
272 } else {
273 let tag_selector = tags.as_ref().map(|tag_map| {
274 Box::new(_tag_selector_from_tags(tag_map.clone())) as Box<dyn Fn(String) -> bool>
275 });
276 let (_derived_branch, _public_branch_url) = forge.publish_derived(
277 local_branch,
278 main_branch,
279 name,
280 Some(overwrite_existing),
281 owner,
282 stop_revision,
283 tag_selector,
284 )?;
285 }
286 for (from_branch_name, to_branch_name) in additional_colocated_branches.unwrap_or_default() {
288 match local_branch
289 .controldir()
290 .open_branch(Some(from_branch_name.as_str()))
291 {
292 Ok(from_branch) => {
293 let tag_selector = tags.as_ref().map(|tag_map| {
294 Box::new(_tag_selector_from_tags(tag_map.clone()))
295 as Box<dyn Fn(String) -> bool>
296 });
297
298 let target_controldir = if let Some(resume_branch) = resume_branch {
300 resume_branch.controldir()
301 } else {
302 main_branch.controldir()
305 };
306
307 match target_controldir.push_branch(
308 from_branch.as_ref(),
309 Some(to_branch_name.as_str()),
310 None, Some(false), tag_selector,
313 ) {
314 Ok(_) => log::debug!(
315 "Successfully pushed colocated branch {} -> {}",
316 from_branch_name,
317 to_branch_name
318 ),
319 Err(e) => log::warn!(
320 "Failed to push colocated branch {} -> {}: {}",
321 from_branch_name,
322 to_branch_name,
323 e
324 ),
325 }
326 }
327 Err(BrzError::NotBranchError(..)) => {
328 log::debug!("Colocated branch {} not found, skipping", from_branch_name);
329 }
330 Err(e) => {
331 log::warn!(
332 "Error accessing colocated branch {}: {}",
333 from_branch_name,
334 e
335 );
336 }
337 }
338 }
339 if let Some(mp) = resume_proposal.as_ref() {
340 if mp.is_closed()? {
341 match mp.reopen() {
342 Ok(_) => {}
343 Err(e) => {
344 log::info!(
345 "Reopening existing proposal failed ({}). Creating new proposal.",
346 e
347 );
348 resume_proposal = None;
349 }
350 }
351 }
352 }
353 if let Some(resume_proposal) = resume_proposal.take() {
354 if resume_proposal.get_description()?.as_deref() != Some(mp_description) {
358 match resume_proposal.set_description(Some(mp_description)) {
359 Ok(_) => (),
360 Err(BrzError::UnsupportedOperation(..)) => (),
361 Err(e) => return Err(e.into()),
362 }
363 }
364 if resume_proposal.get_commit_message()?.as_deref() != commit_message {
365 match resume_proposal.set_commit_message(commit_message) {
366 Ok(_) => (),
367 Err(BrzError::UnsupportedOperation(..)) => (),
368 Err(e) => return Err(e.into()),
369 }
370 }
371 if resume_proposal.get_title()?.as_deref() != title {
372 match resume_proposal.set_title(title) {
373 Ok(_) => (),
374 Err(BrzError::UnsupportedOperation(..)) => (),
375 Err(e) => return Err(e.into()),
376 }
377 }
378 Ok((resume_proposal, false))
379 } else {
380 let mut proposer = forge.get_proposer(main_branch, local_branch)?;
382 proposer = proposer.description(mp_description);
383 if let Some(title) = title {
384 proposer = proposer.title(title);
385 }
386 if let Some(commit_message) = commit_message {
387 proposer = proposer.commit_message(commit_message);
388 }
389 if let Some(labels) = labels {
390 let label_refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
391 proposer = proposer.labels(&label_refs);
392 }
393 if let Some(reviewers) = _reviewers {
394 let reviewer_refs: Vec<&str> = reviewers.iter().map(|s| s.as_str()).collect();
395 proposer = proposer.reviewers(&reviewer_refs);
396 }
397 if let Some(allow_collaboration) = _allow_collaboration {
398 proposer = proposer.allow_collaboration(allow_collaboration);
399 }
400 if let Some(work_in_progress) = work_in_progress {
401 proposer = proposer.work_in_progress(work_in_progress);
402 }
403 let proposal = proposer.build()?;
404
405 if let Some(auto_merge) = auto_merge {
407 if auto_merge {
408 match proposal.merge(true) {
410 Ok(_) => {}
411 Err(BrzError::UnsupportedOperation(..)) => {
412 log::debug!("Auto-merge not supported by this forge");
414 }
415 Err(e) => return Err(e.into()),
416 }
417 }
418 }
419
420 Ok((proposal, true))
421 }
422}
423
424#[derive(Debug)]
425pub enum Error {
427 DivergedBranches(),
429
430 UnrelatedBranchExists,
432
433 Other(BrzError),
435
436 UnsupportedForge(url::Url),
438
439 ForgeLoginRequired,
441
442 InsufficientChangesForNewProposal,
444
445 BranchOpenError(crate::vcs::BranchOpenError),
447
448 EmptyMergeProposal,
450
451 PermissionDenied,
453
454 NoTargetBranch,
456}
457
458impl From<BrzError> for Error {
459 fn from(e: BrzError) -> Self {
460 match e {
461 BrzError::DivergedBranches => Error::DivergedBranches(),
462 BrzError::NotBranchError(..) => Error::UnrelatedBranchExists,
463 BrzError::PermissionDenied(..) => Error::PermissionDenied,
464 BrzError::UnsupportedForge(s) => Error::UnsupportedForge(s),
465 BrzError::ForgeLoginRequired => Error::ForgeLoginRequired,
466 _ => Error::Other(e),
467 }
468 }
469}
470
471impl From<crate::vcs::BranchOpenError> for Error {
472 fn from(e: crate::vcs::BranchOpenError) -> Self {
473 Error::BranchOpenError(e)
474 }
475}
476
477impl std::fmt::Display for Error {
478 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
479 match self {
480 Error::DivergedBranches() => write!(f, "Diverged branches"),
481 Error::Other(e) => write!(f, "{}", e),
482 Error::UnsupportedForge(u) => write!(f, "Unsupported forge: {}", u),
483 Error::ForgeLoginRequired => write!(f, "Forge login required"),
484 Error::BranchOpenError(e) => write!(f, "{}", e),
485 Error::EmptyMergeProposal => write!(f, "Empty merge proposal"),
486 Error::PermissionDenied => write!(f, "Permission denied"),
487 Error::UnrelatedBranchExists => write!(f, "Unrelated branch exists"),
488 Error::InsufficientChangesForNewProposal => {
489 write!(f, "Insufficient changes for new proposal")
490 }
491 Error::NoTargetBranch => write!(f, "No target branch"),
492 }
493 }
494}
495
496pub struct PublishBuilder<'a> {
507 local_branch: &'a GenericBranch,
508 main_branch: &'a GenericBranch,
509 resume_branch: Option<&'a GenericBranch>,
510 mode: Mode,
511 name: &'a str,
512 forge: Option<&'a Forge>,
513 allow_create_proposal: Option<bool>,
514 labels: Option<Vec<String>>,
515 overwrite_existing: Option<bool>,
516 existing_proposal: Option<MergeProposal>,
517 reviewers: Option<Vec<String>>,
518 tags: Option<HashMap<String, RevisionId>>,
519 derived_owner: Option<&'a str>,
520 allow_collaboration: Option<bool>,
521 stop_revision: Option<&'a RevisionId>,
522 auto_merge: Option<bool>,
523 work_in_progress: Option<bool>,
524}
525
526impl<'a> PublishBuilder<'a> {
527 pub fn new(
529 local_branch: &'a GenericBranch,
530 main_branch: &'a GenericBranch,
531 name: &'a str,
532 mode: Mode,
533 ) -> Self {
534 Self {
535 local_branch,
536 main_branch,
537 resume_branch: None,
538 mode,
539 name,
540 forge: None,
541 allow_create_proposal: None,
542 labels: None,
543 overwrite_existing: None,
544 existing_proposal: None,
545 reviewers: None,
546 tags: None,
547 derived_owner: None,
548 allow_collaboration: None,
549 stop_revision: None,
550 auto_merge: None,
551 work_in_progress: None,
552 }
553 }
554
555 pub fn resume_branch(mut self, branch: &'a GenericBranch) -> Self {
557 self.resume_branch = Some(branch);
558 self
559 }
560
561 pub fn forge(mut self, forge: &'a Forge) -> Self {
563 self.forge = Some(forge);
564 self
565 }
566
567 pub fn allow_create_proposal(mut self, allow: bool) -> Self {
569 self.allow_create_proposal = Some(allow);
570 self
571 }
572
573 pub fn labels(mut self, labels: Vec<String>) -> Self {
575 self.labels = Some(labels);
576 self
577 }
578
579 pub fn overwrite_existing(mut self, overwrite: bool) -> Self {
581 self.overwrite_existing = Some(overwrite);
582 self
583 }
584
585 pub fn existing_proposal(mut self, proposal: MergeProposal) -> Self {
587 self.existing_proposal = Some(proposal);
588 self
589 }
590
591 pub fn reviewers(mut self, reviewers: Vec<String>) -> Self {
593 self.reviewers = Some(reviewers);
594 self
595 }
596
597 pub fn tags(mut self, tags: HashMap<String, RevisionId>) -> Self {
599 self.tags = Some(tags);
600 self
601 }
602
603 pub fn derived_owner(mut self, owner: &'a str) -> Self {
605 self.derived_owner = Some(owner);
606 self
607 }
608
609 pub fn allow_collaboration(mut self, allow: bool) -> Self {
611 self.allow_collaboration = Some(allow);
612 self
613 }
614
615 pub fn stop_revision(mut self, revision: &'a RevisionId) -> Self {
617 self.stop_revision = Some(revision);
618 self
619 }
620
621 pub fn auto_merge(mut self, auto: bool) -> Self {
623 self.auto_merge = Some(auto);
624 self
625 }
626
627 pub fn work_in_progress(mut self, wip: bool) -> Self {
629 self.work_in_progress = Some(wip);
630 self
631 }
632
633 pub fn publish(
643 self,
644 get_proposal_description: impl FnOnce(DescriptionFormat, Option<&MergeProposal>) -> String,
645 get_proposal_commit_message: Option<impl FnOnce(Option<&MergeProposal>) -> Option<String>>,
646 get_proposal_title: Option<impl FnOnce(Option<&MergeProposal>) -> Option<String>>,
647 ) -> Result<PublishResult, Error> {
648 publish_changes(
649 self.local_branch,
650 self.main_branch,
651 self.resume_branch,
652 self.mode,
653 self.name,
654 get_proposal_description,
655 get_proposal_commit_message,
656 get_proposal_title,
657 self.forge,
658 self.allow_create_proposal,
659 self.labels,
660 self.overwrite_existing,
661 self.existing_proposal,
662 self.reviewers,
663 self.tags,
664 self.derived_owner,
665 self.allow_collaboration,
666 self.stop_revision,
667 self.auto_merge,
668 self.work_in_progress,
669 )
670 }
671}
672
673pub fn publish_changes(
687 local_branch: &GenericBranch,
688 main_branch: &GenericBranch,
689 resume_branch: Option<&GenericBranch>,
690 mut mode: Mode,
691 name: &str,
692 get_proposal_description: impl FnOnce(DescriptionFormat, Option<&MergeProposal>) -> String,
693 get_proposal_commit_message: Option<impl FnOnce(Option<&MergeProposal>) -> Option<String>>,
694 get_proposal_title: Option<impl FnOnce(Option<&MergeProposal>) -> Option<String>>,
695 forge: Option<&Forge>,
696 allow_create_proposal: Option<bool>,
697 labels: Option<Vec<String>>,
698 overwrite_existing: Option<bool>,
699 existing_proposal: Option<MergeProposal>,
700 reviewers: Option<Vec<String>>,
701 tags: Option<HashMap<String, RevisionId>>,
702 derived_owner: Option<&str>,
703 allow_collaboration: Option<bool>,
704 stop_revision: Option<&RevisionId>,
705 auto_merge: Option<bool>,
706 work_in_progress: Option<bool>,
707) -> Result<PublishResult, Error> {
708 let stop_revision_owned;
709 let stop_revision = match stop_revision {
710 Some(r) => r,
711 None => {
712 stop_revision_owned = local_branch.last_revision();
713 &stop_revision_owned
714 }
715 };
716 let allow_create_proposal = allow_create_proposal.unwrap_or(true);
717
718 if forge.is_none() && mode != Mode::Push && mode != Mode::AttemptPush {
720 return Err(Error::UnsupportedForge(main_branch.get_user_url()));
721 }
722
723 if *stop_revision == main_branch.last_revision() {
726 if let Some(existing_proposal) = existing_proposal.as_ref() {
727 log::info!("closing existing merge proposal - no new revisions");
728 existing_proposal.close()?;
729 }
730 return Ok(PublishResult {
731 mode,
732 target_branch: main_branch.get_user_url(),
733 forge: forge.cloned(),
734 proposal: existing_proposal,
735 is_new: Some(false),
736 });
737 }
738
739 if let Some(resume_branch) = resume_branch {
740 if resume_branch.last_revision() == *stop_revision {
741 log::info!("No changes added; making sure merge proposal is up to date.");
745 }
746 }
747 let write_lock = main_branch.lock_write()?;
748 match mode {
749 Mode::PushDerived => {
750 let forge_ref = forge.as_ref().unwrap(); let (_remote_branch, _public_url) = push_derived_changes(
752 local_branch,
753 main_branch,
754 forge_ref,
755 name,
756 overwrite_existing,
757 derived_owner,
758 tags,
759 Some(stop_revision),
760 )?;
761 return Ok(PublishResult {
762 mode,
763 target_branch: main_branch.get_user_url(),
764 forge: forge.cloned(),
765 proposal: None,
766 is_new: None,
767 });
768 }
769 Mode::Push | Mode::AttemptPush => {
770 let read_lock = local_branch.lock_read()?;
771 let graph = local_branch.repository().get_graph();
773 if !graph.is_ancestor(&main_branch.last_revision(), stop_revision)? {
774 return Err(Error::DivergedBranches());
775 }
776 std::mem::drop(read_lock);
777 match push_changes(
778 local_branch,
779 main_branch,
780 forge,
781 None,
782 None,
783 tags.clone(),
784 Some(stop_revision),
785 ) {
786 Err(e @ Error::PermissionDenied) => {
787 if mode == Mode::AttemptPush {
788 log::info!("push access denied, falling back to propose");
789 mode = Mode::Propose;
790 } else {
791 log::info!("permission denied during push");
792 return Err(e);
793 }
794 }
795 Ok(_) => {
796 return Ok(PublishResult {
797 proposal: None,
798 mode,
799 target_branch: main_branch.get_user_url(),
800 forge: forge.cloned(),
801 is_new: None,
802 });
803 }
804 Err(e) => {
805 return Err(e);
806 }
807 }
808 }
809 Mode::Bts => {
810 unimplemented!();
811 }
812 Mode::Propose => { }
814 }
815
816 assert_eq!(mode, Mode::Propose);
817 if resume_branch.is_none() && !allow_create_proposal {
818 return Err(Error::InsufficientChangesForNewProposal);
819 }
820
821 let forge = forge.ok_or(Error::UnsupportedForge(main_branch.get_user_url()))?;
822
823 let mp_description = get_proposal_description(
824 forge.merge_proposal_description_format().parse().unwrap(),
825 if resume_branch.is_some() {
826 existing_proposal.as_ref()
827 } else {
828 None
829 },
830 );
831 let commit_message = if let Some(get_proposal_commit_message) = get_proposal_commit_message {
832 get_proposal_commit_message(if resume_branch.is_some() {
833 existing_proposal.as_ref()
834 } else {
835 None
836 })
837 } else {
838 None
839 };
840 let title = if let Some(get_proposal_title) = get_proposal_title {
841 get_proposal_title(if resume_branch.is_some() {
842 existing_proposal.as_ref()
843 } else {
844 None
845 })
846 } else {
847 None
848 };
849 let title = if let Some(title) = title {
850 Some(title)
851 } else {
852 match breezyshim::forge::determine_title(mp_description.as_str()) {
853 Ok(title) => Some(title),
854 Err(e) => {
855 log::warn!("Failed to determine title from description: {}", e);
856 None
857 }
858 }
859 };
860 let (proposal, is_new) = propose_changes(
861 local_branch,
862 main_branch,
863 forge, name,
865 mp_description.as_str(),
866 resume_branch,
867 existing_proposal,
868 overwrite_existing,
869 labels,
870 commit_message.as_deref(),
871 title.as_deref(),
872 None,
873 None,
874 reviewers,
875 tags,
876 derived_owner,
877 Some(stop_revision),
878 allow_collaboration,
879 auto_merge,
880 work_in_progress,
881 )?;
882 std::mem::drop(write_lock);
883 Ok(PublishResult {
884 mode,
885 proposal: Some(proposal),
886 is_new: Some(is_new),
887 target_branch: main_branch.get_user_url(),
888 forge: Some(forge.clone()),
889 })
890}
891
892#[derive(Debug)]
894pub struct PublishResult {
895 pub mode: Mode,
897
898 pub proposal: Option<MergeProposal>,
900
901 pub is_new: Option<bool>,
903
904 pub target_branch: url::Url,
906
907 pub forge: Option<Forge>,
909}
910
911pub fn check_proposal_diff_empty(
913 other_branch: &dyn PyBranch,
914 main_branch: &dyn PyBranch,
915 stop_revision: Option<&RevisionId>,
916) -> Result<bool, BrzError> {
917 let stop_revision_owned;
918 let stop_revision = match stop_revision {
919 Some(rev) => rev,
920 None => {
921 stop_revision_owned = other_branch.last_revision();
922 &stop_revision_owned
923 }
924 };
925 let main_revid = main_branch.last_revision();
926 let other_repository = other_branch.repository();
927 other_repository.fetch(&main_branch.repository(), Some(&main_revid))?;
928
929 let lock = other_branch.lock_read();
930 let main_tree = other_repository.revision_tree(&main_revid)?;
931 let revision_graph = other_repository.get_graph();
932 let tree_branch = MemoryBranch::new(&other_repository, None, &main_revid);
933 let mut merger = Merger::new(&tree_branch, &main_tree, &revision_graph);
934 merger.set_other_revision(stop_revision, other_branch)?;
935 if merger.find_base()?.is_none() {
936 merger.set_base_revision(&RevisionId::null(), other_branch)?;
937 }
938 merger.set_merge_type(MergeType::Merge3);
939 let tree_merger = merger.make_merger()?;
940 let tt = tree_merger.make_preview_transform()?;
941 let mut changes = tt.iter_changes()?;
942 std::mem::drop(lock);
943 Ok(!changes.any(|_| true))
944}
945
946pub fn enable_tag_pushing(branch: &dyn Branch) -> Result<(), BrzError> {
948 let config = branch.get_config();
949 config.set_user_option("branch.fetch_tags", true)?;
950 Ok(())
951}
952
953#[cfg(test)]
954mod tests {
955 use super::*;
956 use breezyshim::testing::TestEnv;
957 use breezyshim::tree::MutableTree;
958 use breezyshim::WorkingTree;
959 use serial_test::serial;
960
961 #[test]
962 #[serial]
963 fn test_no_new_commits() {
964 let _test_env = TestEnv::new();
965 use breezyshim::controldir::create_standalone_workingtree;
966 use breezyshim::controldir::ControlDirFormat;
967 let td = tempfile::tempdir().unwrap();
968 let orig = td.path().join("orig");
969 let tree = create_standalone_workingtree(&orig, &ControlDirFormat::default()).unwrap();
970
971 std::fs::write(orig.join("a"), "a").unwrap();
972 MutableTree::add(&tree, &[std::path::Path::new("a")]).unwrap();
973 tree.build_commit().message("blah").commit().unwrap();
974
975 let proposal_url = url::Url::from_file_path(orig.join("proposal")).unwrap();
976
977 let proposal = tree
978 .controldir()
979 .sprout(proposal_url, None, None, None, None)
980 .unwrap()
981 .open_branch(None)
982 .unwrap();
983 assert!(check_proposal_diff_empty(proposal.as_ref(), &tree.branch(), None).unwrap());
984 }
985
986 #[test]
987 #[serial]
988 fn test_no_op_commits() {
989 let _test_env = TestEnv::new();
990 use breezyshim::controldir::create_standalone_workingtree;
991 use breezyshim::controldir::ControlDirFormat;
992 let td = tempfile::tempdir().unwrap();
993 let orig = td.path().join("orig");
994 let tree = create_standalone_workingtree(&orig, &ControlDirFormat::default()).unwrap();
995
996 std::fs::write(orig.join("a"), "a").unwrap();
997 MutableTree::add(&tree, &[std::path::Path::new("a")]).unwrap();
998 tree.build_commit().message("blah").commit().unwrap();
999
1000 let proposal_url = url::Url::from_file_path(orig.join("proposal")).unwrap();
1001
1002 let proposal = tree
1003 .controldir()
1004 .sprout(proposal_url, None, None, None, None)
1005 .unwrap()
1006 .open_workingtree()
1007 .unwrap();
1008 proposal
1009 .build_commit()
1010 .message("another commit that is pointless")
1011 .commit()
1012 .unwrap();
1013
1014 assert!(check_proposal_diff_empty(&proposal.branch(), &tree.branch(), None).unwrap());
1015 }
1016
1017 #[test]
1018 #[serial]
1019 fn test_indep() {
1020 let _test_env = TestEnv::new();
1021 use breezyshim::bazaar::tree::MutableInventoryTree;
1022 use breezyshim::bazaar::FileId;
1023 use breezyshim::controldir::create_standalone_workingtree;
1024 use breezyshim::controldir::ControlDirFormat;
1025 let td = tempfile::tempdir().unwrap();
1026 let orig = td.path().join("orig");
1027 let tree = create_standalone_workingtree(&orig, &ControlDirFormat::default()).unwrap();
1028
1029 std::fs::write(orig.join("a"), "a").unwrap();
1030 MutableTree::add(&tree, &[std::path::Path::new("a")]).unwrap();
1031 tree.build_commit().message("blah").commit().unwrap();
1032
1033 std::fs::write(orig.join("b"), "b").unwrap();
1034 std::fs::write(orig.join("c"), "c").unwrap();
1035 MutableTree::add(
1036 &tree,
1037 &[std::path::Path::new("b"), std::path::Path::new("c")],
1038 )
1039 .unwrap();
1040 tree.build_commit().message("independent").commit().unwrap();
1041
1042 let proposal_path = orig.join("proposal");
1043 let proposal_url = url::Url::from_file_path(proposal_path.as_path()).unwrap();
1044
1045 let proposal = tree
1046 .controldir()
1047 .sprout(proposal_url, None, None, None, None)
1048 .unwrap()
1049 .open_workingtree()
1050 .unwrap();
1051
1052 assert!(proposal_path.exists());
1053
1054 std::fs::write(proposal_path.join("b"), "b").unwrap();
1055
1056 if proposal.supports_setting_file_ids() {
1057 MutableInventoryTree::add(
1058 &proposal,
1059 &[std::path::Path::new("b")],
1060 &[FileId::from("b")],
1061 )
1062 .unwrap();
1063 } else {
1064 MutableTree::add(&proposal, &[std::path::Path::new("b")]).unwrap();
1065 }
1066 proposal
1067 .build_commit()
1068 .message("not pointless")
1069 .commit()
1070 .unwrap();
1071
1072 assert!(check_proposal_diff_empty(&proposal.branch(), &tree.branch(), None).unwrap());
1073
1074 std::mem::drop(td);
1075 }
1076
1077 #[test]
1078 #[serial]
1079 fn test_changes() {
1080 let _test_env = TestEnv::new();
1081 use breezyshim::controldir::create_standalone_workingtree;
1082 use breezyshim::controldir::ControlDirFormat;
1083 let td = tempfile::tempdir().unwrap();
1084 let orig = td.path().join("orig");
1085 let tree = create_standalone_workingtree(&orig, &ControlDirFormat::default()).unwrap();
1086 std::fs::write(orig.join("a"), "a").unwrap();
1087 MutableTree::add(&tree, &[std::path::Path::new("a")]).unwrap();
1088 tree.build_commit().message("blah").commit().unwrap();
1089
1090 let proposal_url = url::Url::from_file_path(td.path().join("proposal")).unwrap();
1091 let proposal_tree = tree
1092 .controldir()
1093 .sprout(proposal_url, None, None, None, None)
1094 .unwrap()
1095 .open_workingtree()
1096 .unwrap();
1097 std::fs::write(proposal_tree.basedir().join("b"), "b").unwrap();
1098 MutableTree::add(&proposal_tree, &[std::path::Path::new("b")]).unwrap();
1099 proposal_tree
1100 .build_commit()
1101 .message("not pointless")
1102 .commit()
1103 .unwrap();
1104
1105 assert!(!check_proposal_diff_empty(&proposal_tree.branch(), &tree.branch(), None).unwrap());
1106 }
1107
1108 #[test]
1109 #[serial]
1110 fn test_push_result() {
1111 let _test_env = TestEnv::new();
1112 use breezyshim::controldir::{
1113 create_branch_convenience_as_generic, create_standalone_workingtree, ControlDirFormat,
1114 };
1115 let td = tempfile::tempdir().unwrap();
1116 let target_path = td.path().join("target");
1117 let source_path = td.path().join("source");
1118 let target_url = url::Url::from_file_path(target_path).unwrap();
1119 let _target_branch =
1120 create_branch_convenience_as_generic(&target_url, None, &ControlDirFormat::default())
1121 .unwrap();
1122 let target = crate::vcs::open_branch(&target_url, None, None, None).unwrap();
1123 let source =
1124 create_standalone_workingtree(&source_path, &ControlDirFormat::default()).unwrap();
1125 let revid = source
1126 .build_commit()
1127 .message("Some change")
1128 .commit()
1129 .unwrap();
1130 push_result(&source.branch(), &target, None, None, None).unwrap();
1131 assert_eq!(target.last_revision(), revid);
1132 }
1133
1134 #[test]
1135 fn test_publish_builder_construction() {
1136 use breezyshim::controldir::create_standalone_workingtree;
1137 use breezyshim::controldir::ControlDirFormat;
1138
1139 let td = tempfile::tempdir().unwrap();
1140 let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
1141 let local_branch = tree.branch();
1142 let main_branch = tree.branch();
1143
1144 let builder = PublishBuilder::new(&local_branch, &main_branch, "test-branch", Mode::Push);
1146
1147 assert_eq!(builder.name, "test-branch");
1149 assert_eq!(builder.mode, Mode::Push);
1150 assert!(builder.forge.is_none());
1151 assert!(builder.labels.is_none());
1152 assert!(builder.reviewers.is_none());
1153 }
1154
1155 #[test]
1156 fn test_publish_builder_chaining() {
1157 use breezyshim::controldir::create_standalone_workingtree;
1158 use breezyshim::controldir::ControlDirFormat;
1159
1160 let td = tempfile::tempdir().unwrap();
1161 let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
1162 let local_branch = tree.branch();
1163 let main_branch = tree.branch();
1164
1165 let builder =
1167 PublishBuilder::new(&local_branch, &main_branch, "test-branch", Mode::Propose)
1168 .labels(vec!["bug".to_string(), "feature".to_string()])
1169 .reviewers(vec!["user1".to_string(), "user2".to_string()])
1170 .allow_create_proposal(false)
1171 .overwrite_existing(true)
1172 .allow_collaboration(true)
1173 .auto_merge(true)
1174 .work_in_progress(false);
1175
1176 assert_eq!(
1178 builder.labels,
1179 Some(vec!["bug".to_string(), "feature".to_string()])
1180 );
1181 assert_eq!(
1182 builder.reviewers,
1183 Some(vec!["user1".to_string(), "user2".to_string()])
1184 );
1185 assert_eq!(builder.allow_create_proposal, Some(false));
1186 assert_eq!(builder.overwrite_existing, Some(true));
1187 assert_eq!(builder.allow_collaboration, Some(true));
1188 assert_eq!(builder.auto_merge, Some(true));
1189 assert_eq!(builder.work_in_progress, Some(false));
1190 }
1191
1192 #[test]
1193 fn test_publish_builder_with_tags() {
1194 use breezyshim::controldir::create_standalone_workingtree;
1195 use breezyshim::controldir::ControlDirFormat;
1196 use std::collections::HashMap;
1197
1198 let td = tempfile::tempdir().unwrap();
1199 let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
1200 let local_branch = tree.branch();
1201 let main_branch = tree.branch();
1202
1203 let mut tags = HashMap::new();
1204 tags.insert("v1.0".to_string(), RevisionId::from(b"rev1".to_vec()));
1205 tags.insert("v2.0".to_string(), RevisionId::from(b"rev2".to_vec()));
1206
1207 let builder = PublishBuilder::new(&local_branch, &main_branch, "test-branch", Mode::Push)
1208 .tags(tags.clone());
1209
1210 assert_eq!(builder.tags, Some(tags));
1211 }
1212
1213 #[test]
1214 fn test_empty_proposal_detection() {
1215 use breezyshim::controldir::create_standalone_workingtree;
1216 use breezyshim::controldir::ControlDirFormat;
1217
1218 let td = tempfile::tempdir().unwrap();
1219 let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
1220 let local_branch = tree.branch();
1221 let main_branch = tree.branch();
1222
1223 let result = PublishBuilder::new(&local_branch, &main_branch, "test-branch", Mode::Push)
1229 .publish(
1230 |_fmt, _mp| "Test description".to_string(),
1231 None::<fn(Option<&MergeProposal>) -> Option<String>>,
1232 None::<fn(Option<&MergeProposal>) -> Option<String>>,
1233 );
1234
1235 assert!(result.is_ok());
1239 }
1240
1241 #[test]
1242 fn test_forge_mode_validation() {
1243 use breezyshim::controldir::create_standalone_workingtree;
1244 use breezyshim::controldir::ControlDirFormat;
1245
1246 let td = tempfile::tempdir().unwrap();
1247 let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
1248 let local_branch = tree.branch();
1249 let main_branch = tree.branch();
1250
1251 let modes_requiring_forge = vec![Mode::Propose, Mode::PushDerived, Mode::Bts];
1253
1254 for mode in modes_requiring_forge {
1255 let result = PublishBuilder::new(&local_branch, &main_branch, "test-branch", mode)
1256 .publish(
1257 |_fmt, _mp| "Test description".to_string(),
1258 None::<fn(Option<&MergeProposal>) -> Option<String>>,
1259 None::<fn(Option<&MergeProposal>) -> Option<String>>,
1260 );
1261
1262 match result {
1264 Err(Error::UnsupportedForge(_)) => {
1265 }
1267 _ => panic!(
1268 "Expected UnsupportedForge error for mode {:?}, got: {:?}",
1269 mode, result
1270 ),
1271 }
1272 }
1273
1274 let modes_not_requiring_forge = vec![Mode::Push, Mode::AttemptPush];
1276
1277 for mode in modes_not_requiring_forge {
1278 let _result = PublishBuilder::new(&local_branch, &main_branch, "test-branch", mode)
1281 .publish(
1282 |_fmt, _mp| "Test description".to_string(),
1283 None::<fn(Option<&MergeProposal>) -> Option<String>>,
1284 None::<fn(Option<&MergeProposal>) -> Option<String>>,
1285 );
1286 }
1287 }
1288
1289 #[test]
1290 fn test_publish_builder_auto_merge() {
1291 use breezyshim::controldir::create_standalone_workingtree;
1292 use breezyshim::controldir::ControlDirFormat;
1293
1294 let td = tempfile::tempdir().unwrap();
1295 let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
1296 let local_branch = tree.branch();
1297 let main_branch = tree.branch();
1298
1299 let builder =
1301 PublishBuilder::new(&local_branch, &main_branch, "test-branch", Mode::Propose);
1302 assert_eq!(builder.auto_merge, None);
1303
1304 let builder = builder.auto_merge(true);
1306 assert_eq!(builder.auto_merge, Some(true));
1307
1308 let builder = builder.auto_merge(false);
1310 assert_eq!(builder.auto_merge, Some(false));
1311 }
1312}