tikhomirov@645: /* tikhomirov@645: * Copyright (c) 2013 TMate Software Ltd tikhomirov@645: * tikhomirov@645: * This program is free software; you can redistribute it and/or modify tikhomirov@645: * it under the terms of the GNU General Public License as published by tikhomirov@645: * the Free Software Foundation; version 2 of the License. tikhomirov@645: * tikhomirov@645: * This program is distributed in the hope that it will be useful, tikhomirov@645: * but WITHOUT ANY WARRANTY; without even the implied warranty of tikhomirov@645: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the tikhomirov@645: * GNU General Public License for more details. tikhomirov@645: * tikhomirov@645: * For information on how to redistribute this software under tikhomirov@645: * the terms of a license other than GNU General Public License tikhomirov@645: * contact TMate Software at support@hg4j.com tikhomirov@645: */ tikhomirov@645: package org.tmatesoft.hg.core; tikhomirov@645: tikhomirov@645: import java.io.File; tikhomirov@645: import java.io.IOException; tikhomirov@649: import java.util.ArrayList; tikhomirov@654: import java.util.Collection; tikhomirov@654: import java.util.Collections; tikhomirov@645: import java.util.List; tikhomirov@645: tikhomirov@645: import org.tmatesoft.hg.internal.BundleGenerator; tikhomirov@649: import org.tmatesoft.hg.internal.Internals; tikhomirov@649: import org.tmatesoft.hg.internal.PhasesHelper; tikhomirov@645: import org.tmatesoft.hg.internal.RepositoryComparator; tikhomirov@649: import org.tmatesoft.hg.internal.RevisionSet; tikhomirov@646: import org.tmatesoft.hg.repo.HgBookmarks; tikhomirov@645: import org.tmatesoft.hg.repo.HgBundle; tikhomirov@645: import org.tmatesoft.hg.repo.HgChangelog; tikhomirov@645: import org.tmatesoft.hg.repo.HgInternals; tikhomirov@645: import org.tmatesoft.hg.repo.HgInvalidStateException; tikhomirov@645: import org.tmatesoft.hg.repo.HgLookup; tikhomirov@645: import org.tmatesoft.hg.repo.HgParentChildMap; tikhomirov@649: import org.tmatesoft.hg.repo.HgPhase; tikhomirov@645: import org.tmatesoft.hg.repo.HgRemoteRepository; tikhomirov@645: import org.tmatesoft.hg.repo.HgRepository; tikhomirov@645: import org.tmatesoft.hg.repo.HgRuntimeException; tikhomirov@645: import org.tmatesoft.hg.util.CancelledException; tikhomirov@653: import org.tmatesoft.hg.util.LogFacility.Severity; tikhomirov@650: import org.tmatesoft.hg.util.Outcome; tikhomirov@646: import org.tmatesoft.hg.util.Pair; tikhomirov@645: import org.tmatesoft.hg.util.ProgressSupport; tikhomirov@645: tikhomirov@645: /** tikhomirov@645: * tikhomirov@645: * @author Artem Tikhomirov tikhomirov@645: * @author TMate Software Ltd. tikhomirov@645: */ tikhomirov@645: public class HgPushCommand extends HgAbstractCommand { tikhomirov@645: tikhomirov@645: private final HgRepository repo; tikhomirov@645: private HgRemoteRepository remoteRepo; tikhomirov@654: private RevisionSet outgoing; tikhomirov@645: tikhomirov@645: public HgPushCommand(HgRepository hgRepo) { tikhomirov@645: repo = hgRepo; tikhomirov@645: } tikhomirov@645: tikhomirov@645: public HgPushCommand destination(HgRemoteRepository hgRemote) { tikhomirov@645: remoteRepo = hgRemote; tikhomirov@645: return this; tikhomirov@645: } tikhomirov@645: tikhomirov@645: public void execute() throws HgRemoteConnectionException, HgIOException, CancelledException, HgLibraryFailureException { tikhomirov@645: final ProgressSupport progress = getProgressSupport(null); tikhomirov@645: try { tikhomirov@645: progress.start(100); tikhomirov@645: // tikhomirov@645: // find out missing tikhomirov@645: // TODO refactor same code in HgOutgoingCommand #getComparator and #getParentHelper tikhomirov@649: final HgChangelog clog = repo.getChangelog(); tikhomirov@649: final HgParentChildMap parentHelper = new HgParentChildMap(clog); tikhomirov@645: parentHelper.init(); tikhomirov@650: final Internals implRepo = HgInternals.getImplementationRepo(repo); tikhomirov@650: final PhasesHelper phaseHelper = new PhasesHelper(implRepo, parentHelper); tikhomirov@645: final RepositoryComparator comparator = new RepositoryComparator(parentHelper, remoteRepo); tikhomirov@645: comparator.compare(new ProgressSupport.Sub(progress, 50), getCancelSupport(null, true)); tikhomirov@645: List l = comparator.getLocalOnlyRevisions(); tikhomirov@650: if (phaseHelper.isCapableOfPhases() && phaseHelper.withSecretRoots()) { tikhomirov@650: RevisionSet secret = phaseHelper.allSecret(); tikhomirov@650: outgoing = new RevisionSet(l).subtract(secret); tikhomirov@650: } else { tikhomirov@650: outgoing = new RevisionSet(l); tikhomirov@650: } tikhomirov@645: // tikhomirov@645: // prepare bundle tikhomirov@649: BundleGenerator bg = new BundleGenerator(implRepo); tikhomirov@650: File bundleFile = bg.create(outgoing.asList()); tikhomirov@645: progress.worked(20); tikhomirov@645: HgBundle b = new HgLookup(repo.getSessionContext()).loadBundle(bundleFile); tikhomirov@645: // tikhomirov@645: // send changes tikhomirov@645: remoteRepo.unbundle(b, comparator.getRemoteHeads()); tikhomirov@645: progress.worked(20); tikhomirov@645: // tikhomirov@649: // update phase information tikhomirov@649: if (phaseHelper.isCapableOfPhases()) { tikhomirov@649: RevisionSet presentSecret = phaseHelper.allSecret(); tikhomirov@649: RevisionSet presentDraft = phaseHelper.allDraft(); tikhomirov@649: RevisionSet secretLeft, draftLeft; tikhomirov@649: HgRemoteRepository.Phases remotePhases = remoteRepo.getPhases(); tikhomirov@652: RevisionSet remoteDrafts = knownRemoteDrafts(remotePhases, parentHelper, outgoing, presentSecret); tikhomirov@649: if (remotePhases.isPublishingServer()) { tikhomirov@649: // although it's unlikely outgoing would affect secret changesets, tikhomirov@649: // it doesn't hurt to check secret roots along with draft ones tikhomirov@649: secretLeft = presentSecret.subtract(outgoing); tikhomirov@649: draftLeft = presentDraft.subtract(outgoing); tikhomirov@649: } else { tikhomirov@649: // shall merge local and remote phase states tikhomirov@649: // revisions that cease to be secret (gonna become Public), e.g. someone else pushed them tikhomirov@649: RevisionSet secretGone = presentSecret.intersect(remoteDrafts); tikhomirov@650: // parents of those remote drafts are public, mark them as public locally, too tikhomirov@650: RevisionSet remotePublic = presentSecret.ancestors(secretGone, parentHelper); tikhomirov@650: secretLeft = presentSecret.subtract(secretGone).subtract(remotePublic); tikhomirov@650: /* tikhomirov@650: * Revisions grow from left to right (parents to the left, children to the right) tikhomirov@650: * tikhomirov@650: * I: Set of local is subset of remote tikhomirov@650: * tikhomirov@650: * local draft tikhomirov@650: * --o---r---o---l---o-- tikhomirov@650: * remote draft tikhomirov@650: * tikhomirov@650: * Remote draft roots shall be updated tikhomirov@650: * tikhomirov@650: * tikhomirov@650: * II: Set of local is superset of remote tikhomirov@650: * tikhomirov@650: * local draft tikhomirov@650: * --o---l---o---r---o-- tikhomirov@650: * remote draft tikhomirov@650: * tikhomirov@650: * Local draft roots shall be updated tikhomirov@650: */ tikhomirov@650: RevisionSet sharedDraft = presentDraft.intersect(remoteDrafts); // (I: ~presentDraft; II: ~remoteDraft tikhomirov@652: // XXX do I really need sharedDrafts here? why not ancestors(remoteDrafts)? tikhomirov@650: RevisionSet localDraftRemotePublic = presentDraft.ancestors(sharedDraft, parentHelper); // I: 0; II: those treated public on remote tikhomirov@652: // remoteDrafts are local revisions known as draft@remote tikhomirov@652: // remoteDraftsLocalPublic - revisions that would cease to be listed as draft on remote tikhomirov@652: RevisionSet remoteDraftsLocalPublic = remoteDrafts.ancestors(sharedDraft, parentHelper); tikhomirov@652: RevisionSet remoteDraftsLeft = remoteDrafts.subtract(remoteDraftsLocalPublic); tikhomirov@650: // forget those deemed public by remote (drafts shared by both remote and local are ok to stay) tikhomirov@652: RevisionSet combinedDraft = presentDraft.union(remoteDraftsLeft); tikhomirov@652: draftLeft = combinedDraft.subtract(localDraftRemotePublic); tikhomirov@649: } tikhomirov@649: final RevisionSet newDraftRoots = draftLeft.roots(parentHelper); tikhomirov@649: final RevisionSet newSecretRoots = secretLeft.roots(parentHelper); tikhomirov@649: phaseHelper.updateRoots(newDraftRoots.asList(), newSecretRoots.asList()); tikhomirov@650: // tikhomirov@650: // if there's a remote draft root that points to revision we know is public tikhomirov@650: RevisionSet remoteDraftsLocalPublic = remoteDrafts.subtract(draftLeft).subtract(secretLeft); tikhomirov@650: if (!remoteDraftsLocalPublic.isEmpty()) { tikhomirov@650: // foreach remoteDraftsLocallyPublic.heads() do push Draft->Public tikhomirov@650: for (Nodeid n : remoteDraftsLocalPublic.heads(parentHelper)) { tikhomirov@650: try { tikhomirov@650: Outcome upo = remoteRepo.updatePhase(HgPhase.Draft, HgPhase.Public, n); tikhomirov@650: if (!upo.isOk()) { tikhomirov@650: implRepo.getLog().dump(getClass(), Severity.Info, "Failed to update remote phase, reason: %s", upo.getMessage()); tikhomirov@650: } tikhomirov@650: } catch (HgRemoteConnectionException ex) { tikhomirov@650: implRepo.getLog().dump(getClass(), Severity.Error, ex, String.format("Failed to update phase of %s", n.shortNotation())); tikhomirov@650: } tikhomirov@650: } tikhomirov@650: } tikhomirov@649: } tikhomirov@645: progress.worked(5); tikhomirov@645: // tikhomirov@646: // update bookmark information tikhomirov@646: HgBookmarks localBookmarks = repo.getBookmarks(); tikhomirov@646: if (!localBookmarks.getAllBookmarks().isEmpty()) { tikhomirov@649: for (Pair bm : remoteRepo.getBookmarks()) { tikhomirov@646: Nodeid localRevision = localBookmarks.getRevision(bm.first()); tikhomirov@646: if (localRevision == null || !parentHelper.knownNode(bm.second())) { tikhomirov@646: continue; tikhomirov@646: } tikhomirov@646: // we know both localRevision and revision of remote bookmark, tikhomirov@646: // need to make sure we don't push older revision than it's at the server tikhomirov@646: if (parentHelper.isChild(bm.second(), localRevision)) { tikhomirov@646: remoteRepo.updateBookmark(bm.first(), bm.second(), localRevision); tikhomirov@646: } tikhomirov@646: } tikhomirov@646: } tikhomirov@646: // XXX WTF is obsolete in namespaces key?? tikhomirov@645: progress.worked(5); tikhomirov@645: } catch (IOException ex) { tikhomirov@645: throw new HgIOException(ex.getMessage(), null); // XXX not a nice idea to throw IOException from BundleGenerator#create tikhomirov@645: } catch (HgRepositoryNotFoundException ex) { tikhomirov@645: final HgInvalidStateException e = new HgInvalidStateException("Failed to load a just-created bundle"); tikhomirov@645: e.initCause(ex); tikhomirov@645: throw new HgLibraryFailureException(e); tikhomirov@645: } catch (HgRuntimeException ex) { tikhomirov@645: throw new HgLibraryFailureException(ex); tikhomirov@645: } finally { tikhomirov@645: progress.done(); tikhomirov@645: } tikhomirov@645: } tikhomirov@645: tikhomirov@654: public Collection getPushedRevisions() { tikhomirov@654: return outgoing == null ? Collections.emptyList() : outgoing.asList(); tikhomirov@654: } tikhomirov@654: tikhomirov@652: private RevisionSet knownRemoteDrafts(HgRemoteRepository.Phases remotePhases, HgParentChildMap parentHelper, RevisionSet outgoing, RevisionSet localSecret) { tikhomirov@650: ArrayList knownRemoteDraftRoots = new ArrayList(); tikhomirov@650: for (Nodeid rdr : remotePhases.draftRoots()) { tikhomirov@650: if (parentHelper.knownNode(rdr)) { tikhomirov@650: knownRemoteDraftRoots.add(rdr); tikhomirov@650: } tikhomirov@650: } tikhomirov@650: // knownRemoteDraftRoots + childrenOf(knownRemoteDraftRoots) is everything remote may treat as Draft tikhomirov@650: RevisionSet remoteDrafts = new RevisionSet(knownRemoteDraftRoots); tikhomirov@652: RevisionSet localChildren = remoteDrafts.children(parentHelper); tikhomirov@652: // we didn't send any local secret revision tikhomirov@652: localChildren = localChildren.subtract(localSecret); tikhomirov@652: // draft roots are among remote drafts tikhomirov@652: remoteDrafts = remoteDrafts.union(localChildren); tikhomirov@650: // 1) outgoing.children gives all local revisions accessible from outgoing. tikhomirov@650: // 2) outgoing.roots.children is equivalent with smaller intermediate set, the way we build tikhomirov@650: // childrenOf doesn't really benefits from that. tikhomirov@650: RevisionSet localChildrenNotSent = outgoing.children(parentHelper).subtract(outgoing); tikhomirov@650: // remote shall know only what we've sent, subtract revisions we didn't actually sent tikhomirov@650: remoteDrafts = remoteDrafts.subtract(localChildrenNotSent); tikhomirov@650: return remoteDrafts; tikhomirov@650: } tikhomirov@645: }