/*
* Copyright (c) 2012 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.repo.ext;
import static org.tmatesoft.hg.util.LogFacility.Severity.Warn;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.tmatesoft.hg.core.HgIOException;
import org.tmatesoft.hg.core.Nodeid;
import org.tmatesoft.hg.internal.Internals;
import org.tmatesoft.hg.internal.LineReader;
import org.tmatesoft.hg.repo.HgInvalidControlFileException;
import org.tmatesoft.hg.util.LogFacility;
import org.tmatesoft.hg.util.Path;
/**
* Mercurial Queues Support.
* Access to MqExtension functionality.
*
* @since 1.1
* @author Artem Tikhomirov
* @author TMate Software Ltd.
*/
public class MqManager {
private static final String PATCHES_DIR = "patches";
private final Internals repo;
private List<PatchRecord> applied = Collections.emptyList();
private List<PatchRecord> allKnown = Collections.emptyList();
private List<String> queueNames = Collections.emptyList();
private String activeQueue = PATCHES_DIR;
/*package-local*/ MqManager(Internals internalRepo) {
repo = internalRepo;
}
/**
* Updates manager with up-to-date state of the mercurial queues.
* @return <code>this</code> for convenience
*/
public MqManager refresh() throws HgInvalidControlFileException {
// MQ doesn't seem to use any custom lock mechanism.
// MQ uses Mercurial's wc/store lock when updating repository (strip/new queue)
applied = allKnown = Collections.emptyList();
queueNames = Collections.emptyList();
final LogFacility log = repo.getSessionContext().getLog();
try {
File queues = repo.getFileFromRepoDir("patches.queues");
if (queues.isFile()) {
LineReader lr = new LineReader(queues, log).trimLines(true).skipEmpty(true);
lr.read(new LineReader.SimpleLineCollector(), queueNames = new LinkedList<String>());
}
final String queueLocation; // path under .hg to patch queue information (status, series and diff files)
File activeQueueFile = repo.getFileFromRepoDir("patches.queue");
// file is there only if it's not default queue ('patches') that is active
if (activeQueueFile.isFile()) {
ArrayList<String> contents = new ArrayList<String>();
new LineReader(activeQueueFile, log).read(new LineReader.SimpleLineCollector(), contents);
if (contents.isEmpty()) {
log.dump(getClass(), Warn, "File %s with active queue name is empty", activeQueueFile.getName());
activeQueue = PATCHES_DIR;
queueLocation = PATCHES_DIR + '/';
} else {
activeQueue = contents.get(0);
queueLocation = PATCHES_DIR + '-' + activeQueue + '/';
}
} else {
activeQueue = PATCHES_DIR;
queueLocation = PATCHES_DIR + '/';
}
final Path.Source patchLocation = new Path.Source() {
public Path path(CharSequence p) {
StringBuilder sb = new StringBuilder(64);
sb.append(".hg/");
sb.append(queueLocation);
sb.append(p);
return Path.create(sb);
}
};
final File fileStatus = repo.getFileFromRepoDir(queueLocation + "status");
final File fileSeries = repo.getFileFromRepoDir(queueLocation + "series");
if (fileStatus.isFile()) {
new LineReader(fileStatus, log).read(new LineReader.LineConsumer<List<PatchRecord>>() {
public boolean consume(String line, List<PatchRecord> result) throws IOException {
int sep = line.indexOf(':');
if (sep == -1) {
log.dump(MqManager.class, Warn, "Bad line in %s:%s", fileStatus.getPath(), line);
return true;
}
Nodeid nid = Nodeid.fromAscii(line.substring(0, sep));
String name = new String(line.substring(sep+1));
result.add(new PatchRecord(nid, name, patchLocation.path(name)));
return true;
}
}, applied = new LinkedList<PatchRecord>());
}
if (fileSeries.isFile()) {
final Map<String,PatchRecord> name2patch = new HashMap<String, PatchRecord>();
for (PatchRecord pr : applied) {
name2patch.put(pr.getName(), pr);
}
LinkedList<String> knownPatchNames = new LinkedList<String>();
new LineReader(fileSeries, log).read(new LineReader.SimpleLineCollector(), knownPatchNames);
// XXX read other queues?
allKnown = new ArrayList<PatchRecord>(knownPatchNames.size());
for (String name : knownPatchNames) {
PatchRecord pr = name2patch.get(name);
if (pr == null) {
pr = new PatchRecord(null, name, patchLocation.path(name));
}
allKnown.add(pr);
}
}
} catch (HgIOException ex) {
throw new HgInvalidControlFileException(ex, true);
}
return this;
}
/**
* Number of patches not yet applied
* @return positive value when there are
*/
public int getQueueSize() {
return getAllKnownPatches().size() - getAppliedPatches().size();
}
/**
* Subset of the patches from the queue that were already applied to the repository
* <p>Analog of 'hg qapplied'
*
* <p>Clients shall call {@link #refresh()} prior to first use
* @return collection of records in no particular order, may be empty if none applied
*/
public List<PatchRecord> getAppliedPatches() {
return Collections.unmodifiableList(applied);
}
/**
* All of the patches in the active queue that MQ knows about for this repository
*
* <p>Clients shall call {@link #refresh()} prior to first use
* @return collection of records in no particular order, may be empty if there are no patches in the queue
*/
public List<PatchRecord> getAllKnownPatches() {
return Collections.unmodifiableList(allKnown);
}
/**
* Name of the patch queue <code>hg qqueue --active</code> which is active now.
* @return patch queue name
*/
public String getActiveQueueName() {
return activeQueue;
}
/**
* Patch queues known in the repository, <code>hg qqueue -l</code> analog.
* There's at least one patch queue (default one names 'patches'). Only one patch queue at a time is active.
*
* @return names of patch queues
*/
public List<String> getQueueNames() {
return Collections.unmodifiableList(queueNames);
}
public final class PatchRecord {
private final Nodeid nodeid;
private final String name;
private final Path location;
// hashCode/equals might be useful if cons becomes public
PatchRecord(Nodeid revision, String name, Path diffLocation) {
nodeid = revision;
this.name = name;
this.location = diffLocation;
}
/**
* Identifies changeset of the patch that has been applied to the repository
*
* @return changeset revision or <code>null</code> if this patch is not yet applied
*/
public Nodeid getRevision() {
return nodeid;
}
/**
* Identifies patch, either based on a user-supplied name (<code>hg qnew <i>patch-name</i></code>) or
* an automatically generated name (like <code><i>revisionIndex</i>.diff</code> for imported changesets).
* Clients shall not rely on this naming scheme, though.
*
* @return never <code>null</code>
*/
public String getName() {
return name;
}
/**
* Location of diff file with the patch, relative to repository root
* @return path to the patch, never <code>null</code>
*/
public Path getPatchLocation() {
return location;
}
@Override
public String toString() {
String fmt = "mq.PatchRecord[name:%s; %spath:%s]";
String ni = nodeid != null ? String.format("applied as: %s; ", nodeid.shortNotation()) : "";
return String.format(fmt, name, ni, location);
}
}
}