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 AlreadyStaged(Topic),
372 Unmerged(Topic, MergeStatus),
374}
375
376impl IntegrationResult {
377 pub fn topic(&self) -> &Topic {
379 match *self {
380 IntegrationResult::Staged(ref sb) => &sb.topic,
381 IntegrationResult::Unstaged(ref u, _) => u,
382 IntegrationResult::AlreadyMerged(ref t) => t,
383 IntegrationResult::AlreadyStaged(ref t) => t,
384 IntegrationResult::Unmerged(ref t, _) => t,
385 }
386 }
387
388 pub fn on_stage(&self) -> bool {
390 match *self {
391 IntegrationResult::Staged(_)
392 | IntegrationResult::AlreadyMerged(_)
393 | IntegrationResult::AlreadyStaged(_) => true,
394 IntegrationResult::Unstaged(_, _) | IntegrationResult::Unmerged(_, _) => false,
395 }
396 }
397}
398
399type RewoundStage = (Option<StagedTopic>, Vec<StagedTopic>, Option<Topic>);
404
405#[derive(Debug)]
407pub struct StageResult {
408 pub old_topic: Option<OldTopicRemoval>,
410 pub results: Vec<IntegrationResult>,
415}
416
417#[derive(Debug)]
421pub struct Stager {
422 ctx: GitContext,
424 topics: Vec<StagedTopic>,
426}
427
428const STAGE_TOPIC_PREFIX: &str = "Stage topic '";
430const STAGE_TOPIC_SUFFIX: &str = "'";
432const TOPIC_ID_TRAILER: &str = "Topic-id: ";
434const TOPIC_URL_TRAILER: &str = "Topic-url: ";
436
437impl Stager {
438 pub fn new(ctx: &GitContext, base: CommitId) -> Self {
440 Self {
441 ctx: ctx.clone(),
442 topics: vec![Self::make_base_staged_topic(base)],
443 }
444 }
445
446 pub fn from_branch(ctx: &GitContext, base: CommitId, stage: CommitId) -> StagerResult<Self> {
458 let cat_file = ctx
459 .git()
460 .arg("cat-file")
461 .arg("-t")
462 .arg(stage.as_str())
463 .output()
464 .map_err(|err| GitError::subcommand("cat-file", err))?;
465 if cat_file.status.success() {
466 let stage_type = String::from_utf8_lossy(&cat_file.stdout);
468 if stage_type.trim() != "commit" {
469 return Err(StagerError::invalid_branch(
470 stage,
471 InvalidCommitReason::NotACommit,
472 ));
473 }
474 } else {
475 let update_ref = ctx
477 .git()
478 .arg("update-ref")
479 .arg(stage.as_str())
480 .arg(base.as_str())
481 .output()
482 .map_err(|err| GitError::subcommand("update-ref", err))?;
483 if !update_ref.status.success() {
484 return Err(StagerError::create_stage_ref(
485 stage,
486 base,
487 &update_ref.stderr,
488 ));
489 }
490 }
491
492 let is_ancestor = ctx
494 .git()
495 .arg("merge-base")
496 .arg("--is-ancestor")
497 .arg(base.as_str())
498 .arg(stage.as_str())
499 .status()
500 .map_err(|err| GitError::subcommand("merge-base --is-ancestor", err))?;
501 let needs_base_update = !is_ancestor.success();
502
503 debug!(target: "git-topic-stage", "creating a stage branch from {} -> {}", base, stage);
504
505 let merge_base = ctx
507 .git()
508 .arg("merge-base")
509 .arg(base.as_str())
510 .arg(stage.as_str())
511 .output()
512 .map_err(|err| GitError::subcommand("merge-base", err))?;
513 if !merge_base.status.success() {
514 return Err(StagerError::invalid_branch(
515 stage,
516 InvalidCommitReason::NotRelated,
517 ));
518 }
519 let merge_base = String::from_utf8_lossy(&merge_base.stdout);
520
521 let mut topics = vec![Self::make_base_staged_topic(CommitId::new(
522 merge_base.trim(),
523 ))];
524
525 let rev_list = ctx
528 .git()
529 .arg("rev-list")
530 .arg("--first-parent")
531 .arg("--reverse")
532 .arg("--parents")
533 .arg(stage.as_str())
534 .arg(format!("^{}", base))
535 .output()
536 .map_err(|err| GitError::subcommand("rev-list --first-parent", err))?;
537 if !rev_list.status.success() {
538 return Err(StagerError::list_stage_history(&rev_list.stderr));
539 }
540 let merges = String::from_utf8_lossy(&rev_list.stdout);
541
542 let new_topics = merges
544 .lines()
545 .map(|merge| {
546 let revs = merge.split_whitespace().collect::<Vec<_>>();
548 let (rev, parents) = revs[..].split_first().expect("invalid rev-list format");
549 let rev_commit = CommitId::new(*rev);
550
551 if parents.len() == 1 {
556 return Err(StagerError::invalid_branch(
557 rev_commit,
558 InvalidCommitReason::NonMergeCommit,
559 ));
560 }
561
562 if parents.len() > 2 {
564 return Err(StagerError::invalid_branch(
565 rev_commit,
566 InvalidCommitReason::OctopusMerge,
567 ));
568 }
569
570 let commit_info = ctx
573 .git()
574 .arg("log")
575 .arg("--max-count=1")
576 .arg("--pretty=%an%n%ae%n%aI%n%B")
577 .arg(rev)
578 .output()
579 .map_err(|err| GitError::subcommand("log", err))?;
580 if !commit_info.status.success() {
581 return Err(StagerError::extract_merge_info(&commit_info.stderr));
582 }
583 let info = String::from_utf8_lossy(&commit_info.stdout);
584 let info = info.lines().collect::<Vec<_>>();
585
586 if info.len() <= 6 {
588 return Err(StagerError::invalid_stage_merge(rev, &commit_info.stdout));
589 }
590
591 let subject = info[3];
593 if !subject.starts_with(STAGE_TOPIC_PREFIX)
594 || !subject.ends_with(STAGE_TOPIC_SUFFIX)
595 {
596 let reason = InvalidCommitReason::InvalidSubject(subject.into());
597 return Err(StagerError::invalid_branch(rev_commit, reason));
598 }
599 let name =
600 &subject[STAGE_TOPIC_PREFIX.len()..subject.len() - STAGE_TOPIC_SUFFIX.len()];
601
602 let mut id = None;
603 let mut url = None;
604
605 for line in info[5..info.len() - 1].iter().rev() {
607 if line.is_empty() {
608 break;
609 } else if let Some(topic_id) = line.strip_prefix(TOPIC_ID_TRAILER) {
610 id = Some(topic_id.parse().map_err(|err| {
611 let reason = InvalidCommitReason::UnparseableId(format!("{}", err));
612 StagerError::invalid_branch(rev_commit.clone(), reason)
613 })?);
614 } else if let Some(topic_url) = line.strip_prefix(TOPIC_URL_TRAILER) {
615 url = Some(topic_url.to_string());
616 } else {
617 warn!(
619 target: "git-topic-stage",
620 "unrecognized trailer in {}: {}",
621 parents[1],
622 line,
623 );
624 }
625 }
626
627 let url = if let Some(url) = url {
629 url
630 } else {
631 return Err(StagerError::invalid_branch(
632 rev_commit,
633 InvalidCommitReason::MissingUrl,
634 ));
635 };
636
637 let id = match id {
638 Some(0) => {
639 return Err(StagerError::invalid_branch(
640 rev_commit,
641 InvalidCommitReason::ZeroId,
642 ));
643 },
644 Some(id) => id,
645 None => {
646 return Err(StagerError::invalid_branch(
647 rev_commit,
648 InvalidCommitReason::MissingId,
649 ));
650 },
651 };
652
653 info!(
654 target: "git-topic-stage",
655 "found staged topic '{}' from {}",
656 name, url,
657 );
658
659 Ok(StagedTopic {
660 merge: rev_commit,
661 topic: Topic::new(
662 CommitId::new(parents[1]),
663 Identity::new(info[0], info[1]),
664 info[2].parse().map_err(StagerError::date_parse)?,
665 id,
666 name,
667 url,
668 ),
669 })
670 })
671 .collect::<StagerResult<Vec<_>>>()?;
672
673 topics.extend(new_topics);
674
675 let mut ids = HashSet::new();
677 for topic in &topics {
678 if !ids.insert(topic.topic.id) {
679 return Err(StagerError::duplicate_topic_id(topic.topic.id));
680 }
681 }
682
683 let mut stager = Self {
684 ctx: ctx.clone(),
685 topics,
686 };
687
688 if needs_base_update {
691 let base_update = CandidateTopic {
692 old_id: Some(Self::make_base_topic(CommitId::new(merge_base.trim()))),
693 new_id: Self::make_base_topic(base),
694 };
695
696 stager.stage(base_update)?;
697 }
698
699 Ok(stager)
700 }
701
702 pub fn git_context(&self) -> &GitContext {
704 &self.ctx
705 }
706
707 pub fn base(&self) -> &CommitId {
709 self.topics[0].commit()
710 }
711
712 pub fn topics(&self) -> &[StagedTopic] {
714 &self.topics[1..]
715 }
716
717 pub fn find_topic_by_id(&self, id: u64) -> Option<&StagedTopic> {
719 self.topics()
720 .iter()
721 .find(|staged_topic| id == staged_topic.topic.id)
722 }
723
724 pub fn find_topic(&self, topic: &Topic) -> Option<&StagedTopic> {
726 self.topics()
727 .iter()
728 .find(|staged_topic| topic == &staged_topic.topic)
729 }
730
731 pub fn head(&self) -> &CommitId {
733 &self
734 .topics
735 .iter()
736 .last()
737 .expect("expected there to be a HEAD topic on the stage")
738 .merge
739 }
740
741 fn make_base_staged_topic(base: CommitId) -> StagedTopic {
743 StagedTopic {
744 merge: base.clone(),
745 topic: Self::make_base_topic(base),
746 }
747 }
748
749 fn make_base_topic(base: CommitId) -> Topic {
751 Topic {
752 commit: base,
753 author: Identity::new("stager", "stager@example.com"),
754 stamp: Utc::now(),
755 id: 0,
756 name: "base".into(),
757 url: "url".into(),
758 }
759 }
760
761 fn rewind_stage(&mut self, topic: CandidateTopic) -> RewoundStage {
763 let root = topic
765 .old_id
766 .and_then(|old| {
767 self.topics
768 .iter()
769 .position(|staged_topic| old == staged_topic.topic)
770 })
771 .unwrap_or(self.topics.len());
772
773 let mut old_stage = self.topics.drain(root..).collect::<Vec<_>>();
775
776 if self.topics.is_empty() {
777 debug!(target: "git-topic-stage", "rewinding the stage to its base");
778 } else {
779 debug!(target: "git-topic-stage", "rewinding the stage to {}", self.head());
780 }
781
782 let (old_topic, new_topic) = if self.topics.is_empty() {
783 self.topics
785 .push(Self::make_base_staged_topic(topic.new_id.commit));
786
787 (Some(old_stage.remove(0)), None)
790 } else if old_stage.is_empty() {
791 (None, Some(topic.new_id))
793 } else {
794 (Some(old_stage.remove(0)), Some(topic.new_id))
797 };
798
799 (old_topic, old_stage, new_topic)
800 }
801
802 fn replay_stage(
804 &mut self,
805 old_stage: Vec<StagedTopic>,
806 ) -> StagerResult<Vec<IntegrationResult>> {
807 debug!(target: "git-topic-stage", "replaying {} branches into the stage", old_stage.len());
808
809 old_stage
811 .into_iter()
812 .map(|old_staged_topic| {
813 let (staged, res) = self.merge_to_stage(old_staged_topic.topic)?;
814 if let Some(t) = staged {
815 self.topics.push(t);
816 }
817 Ok(res)
818 })
819 .collect()
820 }
821
822 pub fn unstage(&mut self, topic: StagedTopic) -> StagerResult<StageResult> {
824 info!(target: "git-topic-stage", "unstaging a topic: {}", topic);
825
826 if topic.commit() == self.base() {
827 debug!(target: "git-topic-stage", "ignoring a request to unstage the base");
828
829 return Err(StagerError::CannotUnstageBase);
830 }
831
832 let (old_topic, old_stage, new_topic) = self.rewind_stage(CandidateTopic {
833 old_id: Some(topic.topic.clone()),
834 new_id: topic.topic,
835 });
836
837 let results = self.replay_stage(old_stage)?;
838
839 if new_topic.is_none() {
840 warn!(target: "git-topic-stage", "unstage called on the base branch");
842 }
843
844 if let Some(ref old_topic) = old_topic {
845 debug!(target: "git-topic-stage", "unstaged {}", old_topic);
846 }
847
848 let old_branch_result = old_topic.map(OldTopicRemoval::Removed);
850
851 Ok(StageResult {
852 old_topic: old_branch_result,
853 results,
854 })
855 }
856
857 pub fn stage(&mut self, topic: CandidateTopic) -> StagerResult<StageResult> {
863 info!(target: "git-topic-stage", "staging a topic: {}", topic);
864
865 let self_consistent = topic.is_self_consistent();
866 if !self_consistent || topic.old_id.is_none() {
867 if !self_consistent {
868 warn!(
869 target: "git-topic-stage",
870 "inconsistent candidate topic submission: {}",
871 topic,
872 );
873 }
874
875 if self
876 .topics
877 .iter()
878 .any(|staged| staged.topic.id == topic.new_id.id)
879 {
880 return Err(StagerError::duplicate_topic_id(topic.new_id.id));
881 }
882 }
883
884 let (old_topic, old_stage, new_topic) = self.rewind_stage(topic);
886
887 let mut results = self.replay_stage(old_stage)?;
888
889 let old_branch_result = if let Some(topic) = new_topic {
890 let (staged, res) = self.merge_to_stage(topic)?;
892
893 if let Some(sb) = staged.clone() {
894 self.topics.push(sb);
895 }
896 results.push(res);
897
898 old_topic.map(|topic| {
900 OldTopicRemoval::Obsoleted {
901 old_merge: topic,
902 replacement: staged,
903 }
904 })
905 } else {
906 old_topic.map(|topic| {
908 OldTopicRemoval::Obsoleted {
909 old_merge: topic,
910 replacement: Some(self.topics[0].clone()),
911 }
912 })
913 };
914
915 Ok(StageResult {
916 old_topic: old_branch_result,
917 results,
918 })
919 }
920
921 pub fn clear(&mut self) -> Vec<StagedTopic> {
925 let new_id = Self::make_base_staged_topic(self.base().clone()).topic;
926 let old_id = Some(new_id.clone());
927
928 info!(target: "git-topic-stage", "clearing the stage");
929
930 self.rewind_stage(CandidateTopic {
931 old_id,
932 new_id,
933 })
934 .1
935 }
936
937 fn merge_to_stage(
939 &self,
940 topic: Topic,
941 ) -> StagerResult<(Option<StagedTopic>, IntegrationResult)> {
942 let base_commit = self.base();
943 let head_commit = self.head();
944 let topic_commit = topic.commit.clone();
945
946 debug!(target: "git-topic-stage", "merging {} into the stage", topic);
947
948 let merge_status = self.ctx.mergeable(base_commit, &topic_commit)?;
952 match merge_status {
953 MergeStatus::Mergeable(_) => (),
954 MergeStatus::AlreadyMerged => {
955 debug!(target: "git-topic-stage", "rejecting: already merged");
957 return Ok((None, IntegrationResult::AlreadyMerged(topic)));
958 },
959 status @ MergeStatus::NoCommonHistory => {
960 debug!(target: "git-topic-stage", "rejecting: no common history");
962 return Ok((None, IntegrationResult::Unmerged(topic, status)));
963 },
964 };
965
966 let bases = match self.ctx.mergeable(head_commit, &topic_commit)? {
970 MergeStatus::Mergeable(bases) => bases,
971 MergeStatus::AlreadyMerged => {
972 debug!(target: "git-topic-stage", "rejecting: already staged");
974 return Ok((None, IntegrationResult::AlreadyStaged(topic)));
975 },
976 status @ MergeStatus::NoCommonHistory => {
977 debug!(target: "git-topic-stage", "rejecting: no common history with the stage");
979 return Ok((None, IntegrationResult::Unmerged(topic, status)));
980 },
981 };
982
983 let workarea = self.ctx.prepare(head_commit)?;
985
986 let merge_result = workarea.setup_merge(&bases, head_commit, &topic_commit)?;
988
989 let mut merge_command = match merge_result {
990 MergeResult::Conflict(conflicts) => {
991 debug!(target: "git-topic-stage", "rejecting: conflicts: {}", conflicts.len());
992
993 return Ok((
994 None,
995 IntegrationResult::Unstaged(topic, UnstageReason::MergeConflict(conflicts)),
996 ));
997 },
998 MergeResult::Ready(command) => command,
999 };
1000
1001 merge_command.author(&topic.author).author_date(topic.stamp);
1003
1004 let merge_commit = merge_command.commit(self.create_message(self.base(), &topic))?;
1006 let staged_topic = StagedTopic {
1007 merge: merge_commit,
1008 topic,
1009 };
1010
1011 debug!(target: "git-topic-stage", "successfully staged as {}", staged_topic);
1012
1013 Ok((
1014 Some(staged_topic.clone()),
1015 IntegrationResult::Staged(staged_topic),
1016 ))
1017 }
1018
1019 fn create_message(&self, _: &CommitId, topic: &Topic) -> String {
1021 format!(
1022 "{}{}{}\n\n{}{}\n{}{}\n",
1023 STAGE_TOPIC_PREFIX,
1024 topic.name,
1025 STAGE_TOPIC_SUFFIX,
1026 TOPIC_ID_TRAILER,
1027 topic.id,
1028 TOPIC_URL_TRAILER,
1029 topic.url,
1030 )
1031 }
1032}