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.Info; tikhomirov@471: import static org.tmatesoft.hg.util.LogFacility.Severity.Warn; tikhomirov@445: tikhomirov@445: import java.io.BufferedReader; tikhomirov@445: import java.io.File; tikhomirov@445: import java.io.FileReader; tikhomirov@445: import java.io.IOException; 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@445: import org.tmatesoft.hg.core.Nodeid; tikhomirov@445: import org.tmatesoft.hg.repo.HgChangelog; tikhomirov@471: import org.tmatesoft.hg.repo.HgInvalidControlFileException; 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@445: 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@648: public RevisionSet allDraft() { tikhomirov@648: return allOf(HgPhase.Draft).subtract(allOf(HgPhase.Secret)); tikhomirov@648: } tikhomirov@648: 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@451: BufferedReader br = null; tikhomirov@445: try { tikhomirov@445: if (!phaseroots.exists()) { tikhomirov@445: return Boolean.FALSE; tikhomirov@445: } tikhomirov@445: HashMap> phase2roots = new HashMap>(); tikhomirov@451: br = new BufferedReader(new FileReader(phaseroots)); tikhomirov@445: String line; tikhomirov@445: while ((line = br.readLine()) != null) { tikhomirov@445: String[] lc = line.trim().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@445: } catch (IOException ex) { tikhomirov@445: throw new HgInvalidControlFileException(ex.toString(), ex, phaseroots); tikhomirov@451: } finally { tikhomirov@451: if (br != null) { tikhomirov@451: try { tikhomirov@451: br.close(); tikhomirov@451: } catch (IOException ex) { tikhomirov@501: repo.getSessionContext().getLog().dump(getClass(), Info, ex, null); tikhomirov@451: // ignore the exception otherwise tikhomirov@451: } tikhomirov@451: } 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: }