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@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@474: 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@650: tikhomirov@650: public boolean withSecretRoots() { tikhomirov@650: return !secretPhaseRoots.isEmpty(); tikhomirov@650: } tikhomirov@445: tikhomirov@628: /** tikhomirov@628: * @param cset revision to query tikhomirov@628: * @return phase of the changeset, never null tikhomirov@628: * @throws HgInvalidControlFileException if failed to access revlog index/data entry. Runtime exception tikhomirov@628: * @throws HgRuntimeException 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@628: * @param csetRevIndex revision index to query tikhomirov@628: * @param csetRev revision nodeid, optional tikhomirov@628: * @return phase of the changeset, never null tikhomirov@628: * @throws HgInvalidControlFileException if failed to access revlog index/data entry. Runtime exception tikhomirov@628: * @throws HgRuntimeException 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@445: tikhomirov@445: 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: /** tikhomirov@648: * @return all revisions with secret phase tikhomirov@648: */ tikhomirov@648: public RevisionSet allSecret() { tikhomirov@648: return allOf(HgPhase.Secret); tikhomirov@648: } tikhomirov@648: 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@649: tikhomirov@649: public void updateRoots(Collection draftRoots, Collection secretRoots) throws HgInvalidControlFileException { tikhomirov@649: draftPhaseRoots = draftRoots.isEmpty() ? Collections.emptyList() : new ArrayList(draftRoots); tikhomirov@649: 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@649: new FileUtils(repo.getLog()).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@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@648: * 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@648: 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@648: 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@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@445: draftPhaseRoots = phase2roots.containsKey(Draft) ? phase2roots.get(Draft) : Collections.emptyList(); tikhomirov@445: 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@445: case Draft : return draftPhaseRoots; tikhomirov@445: case Secret : return secretPhaseRoots; tikhomirov@445: } tikhomirov@445: return Collections.emptyList(); tikhomirov@445: } tikhomirov@449: 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@449: 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: }