view src/org/tmatesoft/hg/core/HgMergeCommand.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 4ffc17c0b534
children
line wrap: on
line source
/*
 * Copyright (c) 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.BAD_REVISION;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

import org.tmatesoft.hg.internal.Callback;
import org.tmatesoft.hg.internal.CsetParamKeeper;
import org.tmatesoft.hg.internal.DirstateBuilder;
import org.tmatesoft.hg.internal.DirstateReader;
import org.tmatesoft.hg.internal.Experimental;
import org.tmatesoft.hg.internal.FileUtils;
import org.tmatesoft.hg.internal.Internals;
import org.tmatesoft.hg.internal.ManifestRevision;
import org.tmatesoft.hg.internal.MergeStateBuilder;
import org.tmatesoft.hg.internal.Pool;
import org.tmatesoft.hg.internal.Transaction;
import org.tmatesoft.hg.internal.WorkingDirFileWriter;
import org.tmatesoft.hg.repo.HgChangelog;
import org.tmatesoft.hg.repo.HgManifest;
import org.tmatesoft.hg.repo.HgParentChildMap;
import org.tmatesoft.hg.repo.HgRepository;
import org.tmatesoft.hg.repo.HgRepositoryLock;
import org.tmatesoft.hg.repo.HgRevisionMap;
import org.tmatesoft.hg.repo.HgRuntimeException;
import org.tmatesoft.hg.util.CancelledException;
import org.tmatesoft.hg.util.Path;

/**
 * Merge two revisions, 'hg merge REV' counterpart
 * 
 * @author Artem Tikhomirov
 * @author TMate Software Ltd.
 * @since 1.2
 */
@Experimental(reason="Provisional API. Work in progress")
public class HgMergeCommand extends HgAbstractCommand<HgMergeCommand> {

	private final HgRepository repo;
	private int firstCset, secondCset, ancestorCset;

	public HgMergeCommand(HgRepository hgRepo) {
		repo = hgRepo;
		firstCset = secondCset = ancestorCset = BAD_REVISION;
	}
	
	public HgMergeCommand changeset(Nodeid changeset) throws HgBadArgumentException {
		initHeadsAndAncestor(new CsetParamKeeper(repo).set(changeset).get());
		return this;
	}
	
	public HgMergeCommand changeset(int revisionIndex) throws HgBadArgumentException {
		initHeadsAndAncestor(new CsetParamKeeper(repo).set(revisionIndex).get());
		return this;
	}

	public void execute(Mediator mediator) throws HgCallbackTargetException, HgRepositoryLockException, HgIOException, HgLibraryFailureException, CancelledException {
		if (firstCset == BAD_REVISION || secondCset == BAD_REVISION || ancestorCset == BAD_REVISION) {
			throw new IllegalArgumentException("Merge heads and their ancestors are not initialized");
		}
		final HgRepositoryLock wdLock = repo.getWorkingDirLock();
		wdLock.acquire();
		try {
			Pool<Nodeid> cacheRevs = new Pool<Nodeid>();
			Pool<Path> cacheFiles = new Pool<Path>();

			Internals implRepo = Internals.getInstance(repo);
			final DirstateBuilder dirstateBuilder = new DirstateBuilder(implRepo);
			dirstateBuilder.fillFrom(new DirstateReader(implRepo, new Path.SimpleSource(repo.getSessionContext().getPathFactory(), cacheFiles)));
			final HgChangelog clog = repo.getChangelog();
			final Nodeid headCset1 = clog.getRevision(firstCset);
			dirstateBuilder.parents(headCset1, clog.getRevision(secondCset));
			//
			MergeStateBuilder mergeStateBuilder = new MergeStateBuilder(implRepo);
			mergeStateBuilder.prepare(headCset1);

			ManifestRevision m1, m2, ma;
			m1 = new ManifestRevision(cacheRevs, cacheFiles).init(repo, firstCset);
			m2 = new ManifestRevision(cacheRevs, cacheFiles).init(repo, secondCset);
			ma = new ManifestRevision(cacheRevs, cacheFiles).init(repo, ancestorCset);
			Transaction transaction = implRepo.getTransactionFactory().create(repo);
			ResolverImpl resolver = new ResolverImpl(implRepo, dirstateBuilder, mergeStateBuilder);
			try {
				for (Path f : m1.files()) {
					Nodeid fileRevBase, fileRevA, fileRevB;
					if (m2.contains(f)) {
						fileRevA = m1.nodeid(f);
						fileRevB = m2.nodeid(f);
						fileRevBase = ma.contains(f) ? ma.nodeid(f) : null;
						if (fileRevA.equals(fileRevB)) {
							HgFileRevision fr = new HgFileRevision(repo, fileRevA, m1.flags(f), f);
							resolver.presentState(f, fr, fr, null);
							mediator.same(fr, resolver);
						} else if (fileRevBase == fileRevA) {
							assert fileRevBase != null;
							HgFileRevision frBase = new HgFileRevision(repo, fileRevBase, ma.flags(f), f);
							HgFileRevision frSecond= new HgFileRevision(repo, fileRevB, m2.flags(f), f);
							resolver.presentState(f, frBase, frSecond, frBase);
							mediator.fastForwardB(frBase, frSecond, resolver);
						} else if (fileRevBase == fileRevB) {
							assert fileRevBase != null;
							HgFileRevision frBase = new HgFileRevision(repo, fileRevBase, ma.flags(f), f);
							HgFileRevision frFirst = new HgFileRevision(repo, fileRevA, m1.flags(f), f);
							resolver.presentState(f, frFirst, frBase, frBase);
							mediator.fastForwardA(frBase, frFirst, resolver);
						} else {
							HgFileRevision frBase = fileRevBase == null ? null : new HgFileRevision(repo, fileRevBase, ma.flags(f), f);
							HgFileRevision frFirst = new HgFileRevision(repo, fileRevA, m1.flags(f), f);
							HgFileRevision frSecond= new HgFileRevision(repo, fileRevB, m2.flags(f), f);
							resolver.presentState(f, frFirst, frSecond, frBase);
							mediator.resolve(frBase, frFirst, frSecond, resolver);
						}
					} else {
						// m2 doesn't contain the file, either new in m1, or deleted in m2
						HgFileRevision frFirst = new HgFileRevision(repo, m1.nodeid(f), m1.flags(f), f);
						if (ma.contains(f)) {
							// deleted in m2
							HgFileRevision frBase = new HgFileRevision(repo, ma.nodeid(f), ma.flags(f), f);
							resolver.presentState(f, frFirst, null, frBase);
							mediator.onlyA(frBase, frFirst, resolver);
						} else {
							// new in m1
							resolver.presentState(f, frFirst, null, null);
							mediator.newInA(frFirst, resolver);
						}
					}
					resolver.apply();
				} // for m1 files
				for (Path f : m2.files()) {
					if (m1.contains(f)) {
						continue;
					}
					HgFileRevision frSecond= new HgFileRevision(repo, m2.nodeid(f), m2.flags(f), f);
					// file in m2 is either new or deleted in m1
					if (ma.contains(f)) {
						// deleted in m1
						HgFileRevision frBase = new HgFileRevision(repo, ma.nodeid(f), ma.flags(f), f);
						resolver.presentState(f, null, frSecond, frBase);
						mediator.onlyB(frBase, frSecond, resolver);
					} else {
						// new in m2
						resolver.presentState(f, null, frSecond, null);
						mediator.newInB(frSecond, resolver);
					}
					resolver.apply();
				}
				resolver.serializeChanged(transaction);
				transaction.commit();
			} catch (HgRuntimeException ex) {
				transaction.rollback();
				mergeStateBuilder.abandon();
				throw ex;
			} catch (HgIOException ex) {
				transaction.rollback();
				mergeStateBuilder.abandon();
				throw ex;
			}
		} catch (HgRuntimeException ex) {
			throw new HgLibraryFailureException(ex);
		} finally {
			wdLock.release();
		}
	}

	private void initHeadsAndAncestor(int csetIndexB) throws HgBadArgumentException {
		firstCset = secondCset = ancestorCset = BAD_REVISION;
		if (csetIndexB == HgRepository.BAD_REVISION) {
			throw new HgBadArgumentException("Need valid second head for merge", null);
		}
		// TODO cache/share parent-child map, e.g. right in HgChangelog?! #getOrCreate
		HgParentChildMap<HgChangelog> pmap = new HgParentChildMap<HgChangelog>(repo.getChangelog());
		pmap.init();
		final HgRevisionMap<HgChangelog> rmap = pmap.getRevisionMap();
		final Nodeid csetA = repo.getWorkingCopyParents().first();
		final Nodeid csetB = rmap.revision(csetIndexB);
		final Nodeid ancestor = pmap.ancestor(csetA, csetB);
		assert !ancestor.isNull();
		if (ancestor.equals(csetA) || ancestor.equals(csetB)) {
			throw new HgBadArgumentException(String.format("Revisions %s and %s are on the same line of descent, use update instead of merge", csetA.shortNotation(), csetB.shortNotation()), null);
		}
		firstCset = rmap.revisionIndex(csetA);
		secondCset = csetIndexB;
		ancestorCset = rmap.revisionIndex(ancestor);
	}

	/**
	 * This is the way client code takes part in the merge process. 
	 * It's advised to subclass {@link MediatorBase} unless special treatment for regular cases is desired
	 */
	@Experimental(reason="Provisional API. Work in progress")
	@Callback
	public interface Mediator {
		/**
		 * file revisions are identical in both heads
		 */
		public void same(HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException;
		/**
		 * file left in first/left/A trunk only, deleted in second/right/B trunk
		 */
		public void onlyA(HgFileRevision base, HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException;
		/**
		 * file left in second/right/B trunk only, deleted in first/left/A trunk
		 */
		public void onlyB(HgFileRevision base, HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException;
		/**
		 * file is missing in ancestor revision and second/right/B trunk, introduced in first/left/A trunk
		 */
		public void newInA(HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException;
		/**
		 * file is missing in ancestor revision and first/left/A trunk, introduced in second/right/B trunk
		 */
		public void newInB(HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException;
		/**
		 * file was changed in first/left/A trunk, unchanged in second/right/B trunk
		 */
		public void fastForwardA(HgFileRevision base, HgFileRevision first, Resolver resolver) throws HgCallbackTargetException;
		/**
		 * file was changed in second/right/B trunk, unchanged in first/left/A trunk 
		 */
		public void fastForwardB(HgFileRevision base, HgFileRevision second, Resolver resolver) throws HgCallbackTargetException;
		/**
		 * File changed (or added, if base is <code>null</code>) in both trunks 
		 */
		public void resolve(HgFileRevision base, HgFileRevision first, HgFileRevision second, Resolver resolver) throws HgCallbackTargetException;
	}

	/**
	 * Clients shall not implement this interface.
	 * They use this API from inside {@link Mediator#resolve(HgFileRevision, HgFileRevision, HgFileRevision, Resolver)}
	 */
	@Experimental(reason="Provisional API. Work in progress")
	public interface Resolver {
		public void use(HgFileRevision rev);
		/**
		 * Replace current revision with stream content.
		 * Note, callers are not expected to {@link InputStream#close()} this stream. 
		 * It will be {@link InputStream#close() closed} at <b>Hg4J</b>'s discretion
		 * not necessarily during invocation of this method. IOW, the library may decide to 
		 * use this stream not right away, at some point of time later, and streams supplied
		 * shall respect this.
		 * 
		 * @param content New content to replace current revision, shall not be <code>null</code> 
		 * @throws IOException propagated exceptions from content
		 */
		public void use(InputStream content) throws IOException;
		/**
		 * Do not use this file for resolution. Marks the file for deletion, if appropriate.
		 */
		public void forget(HgFileRevision rev);
		/**
		 * Record the file for later processing by 'hg resolve'. It's required
		 * that processed file present in both trunks. We need two file revisions
		 * to put an entry into merge/state file.
		 * 
		 * XXX Perhaps, shall take two HgFileRevision arguments to facilitate
		 * extra control over what goes into merge/state and to ensure this method
		 * is not invoked when there are no conflicting revisions. 
		 */
		public void unresolved();
	}

	/**
	 * Base mediator implementation, with regular resolution (and "don't delete anything" approach in mind). 
	 * Subclasses shall override methods to provide alternative implementation or to add extra logic (e.g. ask user).
	 */
	@Experimental(reason="Provisional API. Work in progress")
	public static class MediatorBase implements Mediator {
		/**
		 * Implementation keeps this revision
		 */
		public void same(HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException {
			resolver.use(rev);
		}
		/**
		 * Implementation keeps file revision from first/left/A trunk.
		 * Subclasses may opt to {@link Resolver#forget(HgFileRevision) delete} it as it's done in second/right/B trunk.
		 */
		public void onlyA(HgFileRevision base, HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException {
			resolver.use(rev);
		}
		/**
		 * Implementation restores file from second/right/B trunk. 
		 * Subclasses may ask user to decide if it's necessary to do that 
		 */
		public void onlyB(HgFileRevision base, HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException {
			resolver.use(rev);
		}
		/**
		 * Implementation keeps this revision
		 */
		public void newInA(HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException {
			resolver.use(rev);
		}
		/**
		 * Implementation adds this revision. Subclasses my let user decide if it's necessary to add the file
		 */
		public void newInB(HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException {
			resolver.use(rev);
		}
		/**
		 * Implementation keeps latest revision
		 */
		public void fastForwardA(HgFileRevision base, HgFileRevision first, Resolver resolver) throws HgCallbackTargetException {
			resolver.use(first);
		}
		/**
		 * Implementation keeps latest revision
		 */
		public void fastForwardB(HgFileRevision base, HgFileRevision second, Resolver resolver) throws HgCallbackTargetException {
			resolver.use(second);
		}

		/**
		 * Implementation marks file as unresolved
		 */
		public void resolve(HgFileRevision base, HgFileRevision first, HgFileRevision second, Resolver resolver) throws HgCallbackTargetException {
			resolver.unresolved();
		}
	}

	private static class ResolverImpl implements Resolver {
		
		private final Internals repo;
		private final DirstateBuilder dirstateBuilder;
		private final MergeStateBuilder mergeStateBuilder;
		private boolean changedDirstate;
		private HgFileRevision revA;
		private HgFileRevision revB;
		private HgFileRevision revBase;
		private Path file;
		// resolutions:
		private HgFileRevision resolveUse, resolveForget;
		private File resolveContent;
		private boolean resolveMarkUnresolved;
		
		public ResolverImpl(Internals implRepo, DirstateBuilder dirstateBuilder, MergeStateBuilder mergeStateBuilder) {
			repo = implRepo;
			this.dirstateBuilder = dirstateBuilder;
			this.mergeStateBuilder = mergeStateBuilder;
			changedDirstate = false;
		}
		
		void serializeChanged(Transaction tr) throws HgIOException {
			if (changedDirstate) {
				dirstateBuilder.serialize(tr);
			}
			mergeStateBuilder.serialize();
		}

		void presentState(Path p, HgFileRevision revA, HgFileRevision revB, HgFileRevision base) {
			assert revA != null || revB != null;
			file = p;
			this.revA = revA;
			this.revB = revB;
			revBase = base;
			resolveUse = resolveForget = null;
			resolveContent = null;
			resolveMarkUnresolved = false;
		}

		void apply() throws HgIOException, HgRuntimeException {
			if (resolveMarkUnresolved) {
				HgFileRevision c = revBase;
				if (revBase == null) {
					// fake revision, null parent
					c = new HgFileRevision(repo.getRepo(), Nodeid.NULL, HgManifest.Flags.RegularFile, file);
				}
				mergeStateBuilder.unresolved(file, revA, revB, c, revA.getFileFlags());
				changedDirstate = true;
				dirstateBuilder.recordMergedExisting(file, revA.getPath());
			} else if (resolveForget != null) {
				// it revision to forget comes from second/B trunk, shall record it as removed
				// only when corresponding file in first/A trunk is missing (merge:_forgetremoved())
				if (resolveForget == revA || (resolveForget == revB && revA == null)) {
					changedDirstate = true;
					dirstateBuilder.recordRemoved(file);
				}
			} else if (resolveUse != null) {
				if (resolveUse != revA) {
					changedDirstate = true;
					final WorkingDirFileWriter fw = new WorkingDirFileWriter(repo);
					fw.processFile(resolveUse);
					if (resolveUse == revB) {
						dirstateBuilder.recordMergedFromP2(file);
					} else {
						dirstateBuilder.recordMerged(file, fw.fmode(), fw.mtime(), fw.bytesWritten());
					}
				} // if resolution is to use revA, nothing to do
			} else if (resolveContent != null) {
				changedDirstate = true;
				// FIXME write content to file using transaction?
				InputStream is;
				try {
					is = new FileInputStream(resolveContent);
				} catch (IOException ex) {
					throw new HgIOException("Failed to read temporary content", ex, resolveContent);
				}
				final WorkingDirFileWriter fw = new WorkingDirFileWriter(repo);
				fw.processFile(file, is, revA == null ? revB.getFileFlags() : revA.getFileFlags());
				// XXX if presentState(null, fileOnlyInB), and use(InputStream) - i.e.
				// resolution is to add file with supplied content - shall I put 'Merged', MergedFromP2 or 'Added' into dirstate?
				if (revA == null && revB != null) {
					dirstateBuilder.recordMergedFromP2(file);
				} else {
					dirstateBuilder.recordMerged(file, fw.fmode(), fw.mtime(), fw.bytesWritten());
				}
			} // else no resolution was chosen, fine with that
		}

		public void use(HgFileRevision rev) {
			if (rev == null) {
				throw new IllegalArgumentException();
			}
			assert resolveContent == null;
			assert resolveForget == null;
			resolveUse = rev;
		}

		public void use(InputStream content) throws IOException {
			if (content == null) {
				throw new IllegalArgumentException();
			}
			assert resolveUse == null;
			assert resolveForget == null;
			try {
				resolveContent = FileUtils.createTempFile();
				new FileUtils(repo.getLog(), this).write(content, resolveContent);
			} finally {
				content.close();
			}
			// do not care deleting file in case of failure to allow analyze of the issue
		}

		public void forget(HgFileRevision rev) {
			if (rev == null) {
				throw new IllegalArgumentException();
			}
			if (rev != revA || rev != revB) {
				throw new IllegalArgumentException("Can't forget revision which doesn't represent actual state in either merged trunk");
			}
			assert resolveUse == null;
			assert resolveContent == null;
			resolveForget = rev;
		}

		public void unresolved() {
			if (revA == null || revB == null) {
				throw new UnsupportedOperationException("To mark conflict as unresolved need two revisions");
			}
			resolveMarkUnresolved = true;
		}
	}
}