view src/org/tmatesoft/hg/core/HgManifestCommand.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 98ff1fb49abe
children
line wrap: on
line source
/*
 * Copyright (c) 2011-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.core;

import static org.tmatesoft.hg.repo.HgRepository.*;

import java.util.ConcurrentModificationException;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;

import org.tmatesoft.hg.internal.CsetParamKeeper;
import org.tmatesoft.hg.internal.PathPool;
import org.tmatesoft.hg.repo.HgManifest;
import org.tmatesoft.hg.repo.HgManifest.Flags;
import org.tmatesoft.hg.repo.HgRepository;
import org.tmatesoft.hg.repo.HgRuntimeException;
import org.tmatesoft.hg.util.CancelSupport;
import org.tmatesoft.hg.util.CancelledException;
import org.tmatesoft.hg.util.Path;
import org.tmatesoft.hg.util.PathRewrite;


/**
 * Gives access to list of files in each revision (Mercurial manifest information), 'hg manifest' counterpart.
 *  
 * @author Artem Tikhomirov
 * @author TMate Software Ltd.
 */
public class HgManifestCommand extends HgAbstractCommand<HgManifestCommand> {
	
	private final HgRepository repo;
	private Path.Matcher matcher;
	private int startRev = 0, endRev = TIP;
	private HgManifestHandler visitor;
	private boolean needDirs = false;
	
	private final Mediator mediator = new Mediator();

	public HgManifestCommand(HgRepository hgRepo) {
		repo = hgRepo;
	}

	/**
	 * Parameterize command to visit revisions <code>[rev1..rev2]</code>.
	 * @param rev1 - revision local index to start from. Non-negative. May be {@link HgRepository#TIP} (rev2 argument shall be {@link HgRepository#TIP} as well, then) 
	 * @param rev2 - revision local index to end with, inclusive. Non-negative, greater or equal to rev1. May be {@link HgRepository#TIP}.
	 * @return <code>this</code> for convenience.
	 * @throws IllegalArgumentException if revision arguments are incorrect (see above).
	 */
	public HgManifestCommand range(int rev1, int rev2) {
		// XXX if manifest range is different from that of changelog, need conversion utils (external?)
		boolean badArgs = rev1 == BAD_REVISION || rev2 == BAD_REVISION || rev1 == WORKING_COPY || rev2 == WORKING_COPY;
		badArgs |= rev2 != TIP && rev2 < rev1; // range(3, 1);
		badArgs |= rev1 == TIP && rev2 != TIP; // range(TIP, 2), although this may be legitimate when TIP points to 2
		if (badArgs) {
			// TODO [2.0 API break] throw checked HgBadArgumentException instead
			throw new IllegalArgumentException(String.format("Bad range: [%d, %d]", rev1, rev2));
		}
		startRev = rev1;
		endRev = rev2;
		return this;
	}
	
	/**
	 * Limit command to visit specific subset of repository revisions
	 * 
	 * @see #range(int, int)
	 * @param cset1 range start revision
	 * @param cset2 range end revision
	 * @return <code>this</code> instance for convenience
	 * @throws HgBadArgumentException if revisions are not valid changeset identifiers
	 */
	public HgManifestCommand range(Nodeid cset1, Nodeid cset2) throws HgBadArgumentException {
		CsetParamKeeper pk = new CsetParamKeeper(repo);
		int r1 = pk.set(cset1).get();
		int r2 = pk.set(cset2).get();
		return range(r1, r2);
	}
	
	/**
	 * Select changeset for the command using revision index 
	 * @param csetRevisionIndex index of changeset revision
	 * @return <code>this</code> for convenience.
	 */
	public HgManifestCommand changeset(int csetRevisionIndex) {
		// TODO [2.0 API break] shall throw HgBadArgumentException, like other commands do
		return range(csetRevisionIndex, csetRevisionIndex);
	}
	
	/**
	 * Select changeset for the command
	 * 
	 * @param nid changeset revision
	 * @return <code>this</code> for convenience
	 * @throws HgBadArgumentException if failed to find supplied changeset revision 
	 */
	public HgManifestCommand changeset(Nodeid nid) throws HgBadArgumentException {
		// XXX also see HgLogCommand#changeset(Nodeid)
		final int csetRevIndex = new CsetParamKeeper(repo).set(nid).get();
		return range(csetRevIndex, csetRevIndex);
	}

	public HgManifestCommand dirs(boolean include) {
		// XXX whether directories with directories only are include or not
		// now lists only directories with files
		needDirs = include;
		return this;
	}
	
	/**
	 * Limit manifest walk to a subset of files. 
	 * @param pathMatcher - filter, pass <code>null</code> to clear.
	 * @return <code>this</code> instance for convenience
	 */
	public HgManifestCommand match(Path.Matcher pathMatcher) {
		matcher = pathMatcher;
		return this;
	}
	
	/**
	 * With all parameters set, execute the command.
	 * 
	 * @param handler - callback to get the outcome
 	 * @throws HgCallbackTargetException propagated exception from the handler
	 * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state
	 * @throws CancelledException if execution of the command was cancelled
	 * @throws IllegalArgumentException if handler is <code>null</code>
	 * @throws ConcurrentModificationException if this command is already in use (running)
	 */
	public void execute(HgManifestHandler handler) throws HgCallbackTargetException, HgException, CancelledException {
		if (handler == null) {
			throw new IllegalArgumentException();
		}
		if (visitor != null) {
			throw new ConcurrentModificationException();
		}
		try {
			visitor = handler;
			mediator.start(getCancelSupport(handler, true));
			repo.getManifest().walk(startRev, endRev, mediator);
			mediator.checkFailure();
		} catch (HgRuntimeException ex) {
			throw new HgLibraryFailureException(ex);
		} finally {
			mediator.done();
			visitor = null;
		}
	}

	// I'd rather let HgManifestCommand implement HgManifest.Inspector directly, but this pollutes API alot
	private class Mediator implements HgManifest.Inspector {
		// file names are likely to repeat in each revision, hence caching of Paths.
		// However, once HgManifest.Inspector switches to Path objects, perhaps global Path pool
		// might be more effective?
		private PathPool pathPool;
		private List<HgFileRevision> manifestContent;
		private Nodeid manifestNodeid;
		private Exception failure;
		private CancelSupport cancelHelper;
		
		public void start(CancelSupport cs) {
			assert cs != null;
			// Manifest keeps normalized paths
			pathPool = new PathPool(new PathRewrite.Empty());
			cancelHelper = cs;
		}
		
		public void done() {
			manifestContent = null;
			pathPool = null;
		}
		
		private void recordFailure(HgCallbackTargetException ex) {
			failure = ex;
		}
		private void recordCancel(CancelledException ex) {
			failure = ex;
		}

		public void checkFailure() throws HgCallbackTargetException, CancelledException {
			// TODO post-1.0 perhaps, can combine this code (record/checkFailure) for reuse in more classes (e.g. in Revlog)
			if (failure instanceof HgCallbackTargetException) {
				HgCallbackTargetException ex = (HgCallbackTargetException) failure;
				failure = null;
				throw ex;
			}
			if (failure instanceof CancelledException) {
				CancelledException ex = (CancelledException) failure;
				failure = null;
				throw ex;
			}
		}
	
		public boolean begin(int manifestRevision, Nodeid nid, int changelogRevision) throws HgRuntimeException {
			if (needDirs && manifestContent == null) {
				manifestContent = new LinkedList<HgFileRevision>();
			}
			try {
				visitor.begin(manifestNodeid = nid);
				cancelHelper.checkCancelled();
				return true;
			} catch (HgCallbackTargetException ex) {
				recordFailure(ex);
				return false;
			} catch (CancelledException ex) {
				recordCancel(ex);
				return false;
			}
		}
		public boolean end(int revision) throws HgRuntimeException {
			try {
				if (needDirs) {
					LinkedHashMap<Path, LinkedList<HgFileRevision>> breakDown = new LinkedHashMap<Path, LinkedList<HgFileRevision>>();
					for (HgFileRevision fr : manifestContent) {
						Path filePath = fr.getPath();
						Path dirPath = pathPool.parent(filePath);
						LinkedList<HgFileRevision> revs = breakDown.get(dirPath);
						if (revs == null) {
							revs = new LinkedList<HgFileRevision>();
							breakDown.put(dirPath, revs);
						}
						revs.addLast(fr);
					}
					for (Path dir : breakDown.keySet()) {
						visitor.dir(dir);
						cancelHelper.checkCancelled();
						for (HgFileRevision fr : breakDown.get(dir)) {
							visitor.file(fr);
						}
					}
					manifestContent.clear();
				}
				visitor.end(manifestNodeid);
				cancelHelper.checkCancelled();
				return true;
			} catch (HgCallbackTargetException ex) {
				recordFailure(ex);
				return false;
			} catch (CancelledException ex) {
				recordCancel(ex);
				return false;
			} finally {
				manifestNodeid = null;
			}
		}
		
		public boolean next(Nodeid nid, Path fname, Flags flags) throws HgRuntimeException {
			if (matcher != null && !matcher.accept(fname)) {
				return true;
			}
			try {
				HgFileRevision fr = new HgFileRevision(repo, nid, flags, fname);
				if (needDirs) {
					manifestContent.add(fr);
				} else {
					visitor.file(fr);
				}
				return true;
			} catch (HgCallbackTargetException ex) {
				recordFailure(ex);
				return false;
			}
		}
	}
}