view src/org/tmatesoft/hg/repo/ext/MqManager.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 6526d8adbc0f
children
line wrap: on
line source
/*
 * 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);
		}
	}
}