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