1use std::collections::HashSet;
8use std::fmt::{self, Debug, Display};
9use std::num;
10use std::result::Result;
11
12use chrono::{DateTime, Utc};
13use git_workarea::{
14 CommitId, Conflict, GitContext, GitError, Identity, MergeResult, MergeStatus, WorkAreaError,
15};
16use log::{debug, info, warn};
17use thiserror::Error;
18
19#[derive(Debug, PartialEq, Eq)]
24#[non_exhaustive]
25pub enum InvalidCommitReason {
26 NonMergeCommit,
28 OctopusMerge,
30 NotRelated,
32 NotACommit,
34 InvalidSubject(String),
36 MissingId,
38 MissingUrl,
40 ZeroId,
42 UnparseableId(String),
44}
45
46impl Display for InvalidCommitReason {
47 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
48 let reason = match self {
49 InvalidCommitReason::NonMergeCommit => "non-merge commit",
50 InvalidCommitReason::OctopusMerge => "octopus merge",
51 InvalidCommitReason::NotRelated => "not related",
52 InvalidCommitReason::NotACommit => "not a commit",
53 InvalidCommitReason::InvalidSubject(_) => "invalid subject",
54 InvalidCommitReason::MissingId => "missing id",
55 InvalidCommitReason::MissingUrl => "missing url",
56 InvalidCommitReason::ZeroId => "invalid id (0)",
57 InvalidCommitReason::UnparseableId(_) => "unparseable id",
58 };
59
60 write!(f, "{}", reason)
61 }
62}
63
64#[derive(Debug, Error)]
66#[non_exhaustive]
67pub enum StagerError {
68 #[error("git error: {}", source)]
70 Git {
71 #[from]
73 source: GitError,
74 },
75 #[error("workarea error: {}", source)]
77 WorkArea {
78 #[from]
80 source: WorkAreaError,
81 },
82 #[error("invalid integration branch: {}: {}", commit, reason)]
84 InvalidIntegrationBranch {
85 commit: CommitId,
87 reason: InvalidCommitReason,
89 },
90 #[error("duplicate stage id: {}", id)]
92 DuplicateTopicId {
93 id: u64,
95 },
96 #[error("failed to update the {} ref to {}: {}", stage, base, output)]
98 CreateStageRef {
99 stage: CommitId,
101 base: CommitId,
103 output: String,
105 },
106 #[error(
108 "failed to list the first parent history of the stage branch: {}",
109 output
110 )]
111 ListStageHistory {
112 output: String,
114 },
115 #[error(
117 "failed to get information about a merge on the topic stage: {}",
118 output
119 )]
120 ExtractMergeInfo {
121 output: String,
123 },
124 #[error("invalid stage merge commit {} found: {}", commit, log_info)]
126 InvalidStageMerge {
127 commit: CommitId,
129 log_info: String,
131 },
132 #[error("cannot unstage base")]
134 CannotUnstageBase,
135 #[error("failed to parse date: {}", source)]
137 DateParse {
138 #[source]
140 source: chrono::ParseError,
141 },
142 #[deprecated(since = "4.1.0", note = "No longer used.")]
144 #[error("failed to parse topic ID: {}", source)]
145 IdParse {
146 #[source]
148 source: num::ParseIntError,
149 },
150}
151
152impl StagerError {
153 fn invalid_branch(commit: CommitId, reason: InvalidCommitReason) -> Self {
154 StagerError::InvalidIntegrationBranch {
155 commit,
156 reason,
157 }
158 }
159
160 fn duplicate_topic_id(id: u64) -> Self {
161 StagerError::DuplicateTopicId {
162 id,
163 }
164 }
165
166 fn create_stage_ref(stage: CommitId, base: CommitId, output: &[u8]) -> Self {
167 StagerError::CreateStageRef {
168 stage,
169 base,
170 output: String::from_utf8_lossy(output).into(),
171 }
172 }
173
174 fn list_stage_history(output: &[u8]) -> Self {
175 StagerError::ListStageHistory {
176 output: String::from_utf8_lossy(output).into(),
177 }
178 }
179
180 fn extract_merge_info(output: &[u8]) -> Self {
181 StagerError::ExtractMergeInfo {
182 output: String::from_utf8_lossy(output).into(),
183 }
184 }
185
186 fn invalid_stage_merge(commit: &str, log_info: &[u8]) -> Self {
187 StagerError::InvalidStageMerge {
188 commit: CommitId::new(commit),
189 log_info: String::from_utf8_lossy(log_info).into(),
190 }
191 }
192
193 fn date_parse(source: chrono::ParseError) -> Self {
194 StagerError::DateParse {
195 source,
196 }
197 }
198}
199
200type StagerResult<T> = Result<T, StagerError>;
201
202#[derive(Debug, Clone)]
207pub struct Topic {
208 pub commit: CommitId,
210 pub author: Identity,
212 pub stamp: DateTime<Utc>,
214 pub id: u64,
216 pub name: String,
218 pub url: String,
220}
221
222impl Topic {
223 pub fn new<N, U>(
227 commit: CommitId,
228 author: Identity,
229 stamp: DateTime<Utc>,
230 id: u64,
231 name: N,
232 url: U,
233 ) -> Self
234 where
235 N: Into<String>,
236 U: Into<String>,
237 {
238 Self {
239 commit,
240 author,
241 stamp,
242 id,
243 name: name.into(),
244 url: url.into(),
245 }
246 }
247}
248
249impl Display for Topic {
250 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
251 write!(
252 f,
253 "topic {} at {}, staged by {} at {}",
254 self.name, self.commit, self.author, self.stamp
255 )
256 }
257}
258
259impl PartialEq for Topic {
260 fn eq(&self, rhs: &Self) -> bool {
261 self.commit == rhs.commit && self.id == rhs.id
264 }
265}
266
267#[derive(Debug, Clone, PartialEq)]
269pub struct CandidateTopic {
270 pub old_id: Option<Topic>,
272 pub new_id: Topic,
274}
275
276impl CandidateTopic {
277 fn is_self_consistent(&self) -> bool {
278 self.old_id
279 .as_ref()
280 .map_or(true, |old| old.id == self.new_id.id)
281 }
282}
283
284impl Display for CandidateTopic {
285 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
286 if let Some(ref old_topic) = self.old_id {
287 write!(f, "candidate {}, replacing {}", self.new_id, old_topic)
288 } else {
289 write!(f, "candidate {}, new", self.new_id)
290 }
291 }
292}
293
294#[derive(Debug, Clone, PartialEq)]
296pub struct StagedTopic {
297 pub merge: CommitId,
299 pub topic: Topic,
301}
302
303impl StagedTopic {
304 pub fn commit(&self) -> &CommitId {
306 &self.topic.commit
307 }
308}
309
310impl Display for StagedTopic {
311 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
312 write!(f, "staged {}, via {}", self.topic, self.merge)
313 }
314}
315
316#[non_exhaustive]
318pub enum UnstageReason {
319 MergeConflict(Vec<Conflict>),
321}
322
323impl Debug for UnstageReason {
324 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
325 match *self {
326 UnstageReason::MergeConflict(ref conflicts) => {
327 write!(f, "MergeConflict ( {} conflicts )", conflicts.len())
328 },
329 }
330 }
331}
332
333#[derive(Debug)]
335#[non_exhaustive]
336pub enum OldTopicRemoval {
337 Obsoleted {
339 old_merge: StagedTopic,
341 replacement: Option<StagedTopic>,
343 },
344 Removed(StagedTopic),
346}
347
348impl OldTopicRemoval {
349 pub fn topic(&self) -> &Topic {
351 match *self {
352 OldTopicRemoval::Obsoleted {
353 old_merge: ref ob, ..
354 } => &ob.topic,
355 OldTopicRemoval::Removed(ref rb) => &rb.topic,
356 }
357 }
358}
359
360#[derive(Debug)]
362#[non_exhaustive]
363pub enum IntegrationResult {
364 Staged(StagedTopic),
366 Unstaged(Topic, UnstageReason),
368 AlreadyMerged(Topic),
370 #[deprecated(since = "4.2.0", note = "Unnecessary and unused.")]
372 AlreadyStaged(Topic),
373 Unmerged(Topic, MergeStatus),
375}
376
377impl IntegrationResult {
378 pub fn topic(&self) -> &Topic {
380 #[allow(deprecated)]
381 match *self {
382 IntegrationResult::Staged(ref sb) => &sb.topic,
383 IntegrationResult::Unstaged(ref u, _) => u,
384 IntegrationResult::AlreadyMerged(ref t) => t,
385 IntegrationResult::AlreadyStaged(ref t) => t,
386 IntegrationResult::Unmerged(ref t, _) => t,
387 }
388 }
389
390 pub fn on_stage(&self) -> bool {
392 #[allow(deprecated)]
393 match *self {
394 IntegrationResult::Staged(_)
395 | IntegrationResult::AlreadyMerged(_)
396 | IntegrationResult::AlreadyStaged(_) => true,
397 IntegrationResult::Unstaged(_, _) | IntegrationResult::Unmerged(_, _) => false,
398 }
399 }
400}
401
402type RewoundStage = (Option<StagedTopic>, Vec<StagedTopic>, Option<Topic>);
407
408#[derive(Debug)]
410pub struct StageResult {
411 pub old_topic: Option<OldTopicRemoval>,
413 pub results: Vec<IntegrationResult>,
418}
419
420#[derive(Debug)]
424pub struct Stager {
425 ctx: GitContext,
427 topics: Vec<StagedTopic>,
429}
430
431const STAGE_TOPIC_PREFIX: &str = "Stage topic '";
433const STAGE_TOPIC_SUFFIX: &str = "'";
435const TOPIC_ID_TRAILER: &str = "Topic-id: ";
437const TOPIC_URL_TRAILER: &str = "Topic-url: ";
439
440impl Stager {
441 pub fn new(ctx: &GitContext, base: CommitId) -> Self {
443 Self {
444 ctx: ctx.clone(),
445 topics: vec![Self::make_base_staged_topic(base)],
446 }
447 }
448
449 pub fn from_branch(ctx: &GitContext, base: CommitId, stage: CommitId) -> StagerResult<Self> {
461 let cat_file = ctx
462 .git()
463 .arg("cat-file")
464 .arg("-t")
465 .arg(stage.as_str())
466 .output()
467 .map_err(|err| GitError::subcommand("cat-file", err))?;
468 if cat_file.status.success() {
469 let stage_type = String::from_utf8_lossy(&cat_file.stdout);
471 if stage_type.trim() != "commit" {
472 return Err(StagerError::invalid_branch(
473 stage,
474 InvalidCommitReason::NotACommit,
475 ));
476 }
477 } else {
478 let update_ref = ctx
480 .git()
481 .arg("update-ref")
482 .arg(stage.as_str())
483 .arg(base.as_str())
484 .output()
485 .map_err(|err| GitError::subcommand("update-ref", err))?;
486 if !update_ref.status.success() {
487 return Err(StagerError::create_stage_ref(
488 stage,
489 base,
490 &update_ref.stderr,
491 ));
492 }
493 }
494
495 let is_ancestor = ctx
497 .git()
498 .arg("merge-base")
499 .arg("--is-ancestor")
500 .arg(base.as_str())
501 .arg(stage.as_str())
502 .status()
503 .map_err(|err| GitError::subcommand("merge-base --is-ancestor", err))?;
504 let needs_base_update = !is_ancestor.success();
505
506 debug!(target: "git-topic-stage", "creating a stage branch from {} -> {}", base, stage);
507
508 let merge_base = ctx
510 .git()
511 .arg("merge-base")
512 .arg(base.as_str())
513 .arg(stage.as_str())
514 .output()
515 .map_err(|err| GitError::subcommand("merge-base", err))?;
516 if !merge_base.status.success() {
517 return Err(StagerError::invalid_branch(
518 stage,
519 InvalidCommitReason::NotRelated,
520 ));
521 }
522 let merge_base = String::from_utf8_lossy(&merge_base.stdout);
523
524 let mut topics = vec![Self::make_base_staged_topic(CommitId::new(
525 merge_base.trim(),
526 ))];
527
528 let rev_list = ctx
531 .git()
532 .arg("rev-list")
533 .arg("--first-parent")
534 .arg("--reverse")
535 .arg("--parents")
536 .arg(stage.as_str())
537 .arg(format!("^{}", base))
538 .output()
539 .map_err(|err| GitError::subcommand("rev-list --first-parent", err))?;
540 if !rev_list.status.success() {
541 return Err(StagerError::list_stage_history(&rev_list.stderr));
542 }
543 let merges = String::from_utf8_lossy(&rev_list.stdout);
544
545 let new_topics = merges
547 .lines()
548 .map(|merge| {
549 let revs = merge.split_whitespace().collect::<Vec<_>>();
551 let (rev, parents) = revs[..].split_first().expect("invalid rev-list format");
552 let rev_commit = CommitId::new(*rev);
553
554 if parents.len() == 1 {
559 return Err(StagerError::invalid_branch(
560 rev_commit,
561 InvalidCommitReason::NonMergeCommit,
562 ));
563 }
564
565 if parents.len() > 2 {
567 return Err(StagerError::invalid_branch(
568 rev_commit,
569 InvalidCommitReason::OctopusMerge,
570 ));
571 }
572
573 let commit_info = ctx
576 .git()
577 .arg("log")
578 .arg("--max-count=1")
579 .arg("--pretty=%an%n%ae%n%aI%n%B")
580 .arg(rev)
581 .output()
582 .map_err(|err| GitError::subcommand("log", err))?;
583 if !commit_info.status.success() {
584 return Err(StagerError::extract_merge_info(&commit_info.stderr));
585 }
586 let info = String::from_utf8_lossy(&commit_info.stdout);
587 let info = info.lines().collect::<Vec<_>>();
588
589 if info.len() <= 6 {
591 return Err(StagerError::invalid_stage_merge(rev, &commit_info.stdout));
592 }
593
594 let subject = info[3];
596 if !subject.starts_with(STAGE_TOPIC_PREFIX)
597 || !subject.ends_with(STAGE_TOPIC_SUFFIX)
598 {
599 let reason = InvalidCommitReason::InvalidSubject(subject.into());
600 return Err(StagerError::invalid_branch(rev_commit, reason));
601 }
602 let name =
603 &subject[STAGE_TOPIC_PREFIX.len()..subject.len() - STAGE_TOPIC_SUFFIX.len()];
604
605 let mut id = None;
606 let mut url = None;
607
608 for line in info[5..info.len() - 1].iter().rev() {
610 if line.is_empty() {
611 break;
612 } else if let Some(topic_id) = line.strip_prefix(TOPIC_ID_TRAILER) {
613 id = Some(topic_id.parse().map_err(|err| {
614 let reason = InvalidCommitReason::UnparseableId(format!("{}", err));
615 StagerError::invalid_branch(rev_commit.clone(), reason)
616 })?);
617 } else if let Some(topic_url) = line.strip_prefix(TOPIC_URL_TRAILER) {
618 url = Some(topic_url.to_string());
619 } else {
620 warn!(
622 target: "git-topic-stage",
623 "unrecognized trailer in {}: {}",
624 parents[1],
625 line,
626 );
627 }
628 }
629
630 let url = if let Some(url) = url {
632 url
633 } else {
634 return Err(StagerError::invalid_branch(
635 rev_commit,
636 InvalidCommitReason::MissingUrl,
637 ));
638 };
639
640 let id = match id {
641 Some(0) => {
642 return Err(StagerError::invalid_branch(
643 rev_commit,
644 InvalidCommitReason::ZeroId,
645 ));
646 },
647 Some(id) => id,
648 None => {
649 return Err(StagerError::invalid_branch(
650 rev_commit,
651 InvalidCommitReason::MissingId,
652 ));
653 },
654 };
655
656 info!(
657 target: "git-topic-stage",
658 "found staged topic '{}' from {}",
659 name, url,
660 );
661
662 Ok(StagedTopic {
663 merge: rev_commit,
664 topic: Topic::new(
665 CommitId::new(parents[1]),
666 Identity::new(info[0], info[1]),
667 info[2].parse().map_err(StagerError::date_parse)?,
668 id,
669 name,
670 url,
671 ),
672 })
673 })
674 .collect::<StagerResult<Vec<_>>>()?;
675
676 topics.extend(new_topics);
677
678 let mut ids = HashSet::new();
680 for topic in &topics {
681 if !ids.insert(topic.topic.id) {
682 return Err(StagerError::duplicate_topic_id(topic.topic.id));
683 }
684 }
685
686 let mut stager = Self {
687 ctx: ctx.clone(),
688 topics,
689 };
690
691 if needs_base_update {
694 let base_update = CandidateTopic {
695 old_id: Some(Self::make_base_topic(CommitId::new(merge_base.trim()))),
696 new_id: Self::make_base_topic(base),
697 };
698
699 stager.stage(base_update)?;
700 }
701
702 Ok(stager)
703 }
704
705 pub fn git_context(&self) -> &GitContext {
707 &self.ctx
708 }
709
710 pub fn base(&self) -> &CommitId {
712 self.topics[0].commit()
713 }
714
715 pub fn topics(&self) -> &[StagedTopic] {
717 &self.topics[1..]
718 }
719
720 pub fn find_topic_by_id(&self, id: u64) -> Option<&StagedTopic> {
722 self.topics()
723 .iter()
724 .find(|staged_topic| id == staged_topic.topic.id)
725 }
726
727 pub fn find_topic(&self, topic: &Topic) -> Option<&StagedTopic> {
729 self.topics()
730 .iter()
731 .find(|staged_topic| topic == &staged_topic.topic)
732 }
733
734 pub fn head(&self) -> &CommitId {
736 &self
737 .topics
738 .iter()
739 .last()
740 .expect("expected there to be a HEAD topic on the stage")
741 .merge
742 }
743
744 fn make_base_staged_topic(base: CommitId) -> StagedTopic {
746 StagedTopic {
747 merge: base.clone(),
748 topic: Self::make_base_topic(base),
749 }
750 }
751
752 fn make_base_topic(base: CommitId) -> Topic {
754 Topic {
755 commit: base,
756 author: Identity::new("stager", "stager@example.com"),
757 stamp: Utc::now(),
758 id: 0,
759 name: "base".into(),
760 url: "url".into(),
761 }
762 }
763
764 fn rewind_stage(&mut self, topic: CandidateTopic) -> RewoundStage {
766 let root = topic
768 .old_id
769 .and_then(|old| {
770 self.topics
771 .iter()
772 .position(|staged_topic| old == staged_topic.topic)
773 })
774 .unwrap_or(self.topics.len());
775
776 let mut old_stage = self.topics.drain(root..).collect::<Vec<_>>();
778
779 if self.topics.is_empty() {
780 debug!(target: "git-topic-stage", "rewinding the stage to its base");
781 } else {
782 debug!(target: "git-topic-stage", "rewinding the stage to {}", self.head());
783 }
784
785 let (old_topic, new_topic) = if self.topics.is_empty() {
786 self.topics
788 .push(Self::make_base_staged_topic(topic.new_id.commit));
789
790 (Some(old_stage.remove(0)), None)
793 } else if old_stage.is_empty() {
794 (None, Some(topic.new_id))
796 } else {
797 (Some(old_stage.remove(0)), Some(topic.new_id))
800 };
801
802 (old_topic, old_stage, new_topic)
803 }
804
805 fn replay_stage(
807 &mut self,
808 old_stage: Vec<StagedTopic>,
809 ) -> StagerResult<Vec<IntegrationResult>> {
810 debug!(target: "git-topic-stage", "replaying {} branches into the stage", old_stage.len());
811
812 old_stage
814 .into_iter()
815 .map(|old_staged_topic| {
816 let (staged, res) = self.merge_to_stage(old_staged_topic.topic)?;
817 if let Some(t) = staged {
818 self.topics.push(t);
819 }
820 Ok(res)
821 })
822 .collect()
823 }
824
825 pub fn unstage(&mut self, topic: StagedTopic) -> StagerResult<StageResult> {
827 info!(target: "git-topic-stage", "unstaging a topic: {}", topic);
828
829 if topic.commit() == self.base() {
830 debug!(target: "git-topic-stage", "ignoring a request to unstage the base");
831
832 return Err(StagerError::CannotUnstageBase);
833 }
834
835 let (old_topic, old_stage, new_topic) = self.rewind_stage(CandidateTopic {
836 old_id: Some(topic.topic.clone()),
837 new_id: topic.topic,
838 });
839
840 let results = self.replay_stage(old_stage)?;
841
842 if new_topic.is_none() {
843 warn!(target: "git-topic-stage", "unstage called on the base branch");
845 }
846
847 if let Some(ref old_topic) = old_topic {
848 debug!(target: "git-topic-stage", "unstaged {}", old_topic);
849 }
850
851 let old_branch_result = old_topic.map(OldTopicRemoval::Removed);
853
854 Ok(StageResult {
855 old_topic: old_branch_result,
856 results,
857 })
858 }
859
860 pub fn stage(&mut self, topic: CandidateTopic) -> StagerResult<StageResult> {
866 info!(target: "git-topic-stage", "staging a topic: {}", topic);
867
868 let self_consistent = topic.is_self_consistent();
869 if !self_consistent || topic.old_id.is_none() {
870 if !self_consistent {
871 warn!(
872 target: "git-topic-stage",
873 "inconsistent candidate topic submission: {}",
874 topic,
875 );
876 }
877
878 if self
879 .topics
880 .iter()
881 .any(|staged| staged.topic.id == topic.new_id.id)
882 {
883 return Err(StagerError::duplicate_topic_id(topic.new_id.id));
884 }
885 }
886
887 let (old_topic, old_stage, new_topic) = self.rewind_stage(topic);
889
890 let mut results = self.replay_stage(old_stage)?;
891
892 let old_branch_result = if let Some(topic) = new_topic {
893 let (staged, res) = self.merge_to_stage(topic)?;
895
896 if let Some(sb) = staged.clone() {
897 self.topics.push(sb);
898 }
899 results.push(res);
900
901 old_topic.map(|topic| {
903 OldTopicRemoval::Obsoleted {
904 old_merge: topic,
905 replacement: staged,
906 }
907 })
908 } else {
909 old_topic.map(|topic| {
911 OldTopicRemoval::Obsoleted {
912 old_merge: topic,
913 replacement: Some(self.topics[0].clone()),
914 }
915 })
916 };
917
918 Ok(StageResult {
919 old_topic: old_branch_result,
920 results,
921 })
922 }
923
924 pub fn clear(&mut self) -> Vec<StagedTopic> {
928 let new_id = Self::make_base_staged_topic(self.base().clone()).topic;
929 let old_id = Some(new_id.clone());
930
931 info!(target: "git-topic-stage", "clearing the stage");
932
933 self.rewind_stage(CandidateTopic {
934 old_id,
935 new_id,
936 })
937 .1
938 }
939
940 fn merge_to_stage(
942 &self,
943 topic: Topic,
944 ) -> StagerResult<(Option<StagedTopic>, IntegrationResult)> {
945 let base_commit = self.base();
946 let head_commit = self.head();
947 let topic_commit = topic.commit.clone();
948
949 debug!(target: "git-topic-stage", "merging {} into the stage", topic);
950
951 let merge_status = self.ctx.mergeable(base_commit, &topic_commit)?;
955 match merge_status {
956 MergeStatus::Mergeable(_) => (),
957 MergeStatus::AlreadyMerged => {
958 debug!(target: "git-topic-stage", "rejecting: already merged");
960 return Ok((None, IntegrationResult::AlreadyMerged(topic)));
961 },
962 status @ MergeStatus::NoCommonHistory => {
963 debug!(target: "git-topic-stage", "rejecting: no common history");
965 return Ok((None, IntegrationResult::Unmerged(topic, status)));
966 },
967 };
968
969 let bases = match self.ctx.mergeable(head_commit, &topic_commit)? {
973 MergeStatus::Mergeable(bases) => Some(bases),
974 MergeStatus::AlreadyMerged => None,
975 status @ MergeStatus::NoCommonHistory => {
976 debug!(target: "git-topic-stage", "rejecting: no common history with the stage");
978 return Ok((None, IntegrationResult::Unmerged(topic, status)));
979 },
980 };
981
982 let workarea = self.ctx.prepare(head_commit)?;
984
985 let merge_result = if let Some(bases) = bases {
987 workarea.setup_merge(&bases, head_commit, &topic_commit)?
988 } else {
989 workarea.setup_update_merge(head_commit, &topic_commit)?
990 };
991
992 let mut merge_command = match merge_result {
993 MergeResult::Conflict(conflicts) => {
994 debug!(target: "git-topic-stage", "rejecting: conflicts: {}", conflicts.len());
995
996 return Ok((
997 None,
998 IntegrationResult::Unstaged(topic, UnstageReason::MergeConflict(conflicts)),
999 ));
1000 },
1001 MergeResult::Ready(command) => command,
1002 };
1003
1004 merge_command.author(&topic.author).author_date(topic.stamp);
1006
1007 let merge_commit = merge_command.commit(self.create_message(self.base(), &topic))?;
1009 let staged_topic = StagedTopic {
1010 merge: merge_commit,
1011 topic,
1012 };
1013
1014 debug!(target: "git-topic-stage", "successfully staged as {}", staged_topic);
1015
1016 Ok((
1017 Some(staged_topic.clone()),
1018 IntegrationResult::Staged(staged_topic),
1019 ))
1020 }
1021
1022 fn create_message(&self, _: &CommitId, topic: &Topic) -> String {
1024 format!(
1025 "{}{}{}\n\n{}{}\n{}{}\n",
1026 STAGE_TOPIC_PREFIX,
1027 topic.name,
1028 STAGE_TOPIC_SUFFIX,
1029 TOPIC_ID_TRAILER,
1030 topic.id,
1031 TOPIC_URL_TRAILER,
1032 topic.url,
1033 )
1034 }
1035}