tikhomirov@181: /*
tikhomirov@423:  * Copyright (c) 2011-2012 TMate Software Ltd
tikhomirov@181:  *  
tikhomirov@181:  * This program is free software; you can redistribute it and/or modify
tikhomirov@181:  * it under the terms of the GNU General Public License as published by
tikhomirov@181:  * the Free Software Foundation; version 2 of the License.
tikhomirov@181:  *
tikhomirov@181:  * This program is distributed in the hope that it will be useful,
tikhomirov@181:  * but WITHOUT ANY WARRANTY; without even the implied warranty of
tikhomirov@181:  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
tikhomirov@181:  * GNU General Public License for more details.
tikhomirov@181:  *
tikhomirov@181:  * For information on how to redistribute this software under
tikhomirov@181:  * the terms of a license other than GNU General Public License
tikhomirov@181:  * contact TMate Software at support@hg4j.com
tikhomirov@181:  */
tikhomirov@181: package org.tmatesoft.hg.core;
tikhomirov@181: 
tikhomirov@192: import java.util.ArrayList;
tikhomirov@202: import java.util.HashSet;
tikhomirov@202: import java.util.Iterator;
tikhomirov@192: import java.util.LinkedHashSet;
tikhomirov@192: import java.util.LinkedList;
tikhomirov@181: import java.util.List;
tikhomirov@194: import java.util.Set;
tikhomirov@194: import java.util.TreeSet;
tikhomirov@181: 
tikhomirov@192: import org.tmatesoft.hg.internal.RepositoryComparator;
tikhomirov@192: import org.tmatesoft.hg.internal.RepositoryComparator.BranchChain;
tikhomirov@192: import org.tmatesoft.hg.repo.HgBundle;
tikhomirov@192: import org.tmatesoft.hg.repo.HgChangelog;
tikhomirov@427: import org.tmatesoft.hg.repo.HgChangelog.RawChangeset;
tikhomirov@423: import org.tmatesoft.hg.repo.HgInvalidControlFileException;
tikhomirov@423: import org.tmatesoft.hg.repo.HgInvalidStateException;
tikhomirov@192: import org.tmatesoft.hg.repo.HgRemoteRepository;
tikhomirov@181: import org.tmatesoft.hg.repo.HgRepository;
tikhomirov@427: import org.tmatesoft.hg.repo.HgRuntimeException;
tikhomirov@432: import org.tmatesoft.hg.repo.HgParentChildMap;
tikhomirov@181: import org.tmatesoft.hg.util.CancelledException;
tikhomirov@215: import org.tmatesoft.hg.util.ProgressSupport;
tikhomirov@181: 
tikhomirov@181: /**
tikhomirov@192:  * Command to find out changes available in a remote repository, missing locally.
tikhomirov@192:  * 
tikhomirov@181:  * @author Artem Tikhomirov
tikhomirov@181:  * @author TMate Software Ltd.
tikhomirov@181:  */
tikhomirov@215: public class HgIncomingCommand extends HgAbstractCommand<HgIncomingCommand> {
tikhomirov@181: 
tikhomirov@192: 	private final HgRepository localRepo;
tikhomirov@192: 	private HgRemoteRepository remoteRepo;
tikhomirov@202: 	@SuppressWarnings("unused")
tikhomirov@181: 	private boolean includeSubrepo;
tikhomirov@192: 	private RepositoryComparator comparator;
tikhomirov@192: 	private List<BranchChain> missingBranches;
tikhomirov@432: 	private HgParentChildMap<HgChangelog> parentHelper;
tikhomirov@194: 	private Set<String> branches;
tikhomirov@181: 
tikhomirov@181: 	public HgIncomingCommand(HgRepository hgRepo) {
tikhomirov@192: 	 	localRepo = hgRepo;
tikhomirov@192: 	}
tikhomirov@192: 	
tikhomirov@192: 	public HgIncomingCommand against(HgRemoteRepository hgRemote) {
tikhomirov@192: 		remoteRepo = hgRemote;
tikhomirov@192: 		comparator = null;
tikhomirov@192: 		missingBranches = null;
tikhomirov@192: 		return this;
tikhomirov@181: 	}
tikhomirov@181: 
tikhomirov@181: 	/**
tikhomirov@194: 	 * Select specific branch to push.
tikhomirov@194: 	 * Multiple branch specification possible (changeset from any of these would be included in result).
tikhomirov@419: 	 * Note, {@link #executeLite()} does not respect this setting.
tikhomirov@192: 	 * 
tikhomirov@194: 	 * @param branch - branch name, case-sensitive, non-null.
tikhomirov@181: 	 * @return <code>this</code> for convenience
tikhomirov@194: 	 * @throws IllegalArgumentException when branch argument is null
tikhomirov@181: 	 */
tikhomirov@181: 	public HgIncomingCommand branch(String branch) {
tikhomirov@194: 		if (branch == null) {
tikhomirov@194: 			throw new IllegalArgumentException();
tikhomirov@194: 		}
tikhomirov@194: 		if (branches == null) {
tikhomirov@194: 			branches = new TreeSet<String>();
tikhomirov@194: 		}
tikhomirov@194: 		branches.add(branch);
tikhomirov@194: 		return this;
tikhomirov@181: 	}
tikhomirov@181: 	
tikhomirov@181: 	/**
tikhomirov@192: 	 * PLACEHOLDER, NOT IMPLEMENTED YET.
tikhomirov@192: 	 * 
tikhomirov@181: 	 * Whether to include sub-repositories when collecting changes, default is <code>true</code> XXX or false?
tikhomirov@181: 	 * @return <code>this</code> for convenience
tikhomirov@181: 	 */
tikhomirov@181: 	public HgIncomingCommand subrepo(boolean include) {
tikhomirov@181: 		includeSubrepo = include;
tikhomirov@181: 		throw HgRepository.notImplemented();
tikhomirov@181: 	}
tikhomirov@181: 
tikhomirov@181: 	/**
tikhomirov@194: 	 * Lightweight check for incoming changes, gives only list of revisions to pull.
tikhomirov@194: 	 * Reported changes are from any branch (limits set by {@link #branch(String)} are not taken into account. 
tikhomirov@181: 	 *   
tikhomirov@181: 	 * @return list of nodes present at remote and missing locally
tikhomirov@295: 	 * @throws HgRemoteConnectionException when failed to communicate with remote repository
tikhomirov@427: 	 * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state
tikhomirov@380: 	 * @throws CancelledException if execution of the command was cancelled
tikhomirov@181: 	 */
tikhomirov@427: 	public List<Nodeid> executeLite() throws HgException, CancelledException {
tikhomirov@427: 		try {
tikhomirov@427: 			LinkedHashSet<Nodeid> result = new LinkedHashSet<Nodeid>();
tikhomirov@427: 			RepositoryComparator repoCompare = getComparator();
tikhomirov@427: 			for (BranchChain bc : getMissingBranches()) {
tikhomirov@427: 				List<Nodeid> missing = repoCompare.visitBranches(bc);
tikhomirov@427: 				HashSet<Nodeid> common = new HashSet<Nodeid>(); // ordering is irrelevant  
tikhomirov@427: 				repoCompare.collectKnownRoots(bc, common);
tikhomirov@427: 				// missing could only start with common elements. Once non-common, rest is just distinct branch revision trails.
tikhomirov@427: 				for (Iterator<Nodeid> it = missing.iterator(); it.hasNext() && common.contains(it.next()); it.remove()) ; 
tikhomirov@427: 				result.addAll(missing);
tikhomirov@427: 			}
tikhomirov@427: 			ArrayList<Nodeid> rv = new ArrayList<Nodeid>(result);
tikhomirov@427: 			return rv;
tikhomirov@427: 		} catch (HgRuntimeException ex) {
tikhomirov@427: 			throw new HgLibraryFailureException(ex);
tikhomirov@192: 		}
tikhomirov@181: 	}
tikhomirov@181: 
tikhomirov@181: 	/**
tikhomirov@181: 	 * Full information about incoming changes
tikhomirov@181: 	 * 
tikhomirov@295: 	 * @throws HgCallbackTargetException to re-throw exception from the handler
tikhomirov@427: 	 * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state
tikhomirov@380: 	 * @throws CancelledException if execution of the command was cancelled
tikhomirov@181: 	 */
tikhomirov@427: 	public void executeFull(final HgChangesetHandler handler) throws HgCallbackTargetException, HgException, CancelledException {
tikhomirov@192: 		if (handler == null) {
tikhomirov@192: 			throw new IllegalArgumentException("Delegate can't be null");
tikhomirov@192: 		}
tikhomirov@215: 		final List<Nodeid> common = getCommon();
tikhomirov@202: 		HgBundle changegroup = remoteRepo.getChanges(common);
tikhomirov@215: 		final ProgressSupport ps = getProgressSupport(handler);
tikhomirov@192: 		try {
tikhomirov@322: 			final ChangesetTransformer transformer = new ChangesetTransformer(localRepo, handler, getParentHelper(), ps, getCancelSupport(handler, true));
tikhomirov@215: 			transformer.limitBranches(branches);
tikhomirov@192: 			changegroup.changes(localRepo, new HgChangelog.Inspector() {
tikhomirov@208: 				private int localIndex;
tikhomirov@432: 				private final HgParentChildMap<HgChangelog> parentHelper;
tikhomirov@208: 			
tikhomirov@192: 				{
tikhomirov@192: 					parentHelper = getParentHelper();
tikhomirov@208: 					// new revisions, if any, would be added after all existing, and would get numbered started with last+1
tikhomirov@208: 					localIndex = localRepo.getChangelog().getRevisionCount();
tikhomirov@192: 				}
tikhomirov@192: 				
tikhomirov@192: 				public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) {
tikhomirov@192: 					if (parentHelper.knownNode(nodeid)) {
tikhomirov@192: 						if (!common.contains(nodeid)) {
tikhomirov@423: 							throw new HgInvalidStateException("Bundle shall not report known nodes other than roots we've supplied");
tikhomirov@192: 						}
tikhomirov@192: 						return;
tikhomirov@192: 					}
tikhomirov@208: 					transformer.next(localIndex++, nodeid, cset);
tikhomirov@192: 				}
tikhomirov@192: 			});
tikhomirov@215: 			transformer.checkFailure();
tikhomirov@427: 		} catch (HgRuntimeException ex) {
tikhomirov@427: 			throw new HgLibraryFailureException(ex);
tikhomirov@215: 		} finally {
tikhomirov@215: 			ps.done();
tikhomirov@192: 		}
tikhomirov@192: 	}
tikhomirov@192: 
tikhomirov@366: 	private RepositoryComparator getComparator() throws HgInvalidControlFileException, CancelledException {
tikhomirov@192: 		if (remoteRepo == null) {
tikhomirov@215: 			throw new IllegalArgumentException("Shall specify remote repository to compare against", null);
tikhomirov@192: 		}
tikhomirov@192: 		if (comparator == null) {
tikhomirov@192: 			comparator = new RepositoryComparator(getParentHelper(), remoteRepo);
tikhomirov@202: //			comparator.compare(context); // XXX meanwhile we use distinct path to calculate common  
tikhomirov@192: 		}
tikhomirov@192: 		return comparator;
tikhomirov@192: 	}
tikhomirov@192: 	
tikhomirov@432: 	private HgParentChildMap<HgChangelog> getParentHelper() throws HgInvalidControlFileException {
tikhomirov@192: 		if (parentHelper == null) {
tikhomirov@432: 			parentHelper = new HgParentChildMap<HgChangelog>(localRepo.getChangelog());
tikhomirov@192: 			parentHelper.init();
tikhomirov@192: 		}
tikhomirov@192: 		return parentHelper;
tikhomirov@192: 	}
tikhomirov@192: 	
tikhomirov@366: 	private List<BranchChain> getMissingBranches() throws HgRemoteConnectionException, HgInvalidControlFileException, CancelledException {
tikhomirov@192: 		if (missingBranches == null) {
tikhomirov@215: 			missingBranches = getComparator().calculateMissingBranches();
tikhomirov@192: 		}
tikhomirov@192: 		return missingBranches;
tikhomirov@192: 	}
tikhomirov@192: 
tikhomirov@366: 	private List<Nodeid> getCommon() throws HgRemoteConnectionException, HgInvalidControlFileException, CancelledException {
tikhomirov@202: //		return getComparator(context).getCommon();
tikhomirov@192: 		final LinkedHashSet<Nodeid> common = new LinkedHashSet<Nodeid>();
tikhomirov@192: 		// XXX common can be obtained from repoCompare, but at the moment it would almost duplicate work of calculateMissingBranches
tikhomirov@192: 		// once I refactor latter, common shall be taken from repoCompare.
tikhomirov@215: 		RepositoryComparator repoCompare = getComparator();
tikhomirov@215: 		for (BranchChain bc : getMissingBranches()) {
tikhomirov@202: 			repoCompare.collectKnownRoots(bc, common);
tikhomirov@192: 		}
tikhomirov@192: 		return new LinkedList<Nodeid>(common);
tikhomirov@181: 	}
tikhomirov@181: }