tikhomirov@64: /*
tikhomirov@64:  * Copyright (c) 2011 TMate Software Ltd
tikhomirov@64:  *  
tikhomirov@64:  * This program is free software; you can redistribute it and/or modify
tikhomirov@64:  * it under the terms of the GNU General Public License as published by
tikhomirov@64:  * the Free Software Foundation; version 2 of the License.
tikhomirov@64:  *
tikhomirov@64:  * This program is distributed in the hope that it will be useful,
tikhomirov@64:  * but WITHOUT ANY WARRANTY; without even the implied warranty of
tikhomirov@64:  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
tikhomirov@64:  * GNU General Public License for more details.
tikhomirov@64:  *
tikhomirov@64:  * For information on how to redistribute this software under
tikhomirov@64:  * the terms of a license other than GNU General Public License
tikhomirov@102:  * contact TMate Software at support@hg4j.com
tikhomirov@64:  */
tikhomirov@64: package org.tmatesoft.hg.core;
tikhomirov@64: 
tikhomirov@143: import static org.tmatesoft.hg.repo.HgRepository.*;
tikhomirov@143: import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION;
tikhomirov@74: import static org.tmatesoft.hg.repo.HgRepository.TIP;
tikhomirov@64: 
tikhomirov@64: import java.util.ConcurrentModificationException;
tikhomirov@64: import java.util.LinkedHashMap;
tikhomirov@64: import java.util.LinkedList;
tikhomirov@64: import java.util.List;
tikhomirov@64: 
tikhomirov@131: import org.tmatesoft.hg.core.HgLogCommand.FileRevision;
tikhomirov@74: import org.tmatesoft.hg.repo.HgManifest;
tikhomirov@74: import org.tmatesoft.hg.repo.HgRepository;
tikhomirov@133: import org.tmatesoft.hg.util.Path;
tikhomirov@64: import org.tmatesoft.hg.util.PathPool;
tikhomirov@142: import org.tmatesoft.hg.util.PathRewrite;
tikhomirov@64: 
tikhomirov@64: 
tikhomirov@64: /**
tikhomirov@131:  * Gives access to list of files in each revision (Mercurial manifest information), 'hg manifest' counterpart.
tikhomirov@131:  *  
tikhomirov@64:  * @author Artem Tikhomirov
tikhomirov@64:  * @author TMate Software Ltd.
tikhomirov@64:  */
tikhomirov@215: public class HgManifestCommand extends HgAbstractCommand<HgManifestCommand> {
tikhomirov@64: 	
tikhomirov@64: 	private final HgRepository repo;
tikhomirov@64: 	private Path.Matcher matcher;
tikhomirov@64: 	private int startRev = 0, endRev = TIP;
tikhomirov@64: 	private Handler visitor;
tikhomirov@64: 	private boolean needDirs = false;
tikhomirov@64: 	
tikhomirov@64: 	private final Mediator mediator = new Mediator();
tikhomirov@64: 
tikhomirov@131: 	public HgManifestCommand(HgRepository hgRepo) {
tikhomirov@123: 		repo = hgRepo;
tikhomirov@64: 	}
tikhomirov@64: 
tikhomirov@148: 	/**
tikhomirov@148: 	 * Parameterize command to visit revisions <code>[rev1..rev2]</code>.
tikhomirov@148: 	 * @param rev1 - local revision number to start from. Non-negative. May be {@link HgRepository#TIP} (rev2 argument shall be {@link HgRepository#TIP} as well, then) 
tikhomirov@148: 	 * @param rev2 - local revision number to end with, inclusive. Non-negative, greater or equal to rev1. May be {@link HgRepository#TIP}.
tikhomirov@148: 	 * @return <code>this</code> for convenience.
tikhomirov@148: 	 * @throws IllegalArgumentException if revision arguments are incorrect (see above).
tikhomirov@148: 	 */
tikhomirov@131: 	public HgManifestCommand range(int rev1, int rev2) {
tikhomirov@143: 		// XXX if manifest range is different from that of changelog, need conversion utils (external?)
tikhomirov@143: 		boolean badArgs = rev1 == BAD_REVISION || rev2 == BAD_REVISION || rev1 == WORKING_COPY || rev2 == WORKING_COPY;
tikhomirov@143: 		badArgs |= rev2 != TIP && rev2 < rev1; // range(3, 1);
tikhomirov@143: 		badArgs |= rev1 == TIP && rev2 != TIP; // range(TIP, 2), although this may be legitimate when TIP points to 2
tikhomirov@143: 		if (badArgs) {
tikhomirov@143: 			throw new IllegalArgumentException(String.format("Bad range: [%d, %d]", rev1, rev2));
tikhomirov@143: 		}
tikhomirov@143: 		startRev = rev1;
tikhomirov@143: 		endRev = rev2;
tikhomirov@143: 		return this;
tikhomirov@64: 	}
tikhomirov@64: 	
tikhomirov@131: 	public HgManifestCommand revision(int rev) {
tikhomirov@67: 		startRev = endRev = rev;
tikhomirov@67: 		return this;
tikhomirov@67: 	}
tikhomirov@67: 	
tikhomirov@131: 	public HgManifestCommand dirs(boolean include) {
tikhomirov@64: 		// XXX whether directories with directories only are include or not
tikhomirov@64: 		// now lists only directories with files
tikhomirov@64: 		needDirs = include;
tikhomirov@64: 		return this;
tikhomirov@64: 	}
tikhomirov@64: 	
tikhomirov@64: 	/**
tikhomirov@64: 	 * Limit manifest walk to a subset of files. 
tikhomirov@64: 	 * @param pathMatcher - filter, pass <code>null</code> to clear.
tikhomirov@64: 	 * @return <code>this</code> instance for convenience
tikhomirov@64: 	 */
tikhomirov@131: 	public HgManifestCommand match(Path.Matcher pathMatcher) {
tikhomirov@64: 		matcher = pathMatcher;
tikhomirov@64: 		return this;
tikhomirov@64: 	}
tikhomirov@64: 	
tikhomirov@148: 	/**
tikhomirov@148: 	 * Runs the command.
tikhomirov@148: 	 * @param handler - callback to get the outcome
tikhomirov@148: 	 * @throws IllegalArgumentException if handler is <code>null</code>
tikhomirov@148: 	 * @throws ConcurrentModificationException if this command is already in use (running)
tikhomirov@148: 	 */
tikhomirov@143: 	public void execute(Handler handler) {
tikhomirov@64: 		if (handler == null) {
tikhomirov@64: 			throw new IllegalArgumentException();
tikhomirov@64: 		}
tikhomirov@64: 		if (visitor != null) {
tikhomirov@64: 			throw new ConcurrentModificationException();
tikhomirov@64: 		}
tikhomirov@64: 		try {
tikhomirov@64: 			visitor = handler;
tikhomirov@64: 			mediator.start();
tikhomirov@64: 			repo.getManifest().walk(startRev, endRev, mediator);
tikhomirov@64: 		} finally {
tikhomirov@92: 			mediator.done();
tikhomirov@64: 			visitor = null;
tikhomirov@64: 		}
tikhomirov@64: 	}
tikhomirov@64: 
tikhomirov@64: 	/**
tikhomirov@64: 	 * Callback to walk file/directory tree of a revision
tikhomirov@64: 	 */
tikhomirov@64: 	public interface Handler {
tikhomirov@64: 		void begin(Nodeid manifestRevision);
tikhomirov@64: 		void dir(Path p); // optionally invoked (if walker was configured to spit out directories) prior to any files from this dir and subdirs
tikhomirov@64: 		void file(FileRevision fileRevision); // XXX allow to check p is invalid (df.exists())
tikhomirov@64: 		void end(Nodeid manifestRevision);
tikhomirov@64: 	}
tikhomirov@64: 
tikhomirov@132: 	// I'd rather let HgManifestCommand implement HgManifest.Inspector directly, but this pollutes API alot
tikhomirov@64: 	private class Mediator implements HgManifest.Inspector {
tikhomirov@142: 		// file names are likely to repeat in each revision, hence caching of Paths.
tikhomirov@142: 		// However, once HgManifest.Inspector switches to Path objects, perhaps global Path pool
tikhomirov@142: 		// might be more effective?
tikhomirov@64: 		private PathPool pathPool;
tikhomirov@64: 		private List<FileRevision> manifestContent;
tikhomirov@64: 		private Nodeid manifestNodeid;
tikhomirov@64: 		
tikhomirov@64: 		public void start() {
tikhomirov@142: 			// Manifest keeps normalized paths
tikhomirov@142: 			pathPool = new PathPool(new PathRewrite.Empty());
tikhomirov@64: 		}
tikhomirov@64: 		
tikhomirov@64: 		public void done() {
tikhomirov@64: 			manifestContent = null;
tikhomirov@64: 			pathPool = null;
tikhomirov@64: 		}
tikhomirov@64: 	
tikhomirov@64: 		public boolean begin(int revision, Nodeid nid) {
tikhomirov@64: 			if (needDirs && manifestContent == null) {
tikhomirov@64: 				manifestContent = new LinkedList<FileRevision>();
tikhomirov@64: 			}
tikhomirov@64: 			visitor.begin(manifestNodeid = nid);
tikhomirov@64: 			return true;
tikhomirov@64: 		}
tikhomirov@64: 		public boolean end(int revision) {
tikhomirov@64: 			if (needDirs) {
tikhomirov@64: 				LinkedHashMap<Path, LinkedList<FileRevision>> breakDown = new LinkedHashMap<Path, LinkedList<FileRevision>>();
tikhomirov@64: 				for (FileRevision fr : manifestContent) {
tikhomirov@64: 					Path filePath = fr.getPath();
tikhomirov@64: 					Path dirPath = pathPool.parent(filePath);
tikhomirov@64: 					LinkedList<FileRevision> revs = breakDown.get(dirPath);
tikhomirov@64: 					if (revs == null) {
tikhomirov@64: 						revs = new LinkedList<FileRevision>();
tikhomirov@64: 						breakDown.put(dirPath, revs);
tikhomirov@64: 					}
tikhomirov@64: 					revs.addLast(fr);
tikhomirov@64: 				}
tikhomirov@64: 				for (Path dir : breakDown.keySet()) {
tikhomirov@64: 					visitor.dir(dir);
tikhomirov@64: 					for (FileRevision fr : breakDown.get(dir)) {
tikhomirov@64: 						visitor.file(fr);
tikhomirov@64: 					}
tikhomirov@64: 				}
tikhomirov@64: 				manifestContent.clear();
tikhomirov@64: 			}
tikhomirov@64: 			visitor.end(manifestNodeid);
tikhomirov@64: 			manifestNodeid = null;
tikhomirov@64: 			return true;
tikhomirov@64: 		}
tikhomirov@64: 		public boolean next(Nodeid nid, String fname, String flags) {
tikhomirov@64: 			Path p = pathPool.path(fname);
tikhomirov@64: 			if (matcher != null && !matcher.accept(p)) {
tikhomirov@64: 				return true;
tikhomirov@64: 			}
tikhomirov@64: 			FileRevision fr = new FileRevision(repo, nid, p);
tikhomirov@64: 			if (needDirs) {
tikhomirov@64: 				manifestContent.add(fr);
tikhomirov@64: 			} else {
tikhomirov@64: 				visitor.file(fr);
tikhomirov@64: 			}
tikhomirov@64: 			return true;
tikhomirov@64: 		}
tikhomirov@64: 	}
tikhomirov@64: }