tikhomirov@445: /* tikhomirov@628: * Copyright (c) 2012-2013 TMate Software Ltd tikhomirov@445: * tikhomirov@445: * This program is free software; you can redistribute it and/or modify tikhomirov@445: * it under the terms of the GNU General Public License as published by tikhomirov@445: * the Free Software Foundation; version 2 of the License. tikhomirov@445: * tikhomirov@445: * This program is distributed in the hope that it will be useful, tikhomirov@445: * but WITHOUT ANY WARRANTY; without even the implied warranty of tikhomirov@445: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the tikhomirov@445: * GNU General Public License for more details. tikhomirov@445: * tikhomirov@445: * For information on how to redistribute this software under tikhomirov@445: * the terms of a license other than GNU General Public License tikhomirov@445: * contact TMate Software at support@hg4j.com tikhomirov@445: */ tikhomirov@445: package org.tmatesoft.hg.internal; tikhomirov@445: tikhomirov@445: import static org.tmatesoft.hg.repo.HgPhase.Draft; tikhomirov@445: import static org.tmatesoft.hg.repo.HgPhase.Secret; tikhomirov@647: import static org.tmatesoft.hg.repo.HgRepositoryFiles.Phaseroots; tikhomirov@471: import static org.tmatesoft.hg.util.LogFacility.Severity.Warn; tikhomirov@445: tikhomirov@445: import java.io.File; tikhomirov@649: import java.io.FileWriter; tikhomirov@445: import java.io.IOException; tikhomirov@649: import java.util.ArrayList; tikhomirov@649: import java.util.Collection; tikhomirov@445: import java.util.Collections; tikhomirov@445: import java.util.HashMap; tikhomirov@445: import java.util.LinkedList; tikhomirov@445: import java.util.List; tikhomirov@445: tikhomirov@445: import org.tmatesoft.hg.core.HgChangeset; tikhomirov@649: import org.tmatesoft.hg.core.HgIOException; tikhomirov@445: import org.tmatesoft.hg.core.Nodeid; tikhomirov@445: import org.tmatesoft.hg.repo.HgChangelog; tikhomirov@471: import org.tmatesoft.hg.repo.HgInvalidControlFileException; tikhomirov@652: import org.tmatesoft.hg.repo.HgInvalidStateException; tikhomirov@471: import org.tmatesoft.hg.repo.HgParentChildMap; tikhomirov@445: import org.tmatesoft.hg.repo.HgPhase; tikhomirov@663: import org.tmatesoft.hg.repo.HgRemoteRepository; tikhomirov@445: import org.tmatesoft.hg.repo.HgRepository; tikhomirov@628: import org.tmatesoft.hg.repo.HgRuntimeException; tikhomirov@445: tikhomirov@445: /** tikhomirov@490: * Support to deal with Mercurial phases feature (as of Mercurial version 2.1) tikhomirov@490: * tikhomirov@490: * @see http://mercurial.selenic.com/wiki/Phases tikhomirov@490: * @see http://mercurial.selenic.com/wiki/PhasesDevel tikhomirov@445: * tikhomirov@445: * @author Artem Tikhomirov tikhomirov@445: * @author TMate Software Ltd. tikhomirov@445: */ tikhomirov@445: public final class PhasesHelper { tikhomirov@445: tikhomirov@493: private final Internals repo; tikhomirov@471: private final HgParentChildMap parentHelper; tikhomirov@445: private Boolean repoSupporsPhases; tikhomirov@445: private List draftPhaseRoots; tikhomirov@445: private List secretPhaseRoots; tikhomirov@449: private RevisionDescendants[][] phaseDescendants = new RevisionDescendants[HgPhase.values().length][]; tikhomirov@445: tikhomirov@493: public PhasesHelper(Internals internalRepo) { tikhomirov@493: this(internalRepo, null); tikhomirov@445: } tikhomirov@445: tikhomirov@493: public PhasesHelper(Internals internalRepo, HgParentChildMap pw) { tikhomirov@493: repo = internalRepo; tikhomirov@445: parentHelper = pw; tikhomirov@445: } tikhomirov@663: tikhomirov@474: public HgRepository getRepo() { tikhomirov@493: return repo.getRepo(); tikhomirov@474: } tikhomirov@445: tikhomirov@628: public boolean isCapableOfPhases() throws HgRuntimeException { tikhomirov@445: if (null == repoSupporsPhases) { tikhomirov@445: repoSupporsPhases = readRoots(); tikhomirov@445: } tikhomirov@445: return repoSupporsPhases.booleanValue(); tikhomirov@445: } tikhomirov@663: tikhomirov@650: public boolean withSecretRoots() { tikhomirov@650: return !secretPhaseRoots.isEmpty(); tikhomirov@650: } tikhomirov@445: tikhomirov@628: /** tikhomirov@663: * @param cset tikhomirov@663: * revision to query tikhomirov@628: * @return phase of the changeset, never null tikhomirov@663: * @throws HgInvalidControlFileException tikhomirov@663: * if failed to access revlog index/data entry. Runtime exception tikhomirov@663: * @throws HgRuntimeException tikhomirov@663: * subclass thereof to indicate other issues with the library. Runtime exception tikhomirov@628: */ tikhomirov@628: public HgPhase getPhase(HgChangeset cset) throws HgRuntimeException { tikhomirov@445: final Nodeid csetRev = cset.getNodeid(); tikhomirov@471: final int csetRevIndex = cset.getRevisionIndex(); tikhomirov@445: return getPhase(csetRevIndex, csetRev); tikhomirov@445: } tikhomirov@445: tikhomirov@628: /** tikhomirov@663: * @param csetRevIndex tikhomirov@663: * revision index to query tikhomirov@663: * @param csetRev tikhomirov@663: * revision nodeid, optional tikhomirov@628: * @return phase of the changeset, never null tikhomirov@663: * @throws HgInvalidControlFileException tikhomirov@663: * if failed to access revlog index/data entry. Runtime exception tikhomirov@663: * @throws HgRuntimeException tikhomirov@663: * subclass thereof to indicate other issues with the library. Runtime exception tikhomirov@628: */ tikhomirov@628: public HgPhase getPhase(final int csetRevIndex, Nodeid csetRev) throws HgRuntimeException { tikhomirov@445: if (!isCapableOfPhases()) { tikhomirov@445: return HgPhase.Undefined; tikhomirov@445: } tikhomirov@449: // csetRev is only used when parentHelper is available tikhomirov@449: if (parentHelper != null && (csetRev == null || csetRev.isNull())) { tikhomirov@493: csetRev = getRepo().getChangelog().getRevision(csetRevIndex); tikhomirov@445: } tikhomirov@663: tikhomirov@663: for (HgPhase phase : new HgPhase[] { HgPhase.Secret, HgPhase.Draft }) { tikhomirov@445: List roots = getPhaseRoots(phase); tikhomirov@445: if (roots.isEmpty()) { tikhomirov@445: continue; tikhomirov@445: } tikhomirov@445: if (parentHelper != null) { tikhomirov@449: if (roots.contains(csetRev)) { tikhomirov@449: return phase; tikhomirov@449: } tikhomirov@445: if (parentHelper.childrenOf(roots).contains(csetRev)) { tikhomirov@445: return phase; tikhomirov@445: } tikhomirov@445: } else { tikhomirov@445: // no parent helper tikhomirov@449: // search all descendants.RevisuionDescendats includes root as well. tikhomirov@449: for (RevisionDescendants rd : getPhaseDescendants(phase)) { tikhomirov@449: // isCandidate is to go straight to another root if changeset was added later that the current root tikhomirov@449: if (rd.isCandidate(csetRevIndex) && rd.isDescendant(csetRevIndex)) { tikhomirov@449: return phase; tikhomirov@445: } tikhomirov@445: } tikhomirov@445: } tikhomirov@445: } tikhomirov@445: return HgPhase.Public; tikhomirov@648: } tikhomirov@445: tikhomirov@648: /** tikhomirov@648: * @return all revisions with secret phase tikhomirov@648: */ tikhomirov@648: public RevisionSet allSecret() { tikhomirov@648: return allOf(HgPhase.Secret); tikhomirov@648: } tikhomirov@663: tikhomirov@649: /** tikhomirov@649: * @return all revisions with draft phase tikhomirov@649: */ tikhomirov@648: public RevisionSet allDraft() { tikhomirov@648: return allOf(HgPhase.Draft).subtract(allOf(HgPhase.Secret)); tikhomirov@648: } tikhomirov@663: tikhomirov@663: // XXX throw HgIOException instead? tikhomirov@649: public void updateRoots(Collection draftRoots, Collection secretRoots) throws HgInvalidControlFileException { tikhomirov@663: draftPhaseRoots = draftRoots.isEmpty() ? Collections. emptyList() : new ArrayList(draftRoots); tikhomirov@663: secretPhaseRoots = secretRoots.isEmpty() ? Collections. emptyList() : new ArrayList(secretRoots); tikhomirov@649: String fmt = "%d %s\n"; tikhomirov@649: File phaseroots = repo.getRepositoryFile(Phaseroots); tikhomirov@649: FileWriter fw = null; tikhomirov@649: try { tikhomirov@649: fw = new FileWriter(phaseroots); tikhomirov@649: for (Nodeid n : secretPhaseRoots) { tikhomirov@649: fw.write(String.format(fmt, HgPhase.Secret.mercurialOrdinal(), n.toString())); tikhomirov@649: } tikhomirov@649: for (Nodeid n : draftPhaseRoots) { tikhomirov@649: fw.write(String.format(fmt, HgPhase.Draft.mercurialOrdinal(), n.toString())); tikhomirov@649: } tikhomirov@649: fw.flush(); tikhomirov@649: } catch (IOException ex) { tikhomirov@649: throw new HgInvalidControlFileException(ex.getMessage(), ex, phaseroots); tikhomirov@649: } finally { tikhomirov@654: new FileUtils(repo.getLog(), this).closeQuietly(fw); tikhomirov@649: } tikhomirov@649: } tikhomirov@648: tikhomirov@652: public void newCommitNode(Nodeid newChangeset, HgPhase newCommitPhase) throws HgRuntimeException { tikhomirov@652: final int riCset = repo.getRepo().getChangelog().getRevisionIndex(newChangeset); tikhomirov@652: HgPhase ph = getPhase(riCset, newChangeset); tikhomirov@652: if (ph.compareTo(newCommitPhase) >= 0) { tikhomirov@652: // present phase is more secret than the desired one tikhomirov@652: return; tikhomirov@652: } tikhomirov@652: // newCommitPhase can't be public here, condition above would be satisfied tikhomirov@652: assert newCommitPhase != HgPhase.Public; tikhomirov@652: // ph is e.g public when newCommitPhase is draft tikhomirov@652: // or is draft when desired phase is secret tikhomirov@652: final RevisionSet rs = allOf(newCommitPhase).union(new RevisionSet(Collections.singleton(newChangeset))); tikhomirov@652: final RevisionSet newRoots; tikhomirov@652: if (parentHelper != null) { tikhomirov@652: newRoots = rs.roots(parentHelper); tikhomirov@652: } else { tikhomirov@652: newRoots = rs.roots(repo.getRepo()); tikhomirov@652: } tikhomirov@652: if (newCommitPhase == HgPhase.Draft) { tikhomirov@652: updateRoots(newRoots.asList(), secretPhaseRoots); tikhomirov@652: } else if (newCommitPhase == HgPhase.Secret) { tikhomirov@652: updateRoots(draftPhaseRoots, newRoots.asList()); tikhomirov@652: } else { tikhomirov@652: throw new HgInvalidStateException(String.format("Unexpected phase %s for new commits", newCommitPhase)); tikhomirov@652: } tikhomirov@652: } tikhomirov@652: tikhomirov@648: /** tikhomirov@663: * @return set of revisions that are public locally, but draft on remote. tikhomirov@663: */ tikhomirov@663: public RevisionSet synchronizeWithRemote(HgRemoteRepository.Phases remotePhases, RevisionSet sharedWithRemote) throws HgInvalidControlFileException { tikhomirov@663: assert parentHelper != null; tikhomirov@663: RevisionSet presentSecret = allSecret(); tikhomirov@663: RevisionSet presentDraft = allDraft(); tikhomirov@663: RevisionSet secretLeft, draftLeft; tikhomirov@663: RevisionSet remoteDrafts = knownRemoteDrafts(remotePhases, sharedWithRemote, presentSecret); tikhomirov@663: if (remotePhases.isPublishingServer()) { tikhomirov@663: // although it's unlikely shared revisions would affect secret changesets, tikhomirov@663: // it doesn't hurt to check secret roots along with draft ones tikhomirov@663: // tikhomirov@663: // local drafts that are known to be public now tikhomirov@663: RevisionSet draftsBecomePublic = presentDraft.intersect(sharedWithRemote); tikhomirov@663: RevisionSet secretsBecomePublic = presentSecret.intersect(sharedWithRemote); tikhomirov@663: // any ancestor of the public revision is public, too tikhomirov@663: RevisionSet draftsGone = presentDraft.ancestors(draftsBecomePublic, parentHelper); tikhomirov@663: RevisionSet secretsGone = presentSecret.ancestors(secretsBecomePublic, parentHelper); tikhomirov@663: // remove public and their ancestors from drafts tikhomirov@663: draftLeft = presentDraft.subtract(draftsGone).subtract(draftsBecomePublic); tikhomirov@663: secretLeft = presentSecret.subtract(secretsGone).subtract(secretsBecomePublic); tikhomirov@663: } else { tikhomirov@663: // shall merge local and remote phase states tikhomirov@663: // revisions that cease to be secret (gonna become Public), e.g. someone else pushed them tikhomirov@663: RevisionSet secretGone = presentSecret.intersect(remoteDrafts); tikhomirov@663: // parents of those remote drafts are public, mark them as public locally, too tikhomirov@663: RevisionSet remotePublic = presentSecret.ancestors(secretGone, parentHelper); tikhomirov@663: secretLeft = presentSecret.subtract(secretGone).subtract(remotePublic); tikhomirov@663: /* tikhomirov@663: * Revisions grow from left to right (parents to the left, children to the right) tikhomirov@663: * tikhomirov@663: * I: Set of local is subset of remote tikhomirov@663: * tikhomirov@663: * local draft tikhomirov@663: * --o---r---o---l---o-- tikhomirov@663: * remote draft tikhomirov@663: * tikhomirov@663: * Remote draft roots shall be updated tikhomirov@663: * tikhomirov@663: * tikhomirov@663: * II: Set of local is superset of remote tikhomirov@663: * tikhomirov@663: * local draft tikhomirov@663: * --o---l---o---r---o-- tikhomirov@663: * remote draft tikhomirov@663: * tikhomirov@663: * Local draft roots shall be updated tikhomirov@663: */ tikhomirov@663: RevisionSet sharedDraft = presentDraft.intersect(remoteDrafts); // (I: ~presentDraft; II: ~remoteDraft tikhomirov@663: // XXX do I really need sharedDrafts here? why not ancestors(remoteDrafts)? tikhomirov@663: RevisionSet localDraftRemotePublic = presentDraft.ancestors(sharedDraft, parentHelper); // I: 0; II: those treated public on remote tikhomirov@663: // remoteDrafts are local revisions known as draft@remote tikhomirov@663: // remoteDraftsLocalPublic - revisions that would cease to be listed as draft on remote tikhomirov@663: RevisionSet remoteDraftsLocalPublic = remoteDrafts.ancestors(sharedDraft, parentHelper); tikhomirov@663: RevisionSet remoteDraftsLeft = remoteDrafts.subtract(remoteDraftsLocalPublic); tikhomirov@663: // forget those deemed public by remote (drafts shared by both remote and local are ok to stay) tikhomirov@663: RevisionSet combinedDraft = presentDraft.union(remoteDraftsLeft); tikhomirov@663: draftLeft = combinedDraft.subtract(localDraftRemotePublic); tikhomirov@663: } tikhomirov@663: final RevisionSet newDraftRoots = draftLeft.roots(parentHelper); tikhomirov@663: final RevisionSet newSecretRoots = secretLeft.roots(parentHelper); tikhomirov@663: updateRoots(newDraftRoots.asList(), newSecretRoots.asList()); tikhomirov@663: // tikhomirov@663: // if there's a remote draft root that points to revision we know is public tikhomirov@663: RevisionSet remoteDraftsLocalPublic = remoteDrafts.subtract(draftLeft).subtract(secretLeft); tikhomirov@663: return remoteDraftsLocalPublic; tikhomirov@663: } tikhomirov@663: tikhomirov@663: // shared - set of revisions we've shared with remote tikhomirov@663: private RevisionSet knownRemoteDrafts(HgRemoteRepository.Phases remotePhases, RevisionSet shared, RevisionSet localSecret) { tikhomirov@663: ArrayList knownRemoteDraftRoots = new ArrayList(); tikhomirov@663: for (Nodeid rdr : remotePhases.draftRoots()) { tikhomirov@663: if (parentHelper.knownNode(rdr)) { tikhomirov@663: knownRemoteDraftRoots.add(rdr); tikhomirov@663: } tikhomirov@663: } tikhomirov@663: // knownRemoteDraftRoots + childrenOf(knownRemoteDraftRoots) is everything remote may treat as Draft tikhomirov@663: RevisionSet remoteDrafts = new RevisionSet(knownRemoteDraftRoots); tikhomirov@663: RevisionSet localChildren = remoteDrafts.children(parentHelper); tikhomirov@663: // we didn't send any local secret revision tikhomirov@663: localChildren = localChildren.subtract(localSecret); tikhomirov@663: // draft roots are among remote drafts tikhomirov@663: remoteDrafts = remoteDrafts.union(localChildren); tikhomirov@663: // remoteDrafts is set of local revisions remote may see as Draft. However, tikhomirov@663: // need to remove from this set revisions we didn't share with remote: tikhomirov@663: // 1) shared.children gives all local revisions accessible from shared. tikhomirov@663: // 2) shared.roots.children is equivalent with smaller intermediate set, the way we build tikhomirov@663: // childrenOf doesn't really benefits from that. tikhomirov@663: RevisionSet localChildrenNotSent = shared.children(parentHelper).subtract(shared); tikhomirov@663: // remote shall know only what we've sent, subtract revisions we didn't actually sent tikhomirov@663: remoteDrafts = remoteDrafts.subtract(localChildrenNotSent); tikhomirov@663: return remoteDrafts; tikhomirov@663: } tikhomirov@663: tikhomirov@663: /** tikhomirov@648: * For a given phase, collect all revisions with phase that is the same or more private (i.e. for Draft, returns Draft+Secret) tikhomirov@663: * The reason is not a nice API intention (which is awful, indeed), but an ease of implementation tikhomirov@648: */ tikhomirov@648: private RevisionSet allOf(HgPhase phase) { tikhomirov@648: assert phase != HgPhase.Public; tikhomirov@648: if (!isCapableOfPhases()) { tikhomirov@663: return new RevisionSet(Collections. emptyList()); tikhomirov@648: } tikhomirov@648: final List roots = getPhaseRoots(phase); tikhomirov@648: if (parentHelper != null) { tikhomirov@648: return new RevisionSet(roots).union(new RevisionSet(parentHelper.childrenOf(roots))); tikhomirov@648: } else { tikhomirov@663: RevisionSet rv = new RevisionSet(Collections. emptyList()); tikhomirov@648: for (RevisionDescendants rd : getPhaseDescendants(phase)) { tikhomirov@648: rv = rv.union(rd.asRevisionSet()); tikhomirov@648: } tikhomirov@648: return rv; tikhomirov@648: } tikhomirov@445: } tikhomirov@445: tikhomirov@628: private Boolean readRoots() throws HgRuntimeException { tikhomirov@647: File phaseroots = repo.getRepositoryFile(Phaseroots); tikhomirov@445: try { tikhomirov@445: if (!phaseroots.exists()) { tikhomirov@663: if (repo.shallCreatePhaseroots()) { tikhomirov@663: draftPhaseRoots = Collections.emptyList(); tikhomirov@663: secretPhaseRoots = Collections.emptyList(); tikhomirov@663: return Boolean.TRUE; tikhomirov@663: } tikhomirov@445: return Boolean.FALSE; tikhomirov@445: } tikhomirov@649: LineReader lr = new LineReader(phaseroots, repo.getLog()); tikhomirov@649: final Collection lines = lr.read(new LineReader.SimpleLineCollector(), new LinkedList()); tikhomirov@445: HashMap> phase2roots = new HashMap>(); tikhomirov@649: for (String line : lines) { tikhomirov@649: String[] lc = line.split("\\s+"); tikhomirov@445: if (lc.length == 0) { tikhomirov@445: continue; tikhomirov@445: } tikhomirov@445: if (lc.length != 2) { tikhomirov@501: repo.getSessionContext().getLog().dump(getClass(), Warn, "Bad line in phaseroots:%s", line); tikhomirov@445: continue; tikhomirov@445: } tikhomirov@445: int phaseIndex = Integer.parseInt(lc[0]); tikhomirov@445: Nodeid rootRev = Nodeid.fromAscii(lc[1]); tikhomirov@493: if (!getRepo().getChangelog().isKnown(rootRev)) { tikhomirov@501: repo.getSessionContext().getLog().dump(getClass(), Warn, "Phase(%d) root node %s doesn't exist in the repository, ignored.", phaseIndex, rootRev); tikhomirov@451: continue; tikhomirov@451: } tikhomirov@445: HgPhase phase = HgPhase.parse(phaseIndex); tikhomirov@445: List roots = phase2roots.get(phase); tikhomirov@445: if (roots == null) { tikhomirov@445: phase2roots.put(phase, roots = new LinkedList()); tikhomirov@445: } tikhomirov@445: roots.add(rootRev); tikhomirov@445: } tikhomirov@663: draftPhaseRoots = phase2roots.containsKey(Draft) ? phase2roots.get(Draft) : Collections. emptyList(); tikhomirov@663: secretPhaseRoots = phase2roots.containsKey(Secret) ? phase2roots.get(Secret) : Collections. emptyList(); tikhomirov@649: } catch (HgIOException ex) { tikhomirov@649: throw new HgInvalidControlFileException(ex, true); tikhomirov@445: } tikhomirov@445: return Boolean.TRUE; tikhomirov@445: } tikhomirov@445: tikhomirov@445: private List getPhaseRoots(HgPhase phase) { tikhomirov@445: switch (phase) { tikhomirov@663: case Draft: tikhomirov@663: return draftPhaseRoots; tikhomirov@663: case Secret: tikhomirov@663: return secretPhaseRoots; tikhomirov@445: } tikhomirov@445: return Collections.emptyList(); tikhomirov@445: } tikhomirov@449: tikhomirov@628: private RevisionDescendants[] getPhaseDescendants(HgPhase phase) throws HgRuntimeException { tikhomirov@447: int ordinal = phase.ordinal(); tikhomirov@449: if (phaseDescendants[ordinal] == null) { tikhomirov@449: phaseDescendants[ordinal] = buildPhaseDescendants(phase); tikhomirov@447: } tikhomirov@449: return phaseDescendants[ordinal]; tikhomirov@449: } tikhomirov@449: tikhomirov@628: private RevisionDescendants[] buildPhaseDescendants(HgPhase phase) throws HgRuntimeException { tikhomirov@449: int[] roots = toIndexes(getPhaseRoots(phase)); tikhomirov@449: RevisionDescendants[] rv = new RevisionDescendants[roots.length]; tikhomirov@449: for (int i = 0; i < roots.length; i++) { tikhomirov@493: rv[i] = new RevisionDescendants(getRepo(), roots[i]); tikhomirov@449: rv[i].build(); tikhomirov@449: } tikhomirov@449: return rv; tikhomirov@449: } tikhomirov@663: tikhomirov@628: private int[] toIndexes(List roots) throws HgRuntimeException { tikhomirov@449: int[] rv = new int[roots.size()]; tikhomirov@449: for (int i = 0; i < rv.length; i++) { tikhomirov@493: rv[i] = getRepo().getChangelog().getRevisionIndex(roots.get(i)); tikhomirov@449: } tikhomirov@449: return rv; tikhomirov@447: } tikhomirov@445: }