use crates::chrono::{DateTime, Utc};
use crates::git_workarea::{CommitId, Conflict, GitContext, Identity, MergeResult, MergeStatus};
use error::*;
use std::fmt::{self, Debug, Display};
#[derive(Debug, Clone)]
pub struct Topic {
pub commit: CommitId,
pub author: Identity,
pub stamp: DateTime<Utc>,
pub id: u64,
pub name: String,
pub url: String,
}
impl Topic {
pub fn new<N, U>(commit: CommitId, author: Identity, stamp: DateTime<Utc>, id: u64, name: N,
url: U)
-> Self
where N: ToString,
U: ToString,
{
Topic {
commit: commit,
author: author,
stamp: stamp,
id: id,
name: name.to_string(),
url: url.to_string(),
}
}
}
impl Display for Topic {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f,
"topic {} at {}, staged by {} at {}",
self.name,
self.commit,
self.author,
self.stamp)
}
}
impl PartialEq for Topic {
fn eq(&self, rhs: &Self) -> bool {
self.commit == rhs.commit && self.id == rhs.id
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CandidateTopic {
pub old_id: Option<Topic>,
pub new_id: Topic,
}
impl Display for CandidateTopic {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if let Some(ref old_topic) = self.old_id {
write!(f, "candidate {}, replacing {}", self.new_id, old_topic)
} else {
write!(f, "candidate {}, new", self.new_id)
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct StagedTopic {
pub merge: CommitId,
pub topic: Topic,
}
impl StagedTopic {
#[deprecated(since="1.1.0", note="access the member directly instead")]
pub fn topic(&self) -> &Topic {
&self.topic
}
pub fn commit(&self) -> &CommitId {
&self.topic.commit
}
}
impl Display for StagedTopic {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "staged {}, via {}", self.topic, self.merge)
}
}
pub enum UnstageReason {
MergeConflict(Vec<Conflict>),
}
impl Debug for UnstageReason {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
UnstageReason::MergeConflict(ref conflicts) => {
write!(f, "MergeConflict ( {} conflicts )", conflicts.len())
},
}
}
}
#[derive(Debug)]
pub enum OldTopicRemoval {
Obsoleted {
old_merge: StagedTopic,
replacement: Option<StagedTopic>,
},
Removed(StagedTopic),
}
impl OldTopicRemoval {
pub fn topic(&self) -> &Topic {
match *self {
OldTopicRemoval::Obsoleted { old_merge: ref ob, .. } => &ob.topic,
OldTopicRemoval::Removed(ref rb) => &rb.topic,
}
}
#[deprecated(since="1.1.0", note="renamed to topic()")]
pub fn branch(&self) -> &Topic {
self.topic()
}
}
#[derive(Debug)]
pub enum IntegrationResult {
Staged(StagedTopic),
Unstaged(Topic, UnstageReason),
Unmerged(Topic, MergeStatus),
}
impl IntegrationResult {
pub fn topic(&self) -> &Topic {
match *self {
IntegrationResult::Staged(ref sb) => &sb.topic,
IntegrationResult::Unstaged(ref u, _) => u,
IntegrationResult::Unmerged(ref t, _) => t,
}
}
pub fn on_stage(&self) -> bool {
match *self {
IntegrationResult::Staged(_) => true,
IntegrationResult::Unstaged(_, _) |
IntegrationResult::Unmerged(_, _) => false,
}
}
}
type RewoundStage = (Option<StagedTopic>, Vec<StagedTopic>, Option<Topic>);
#[derive(Debug)]
pub struct StageResult {
pub old_topic: Option<OldTopicRemoval>,
pub results: Vec<IntegrationResult>,
}
#[derive(Debug)]
pub struct Stager {
ctx: GitContext,
topics: Vec<StagedTopic>,
identity: Identity,
}
static STAGE_TOPIC_PREFIX: &'static str = "Stage topic '";
static STAGE_TOPIC_SUFFIX: &'static str = "'";
static TOPIC_ID_TRAILER: &'static str = "Topic-id: ";
static TOPIC_URL_TRAILER: &'static str = "Topic-url: ";
impl Stager {
pub fn new(ctx: &GitContext, base: CommitId, identity: Identity) -> Self {
Stager {
ctx: ctx.clone(),
topics: vec![Self::make_base_staged_topic(base)],
identity: identity,
}
}
pub fn from_branch(ctx: &GitContext, base: CommitId, stage: CommitId, identity: Identity)
-> Result<Self> {
let cat_file = ctx.git()
.arg("cat-file")
.arg("-t")
.arg(stage.as_str())
.output()
.chain_err(|| "failed to construct cat-file command")?;
if cat_file.status.success() {
let stage_type = String::from_utf8_lossy(&cat_file.stdout);
if stage_type.trim() != "commit" {
bail!(ErrorKind::InvalidIntegrationBranch(stage, InvalidCommitReason::NotACommit));
}
} else {
let update_ref = ctx.git()
.arg("update-ref")
.arg(stage.as_str())
.arg(base.as_str())
.output()
.chain_err(|| "failed to construct update-ref command")?;
if !update_ref.status.success() {
bail!(ErrorKind::Git(format!("failed to update the {} ref to {}: {}",
stage,
base,
String::from_utf8_lossy(&update_ref.stderr))));
}
}
let is_ancestor = ctx.git()
.arg("merge-base")
.arg("--is-ancestor")
.arg(base.as_str())
.arg(stage.as_str())
.status()
.chain_err(|| "failed to construct merge-base command")?;
let needs_base_update = !is_ancestor.success();
debug!(target: "git-topic-stage", "creating a stage branch from {} -> {}", base, stage);
let rev_list = ctx.git()
.arg("rev-list")
.arg("--first-parent")
.arg("--reverse")
.arg("--parents")
.arg(stage.as_str())
.arg(&format!("^{}", base))
.output()
.chain_err(|| "failed to construct rev-list command")?;
if !rev_list.status.success() {
bail!(ErrorKind::Git(format!("failed to list the first parent history of the stage \
branch: {}",
String::from_utf8_lossy(&rev_list.stderr))));
}
let merges = String::from_utf8_lossy(&rev_list.stdout);
let merge_base = ctx.git()
.arg("merge-base")
.arg(base.as_str())
.arg(stage.as_str())
.output()
.chain_err(|| "failed to construct merge-base command")?;
if !merge_base.status.success() {
bail!(ErrorKind::InvalidIntegrationBranch(stage, InvalidCommitReason::NotRelated));
}
let merge_base = String::from_utf8_lossy(&merge_base.stdout);
let mut topics = vec![Self::make_base_staged_topic(CommitId::new(merge_base.trim()))];
let new_topics = merges.lines()
.map(|merge| {
let revs = merge.split_whitespace().collect::<Vec<_>>();
let (rev, parents) = revs[..].split_first().expect("invalid rev-list format");
let rev_commit = CommitId::new(rev);
if parents.len() == 1 {
bail!(ErrorKind::InvalidIntegrationBranch(rev_commit,
InvalidCommitReason::NonMergeCommit));
}
if parents.len() > 2 {
bail!(ErrorKind::InvalidIntegrationBranch(rev_commit,
InvalidCommitReason::OctopusMerge));
}
let commit_info = ctx.git()
.arg("log")
.arg("--max-count=1")
.arg("--pretty=%an%n%ae%n%aI%n%B")
.arg(rev)
.output()
.chain_err(|| "failed to construct log command")?;
if !commit_info.status.success() {
bail!(ErrorKind::Git(format!("failed to get information about a merge on \
the topic stage: {}",
String::from_utf8_lossy(&commit_info.stderr))));
}
let info = String::from_utf8_lossy(&commit_info.stdout);
let info = info.lines().collect::<Vec<_>>();
assert!(info.len() > 6,
"expected at least 6 lines of information from the merge message, got {}",
info.len());
let subject = info[3];
if !subject.starts_with(STAGE_TOPIC_PREFIX) ||
!subject.ends_with(STAGE_TOPIC_SUFFIX) {
let reason = InvalidCommitReason::InvalidSubject(subject.to_string());
bail!(ErrorKind::InvalidIntegrationBranch(rev_commit, reason));
}
let name = &subject[STAGE_TOPIC_PREFIX.len()..
subject.len() - STAGE_TOPIC_SUFFIX.len()];
let mut id = None;
let mut url = None;
for line in info[5..info.len() - 1].iter().rev() {
if line.is_empty() {
break;
} else if line.starts_with(TOPIC_ID_TRAILER) {
id = Some(line[TOPIC_ID_TRAILER.len()..].parse()
.chain_err(|| ErrorKind::IdParse)?);
} else if line.starts_with(TOPIC_URL_TRAILER) {
url = Some(line[TOPIC_URL_TRAILER.len()..].to_string());
} else {
warn!(target: "git-topic-stage",
"unrecognized trailier in {}: {}",
parents[1],
line);
}
}
let url = if let Some(url) = url {
url
} else {
bail!(ErrorKind::InvalidIntegrationBranch(rev_commit,
InvalidCommitReason::MissingUrl));
};
let id = match id {
Some(0) => {
bail!(ErrorKind::InvalidIntegrationBranch(rev_commit,
InvalidCommitReason::ZeroId));
},
Some(id) => id,
None => {
bail!(ErrorKind::InvalidIntegrationBranch(rev_commit,
InvalidCommitReason::MissingId));
},
};
info!(target: "git-topic-stage",
"found staged topic '{}' from {}",
name, url);
Ok(StagedTopic {
merge: rev_commit,
topic: Topic::new(CommitId::new(parents[1]),
Identity::new(info[0], info[1]),
info[2].parse().chain_err(|| ErrorKind::DateParse)?,
id,
name,
url),
})
})
.collect::<Result<Vec<_>>>()?;
topics.extend(new_topics.into_iter());
let mut stager = Stager {
ctx: ctx.clone(),
topics: topics,
identity: identity,
};
if needs_base_update {
let base_update = CandidateTopic {
old_id: Some(Self::make_base_topic(CommitId::new(merge_base.trim()))),
new_id: Self::make_base_topic(base),
};
stager.stage(base_update)?;
}
Ok(stager)
}
pub fn git_context(&self) -> &GitContext {
&self.ctx
}
pub fn identity(&self) -> &Identity {
&self.identity
}
pub fn base(&self) -> &CommitId {
self.topics[0].commit()
}
pub fn topics(&self) -> &[StagedTopic] {
&self.topics[1..]
}
pub fn find_topic_by_id(&self, id: u64) -> Option<&StagedTopic> {
self.topics()
.iter()
.find(|staged_topic| id == staged_topic.topic.id)
}
pub fn find_topic(&self, topic: &Topic) -> Option<&StagedTopic> {
self.topics()
.iter()
.find(|staged_topic| topic == &staged_topic.topic)
}
pub fn head(&self) -> &CommitId {
&self.topics.iter().last().expect("expected there to be a HEAD topic on the stage").merge
}
fn make_base_staged_topic(base: CommitId) -> StagedTopic {
StagedTopic {
merge: base.clone(),
topic: Self::make_base_topic(base),
}
}
fn make_base_topic(base: CommitId) -> Topic {
Topic {
commit: base,
author: Identity::new("stager", "stager@example.com"),
stamp: Utc::now(),
id: 0,
name: "base".to_string(),
url: "url".to_string(),
}
}
fn rewind_stage(&mut self, topic: CandidateTopic) -> RewoundStage {
let root = topic.old_id
.and_then(|old| self.topics.iter().position(|staged_topic| old == staged_topic.topic))
.unwrap_or_else(|| self.topics.len());
let mut old_stage = self.topics.drain(root..).collect::<Vec<_>>();
if self.topics.is_empty() {
debug!(target: "git-topic-stage", "rewinding the stage to its base");
} else {
debug!(target: "git-topic-stage", "rewinding the stage to {}", self.head());
}
let (old_topic, new_topic) = if self.topics.is_empty() {
self.topics.push(Self::make_base_staged_topic(topic.new_id.commit));
(Some(old_stage.remove(0)), None)
} else if old_stage.is_empty() {
(None, Some(topic.new_id))
} else {
(Some(old_stage.remove(0)), Some(topic.new_id))
};
(old_topic, old_stage, new_topic)
}
fn replay_stage(&mut self, old_stage: Vec<StagedTopic>) -> Result<Vec<IntegrationResult>> {
debug!(target: "git-topic-stage", "replaying {} branches into the stage", old_stage.len());
old_stage.into_iter()
.map(|old_staged_topic| {
let (staged, res) = self.merge_to_stage(old_staged_topic.topic)?;
staged.map(|t| self.topics.push(t));
Ok(res)
})
.collect()
}
pub fn unstage(&mut self, topic: StagedTopic) -> Result<StageResult> {
info!(target: "git-topic-stage", "unstaging a topic: {}", topic);
if topic.commit() == self.base() {
debug!(target: "git-topic-stage", "ignoring a request to unstage the base");
bail!(ErrorKind::CannotUnstageBase);
}
let (old_topic, old_stage, new_topic) = self.rewind_stage(CandidateTopic {
old_id: Some(topic.topic.clone()),
new_id: topic.topic,
});
let results = self.replay_stage(old_stage)?;
if new_topic.is_none() {
warn!(target: "git-topic-stage", "unstage called on the base branch");
}
if let Some(ref old_topic) = old_topic {
debug!(target: "git-topic-stage", "unstaged {}", old_topic);
}
let old_branch_result = old_topic.map(OldTopicRemoval::Removed);
Ok(StageResult {
old_topic: old_branch_result,
results: results,
})
}
pub fn stage(&mut self, topic: CandidateTopic) -> Result<StageResult> {
info!(target: "git-topic-stage", "staging a topic: {}", topic);
let (old_topic, old_stage, new_topic) = self.rewind_stage(topic);
let mut results = self.replay_stage(old_stage)?;
let old_branch_result = if let Some(topic) = new_topic {
let (staged, res) = self.merge_to_stage(topic)?;
staged.clone().map(|sb| self.topics.push(sb));
results.push(res);
old_topic.map(|topic| {
OldTopicRemoval::Obsoleted {
old_merge: topic,
replacement: staged,
}
})
} else {
old_topic.map(|topic| {
OldTopicRemoval::Obsoleted {
old_merge: topic,
replacement: Some(self.topics[0].clone()),
}
})
};
Ok(StageResult {
old_topic: old_branch_result,
results: results,
})
}
pub fn clear(&mut self) -> Vec<StagedTopic> {
let new_id = Self::make_base_staged_topic(self.base().clone()).topic;
let old_id = Some(new_id.clone());
info!(target: "git-topic-stage", "clearing the stage");
self.rewind_stage(CandidateTopic {
old_id: old_id,
new_id: new_id,
})
.1
}
fn merge_to_stage(&self, topic: Topic) -> Result<(Option<StagedTopic>, IntegrationResult)> {
let base_commit = self.base();
let head_commit = self.head();
let topic_commit = topic.commit.clone();
debug!(target: "git-topic-stage", "merging {} into the stage", topic);
let merge_status = self.ctx.mergeable(base_commit, &topic_commit)?;
let bases = if let MergeStatus::Mergeable(bases) = merge_status {
bases
} else {
debug!(target: "git-topic-stage", "rejecting: unmergeable: {}", merge_status);
return Ok((None, IntegrationResult::Unmerged(topic.clone(), merge_status)));
};
let workarea = self.ctx.prepare(head_commit)?;
let merge_result = workarea.setup_merge(&bases, head_commit, &topic_commit)?;
let mut merge_command = match merge_result {
MergeResult::Conflict(conflicts) => {
debug!(target: "git-topic-stage", "rejecting: conflicts: {}", conflicts.len());
return Ok((None,
IntegrationResult::Unstaged(topic,
UnstageReason::MergeConflict(conflicts))));
},
MergeResult::Ready(command) => command,
MergeResult::_Phantom(..) => unreachable!(),
};
merge_command.committer(&self.identity)
.author(&topic.author)
.author_date(&topic.stamp);
let merge_commit = merge_command.commit(self.create_message(self.base(), &topic))?;
let staged_topic = StagedTopic {
merge: merge_commit,
topic: topic,
};
debug!(target: "git-topic-stage", "successfully staged as {}", staged_topic);
Ok((Some(staged_topic.clone()), IntegrationResult::Staged(staged_topic)))
}
fn create_message(&self, _: &CommitId, topic: &Topic) -> String {
format!("{}{}{}\n\n{}{}\n{}{}\n",
STAGE_TOPIC_PREFIX,
topic.name,
STAGE_TOPIC_SUFFIX,
TOPIC_ID_TRAILER,
topic.id,
TOPIC_URL_TRAILER,
topic.url)
}
}