tikhomirov@463: /* tikhomirov@463: * Copyright (c) 2012 TMate Software Ltd tikhomirov@463: * tikhomirov@463: * This program is free software; you can redistribute it and/or modify tikhomirov@463: * it under the terms of the GNU General Public License as published by tikhomirov@463: * the Free Software Foundation; version 2 of the License. tikhomirov@463: * tikhomirov@463: * This program is distributed in the hope that it will be useful, tikhomirov@463: * but WITHOUT ANY WARRANTY; without even the implied warranty of tikhomirov@463: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the tikhomirov@463: * GNU General Public License for more details. tikhomirov@463: * tikhomirov@463: * For information on how to redistribute this software under tikhomirov@463: * the terms of a license other than GNU General Public License tikhomirov@463: * contact TMate Software at support@hg4j.com tikhomirov@463: */ tikhomirov@463: package org.tmatesoft.hg.repo.ext; tikhomirov@463: tikhomirov@471: import static org.tmatesoft.hg.util.LogFacility.Severity.Warn; tikhomirov@471: tikhomirov@463: import java.io.File; tikhomirov@463: import java.io.IOException; tikhomirov@464: import java.util.ArrayList; tikhomirov@463: import java.util.Collections; tikhomirov@464: import java.util.HashMap; tikhomirov@463: import java.util.LinkedList; tikhomirov@463: import java.util.List; tikhomirov@464: import java.util.Map; tikhomirov@463: tikhomirov@463: import org.tmatesoft.hg.core.Nodeid; tikhomirov@493: import org.tmatesoft.hg.internal.Internals; tikhomirov@480: import org.tmatesoft.hg.internal.LineReader; tikhomirov@471: import org.tmatesoft.hg.repo.HgInvalidControlFileException; tikhomirov@471: import org.tmatesoft.hg.repo.HgInvalidFileException; tikhomirov@463: import org.tmatesoft.hg.util.LogFacility; tikhomirov@463: import org.tmatesoft.hg.util.Path; tikhomirov@463: tikhomirov@463: /** tikhomirov@463: * Mercurial Queues Support. tikhomirov@463: * Access to MqExtension functionality. tikhomirov@463: * tikhomirov@501: * FIXME check we don't hold any mq files for too long, close them, use tikhomirov@501: * the same lock mechanism as mq does (if any). Check if MQ uses Mercurial's store lock tikhomirov@501: * tikhomirov@501: * @since 1.1 tikhomirov@463: * @author Artem Tikhomirov tikhomirov@463: * @author TMate Software Ltd. tikhomirov@463: */ tikhomirov@463: public class MqManager { tikhomirov@463: tikhomirov@465: private static final String PATCHES_DIR = "patches"; tikhomirov@465: tikhomirov@501: private final Internals repo; tikhomirov@463: private List applied = Collections.emptyList(); tikhomirov@463: private List allKnown = Collections.emptyList(); tikhomirov@464: private List queueNames = Collections.emptyList(); tikhomirov@465: private String activeQueue = PATCHES_DIR; tikhomirov@463: tikhomirov@501: /*package-local*/ MqManager(Internals internalRepo) { tikhomirov@501: repo = internalRepo; tikhomirov@463: } tikhomirov@463: tikhomirov@463: /** tikhomirov@463: * Updates manager with up-to-date state of the mercurial queues. tikhomirov@501: * @return this for convenience tikhomirov@463: */ tikhomirov@501: public MqManager refresh() throws HgInvalidControlFileException { tikhomirov@464: applied = allKnown = Collections.emptyList(); tikhomirov@464: queueNames = Collections.emptyList(); tikhomirov@490: final LogFacility log = repo.getSessionContext().getLog(); tikhomirov@463: try { tikhomirov@501: File queues = repo.getFileFromRepoDir("patches.queues"); tikhomirov@464: if (queues.isFile()) { tikhomirov@464: LineReader lr = new LineReader(queues, log).trimLines(true).skipEmpty(true); tikhomirov@480: lr.read(new LineReader.SimpleLineCollector(), queueNames = new LinkedList()); tikhomirov@464: } tikhomirov@465: final String queueLocation; // path under .hg to patch queue information (status, series and diff files) tikhomirov@501: File activeQueueFile = repo.getFileFromRepoDir("patches.queue"); tikhomirov@465: // file is there only if it's not default queue ('patches') that is active tikhomirov@464: if (activeQueueFile.isFile()) { tikhomirov@465: ArrayList contents = new ArrayList(); tikhomirov@480: new LineReader(activeQueueFile, log).read(new LineReader.SimpleLineCollector(), contents); tikhomirov@464: if (contents.isEmpty()) { tikhomirov@471: log.dump(getClass(), Warn, "File %s with active queue name is empty", activeQueueFile.getName()); tikhomirov@465: activeQueue = PATCHES_DIR; tikhomirov@465: queueLocation = PATCHES_DIR + '/'; tikhomirov@464: } else { tikhomirov@464: activeQueue = contents.get(0); tikhomirov@465: queueLocation = PATCHES_DIR + '-' + activeQueue + '/'; tikhomirov@464: } tikhomirov@464: } else { tikhomirov@465: activeQueue = PATCHES_DIR; tikhomirov@465: queueLocation = PATCHES_DIR + '/'; tikhomirov@464: } tikhomirov@465: final Path.Source patchLocation = new Path.Source() { tikhomirov@465: tikhomirov@471: public Path path(CharSequence p) { tikhomirov@465: StringBuilder sb = new StringBuilder(64); tikhomirov@465: sb.append(".hg/"); tikhomirov@465: sb.append(queueLocation); tikhomirov@465: sb.append(p); tikhomirov@465: return Path.create(sb); tikhomirov@465: } tikhomirov@465: }; tikhomirov@501: final File fileStatus = repo.getFileFromRepoDir(queueLocation + "status"); tikhomirov@501: final File fileSeries = repo.getFileFromRepoDir(queueLocation + "series"); tikhomirov@463: if (fileStatus.isFile()) { tikhomirov@480: new LineReader(fileStatus, log).read(new LineReader.LineConsumer>() { tikhomirov@463: tikhomirov@463: public boolean consume(String line, List result) throws IOException { tikhomirov@463: int sep = line.indexOf(':'); tikhomirov@463: if (sep == -1) { tikhomirov@471: log.dump(MqManager.class, Warn, "Bad line in %s:%s", fileStatus.getPath(), line); tikhomirov@463: return true; tikhomirov@463: } tikhomirov@463: Nodeid nid = Nodeid.fromAscii(line.substring(0, sep)); tikhomirov@463: String name = new String(line.substring(sep+1)); tikhomirov@465: result.add(new PatchRecord(nid, name, patchLocation.path(name))); tikhomirov@463: return true; tikhomirov@463: } tikhomirov@463: }, applied = new LinkedList()); tikhomirov@463: } tikhomirov@463: if (fileSeries.isFile()) { tikhomirov@464: final Map name2patch = new HashMap(); tikhomirov@464: for (PatchRecord pr : applied) { tikhomirov@464: name2patch.put(pr.getName(), pr); tikhomirov@464: } tikhomirov@464: LinkedList knownPatchNames = new LinkedList(); tikhomirov@480: new LineReader(fileSeries, log).read(new LineReader.SimpleLineCollector(), knownPatchNames); tikhomirov@464: // XXX read other queues? tikhomirov@464: allKnown = new ArrayList(knownPatchNames.size()); tikhomirov@464: for (String name : knownPatchNames) { tikhomirov@464: PatchRecord pr = name2patch.get(name); tikhomirov@464: if (pr == null) { tikhomirov@465: pr = new PatchRecord(null, name, patchLocation.path(name)); tikhomirov@463: } tikhomirov@464: allKnown.add(pr); tikhomirov@464: } tikhomirov@463: } tikhomirov@463: } catch (HgInvalidFileException ex) { tikhomirov@463: HgInvalidControlFileException th = new HgInvalidControlFileException(ex.getMessage(), ex.getCause(), ex.getFile()); tikhomirov@463: th.setStackTrace(ex.getStackTrace()); tikhomirov@463: throw th; tikhomirov@463: } tikhomirov@501: return this; tikhomirov@463: } tikhomirov@464: tikhomirov@464: /** tikhomirov@464: * Number of patches not yet applied tikhomirov@464: * @return positive value when there are tikhomirov@464: */ tikhomirov@464: public int getQueueSize() { tikhomirov@464: return getAllKnownPatches().size() - getAppliedPatches().size(); tikhomirov@464: } tikhomirov@463: tikhomirov@463: /** tikhomirov@463: * Subset of the patches from the queue that were already applied to the repository tikhomirov@464: *

Analog of 'hg qapplied' tikhomirov@463: * tikhomirov@463: *

Clients shall call {@link #refresh()} prior to first use tikhomirov@463: * @return collection of records in no particular order, may be empty if none applied tikhomirov@463: */ tikhomirov@463: public List getAppliedPatches() { tikhomirov@463: return Collections.unmodifiableList(applied); tikhomirov@463: } tikhomirov@463: tikhomirov@463: /** tikhomirov@465: * All of the patches in the active queue that MQ knows about for this repository tikhomirov@463: * tikhomirov@463: *

Clients shall call {@link #refresh()} prior to first use tikhomirov@463: * @return collection of records in no particular order, may be empty if there are no patches in the queue tikhomirov@463: */ tikhomirov@463: public List getAllKnownPatches() { tikhomirov@463: return Collections.unmodifiableList(allKnown); tikhomirov@463: } tikhomirov@463: tikhomirov@464: /** tikhomirov@464: * Name of the patch queue hg qqueue --active which is active now. tikhomirov@464: * @return patch queue name tikhomirov@464: */ tikhomirov@464: public String getActiveQueueName() { tikhomirov@464: return activeQueue; tikhomirov@464: } tikhomirov@464: tikhomirov@464: /** tikhomirov@464: * Patch queues known in the repository, hg qqueue -l analog. tikhomirov@464: * There's at least one patch queue (default one names 'patches'). Only one patch queue at a time is active. tikhomirov@464: * tikhomirov@464: * @return names of patch queues tikhomirov@464: */ tikhomirov@464: public List getQueueNames() { tikhomirov@464: return Collections.unmodifiableList(queueNames); tikhomirov@464: } tikhomirov@464: tikhomirov@475: public final class PatchRecord { tikhomirov@463: private final Nodeid nodeid; tikhomirov@463: private final String name; tikhomirov@463: private final Path location; tikhomirov@464: tikhomirov@464: // hashCode/equals might be useful if cons becomes public tikhomirov@463: tikhomirov@463: PatchRecord(Nodeid revision, String name, Path diffLocation) { tikhomirov@463: nodeid = revision; tikhomirov@463: this.name = name; tikhomirov@463: this.location = diffLocation; tikhomirov@463: } tikhomirov@463: tikhomirov@463: /** tikhomirov@463: * Identifies changeset of the patch that has been applied to the repository tikhomirov@463: * tikhomirov@463: * @return changeset revision or null if this patch is not yet applied tikhomirov@463: */ tikhomirov@463: public Nodeid getRevision() { tikhomirov@463: return nodeid; tikhomirov@463: } tikhomirov@463: tikhomirov@463: /** tikhomirov@463: * Identifies patch, either based on a user-supplied name (hg qnew patch-name) or tikhomirov@463: * an automatically generated name (like revisionIndex.diff for imported changesets). tikhomirov@463: * Clients shall not rely on this naming scheme, though. tikhomirov@463: * tikhomirov@463: * @return never null tikhomirov@463: */ tikhomirov@463: public String getName() { tikhomirov@463: return name; tikhomirov@463: } tikhomirov@463: tikhomirov@463: /** tikhomirov@463: * Location of diff file with the patch, relative to repository root tikhomirov@463: * @return path to the patch, never null tikhomirov@463: */ tikhomirov@463: public Path getPatchLocation() { tikhomirov@463: return location; tikhomirov@463: } tikhomirov@475: tikhomirov@475: @Override tikhomirov@475: public String toString() { tikhomirov@475: String fmt = "mq.PatchRecord[name:%s; %spath:%s]"; tikhomirov@475: String ni = nodeid != null ? String.format("applied as: %s; ", nodeid.shortNotation()) : ""; tikhomirov@475: return String.format(fmt, name, ni, location); tikhomirov@475: } tikhomirov@463: } tikhomirov@463: }