diff src/org/tmatesoft/hg/core/HgMergeCommand.java @ 705:b4242b7e7dfe

Merge command: implement conflict resolution alternatives
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Thu, 15 Aug 2013 18:43:50 +0200
parents 7743a9c10bfa
children cd5c87d96315
line wrap: on
line diff
--- a/src/org/tmatesoft/hg/core/HgMergeCommand.java	Wed Aug 14 20:07:26 2013 +0200
+++ b/src/org/tmatesoft/hg/core/HgMergeCommand.java	Thu Aug 15 18:43:50 2013 +0200
@@ -18,13 +18,23 @@
 
 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.HgParentChildMap;
 import org.tmatesoft.hg.repo.HgRepository;
@@ -62,7 +72,7 @@
 		return this;
 	}
 
-	public void execute(Mediator mediator) throws HgCallbackTargetException, HgRepositoryLockException, HgLibraryFailureException, CancelledException {
+	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");
 		}
@@ -71,65 +81,91 @@
 		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();
+			dirstateBuilder.parents(clog.getRevision(firstCset), clog.getRevision(secondCset));
+			//
+			MergeStateBuilder mergeStateBuilder = new MergeStateBuilder(implRepo);
+
 			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);
-			ResolverImpl resolver = new ResolverImpl();
-			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);
-						mediator.same(fr, 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);
-						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);
-						mediator.fastForwardA(frBase, frFirst, resolver);
+			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);
+							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);
+							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);
+							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);
+							mediator.resolve(frBase, frFirst, frSecond, 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);
-						mediator.resolve(frBase, frFirst, frSecond, resolver);
+						// 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);
+						resolver.presentState(f, frFirst, null);
+						if (ma.contains(f)) {
+							// deleted in m2
+							HgFileRevision frBase = new HgFileRevision(repo, ma.nodeid(f), ma.flags(f), f);
+							mediator.onlyA(frBase, frFirst, resolver);
+						} else {
+							// new in m1
+							mediator.newInA(frFirst, 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);
+					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
+					resolver.presentState(f, null, frSecond);
 					if (ma.contains(f)) {
-						// deleted in m2
+						// deleted in m1
 						HgFileRevision frBase = new HgFileRevision(repo, ma.nodeid(f), ma.flags(f), f);
-						mediator.onlyA(frBase, frFirst, resolver);
+						mediator.onlyB(frBase, frSecond, resolver);
 					} else {
-						// new in m1
-						mediator.newInA(frFirst, resolver);
+						// new in m2
+						mediator.newInB(frSecond, resolver);
 					}
+					resolver.apply();
 				}
-				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);
-					mediator.onlyB(frBase, frSecond, resolver);
-				} else {
-					// new in m2
-					mediator.newInB(frSecond, resolver);
-				}
-				resolver.apply();
+				resolver.serializeChanged(transaction);
+				transaction.commit();
+			} catch (HgRuntimeException ex) {
+				transaction.rollback();
+				throw ex;
+			} catch (HgIOException ex) {
+				transaction.rollback();
+				throw ex;
 			}
 		} catch (HgRuntimeException ex) {
 			throw new HgLibraryFailureException(ex);
@@ -160,18 +196,43 @@
 	}
 
 	/**
-	 * This is the way client code takes part in the merge process
+	 * 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 {
-		public void same(HgFileRevision first, HgFileRevision second, Resolver resolver) throws HgCallbackTargetException;
+		/**
+		 * 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;
 	}
 
@@ -182,24 +243,170 @@
 	@Experimental(reason="Provisional API. Work in progress")
 	public interface Resolver {
 		public void use(HgFileRevision rev);
-		public void use(InputStream content);
+		/**
+		 * 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;
+		public void forget(HgFileRevision rev);
 		public void unresolved(); // record the file for later processing by 'hg resolve'
 	}
 
+	/**
+	 * Base mediator implementation, with regular resolution
+	 */
+	@Experimental(reason="Provisional API. Work in progress")
+	public abstract class MediatorBase implements Mediator {
+		public void same(HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException {
+			resolver.use(rev);
+		}
+		public void onlyA(HgFileRevision base, HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException {
+			resolver.use(rev);
+		}
+		public void onlyB(HgFileRevision base, HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException {
+			resolver.use(rev);
+		}
+		public void newInA(HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException {
+			resolver.use(rev);
+		}
+		public void newInB(HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException {
+			resolver.use(rev);
+		}
+		public void fastForwardA(HgFileRevision base, HgFileRevision first, Resolver resolver) throws HgCallbackTargetException {
+			resolver.use(first);
+		}
+		public void fastForwardB(HgFileRevision base, HgFileRevision second, Resolver resolver) throws HgCallbackTargetException {
+			resolver.use(second);
+		}
+	}
+
 	private static class ResolverImpl implements Resolver {
-		void apply() {
+		
+		private final Internals repo;
+		private final DirstateBuilder dirstateBuilder;
+		private final MergeStateBuilder mergeStateBuilder;
+		private boolean changedDirstate;
+		private HgFileRevision revA;
+		private HgFileRevision revB;
+		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(tr);
+		}
+
+		void presentState(Path p, HgFileRevision revA, HgFileRevision revB) {
+			assert revA != null || revB != null;
+			file = p;
+			this.revA = revA;
+			this.revB = revB;
+			resolveUse = resolveForget = null;
+			resolveContent = null;
+			resolveMarkUnresolved = false;
+		}
+
+		void apply() throws HgIOException, HgRuntimeException {
+			if (resolveMarkUnresolved) {
+				mergeStateBuilder.unresolved(file);
+			} else if (resolveForget != null) {
+				if (resolveForget == revA) {
+					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 {
+				assert false;
+			}
 		}
 
 		public void use(HgFileRevision rev) {
-			// TODO Auto-generated method stub
+			if (rev == null) {
+				throw new IllegalArgumentException();
+			}
+			assert resolveContent == null;
+			assert resolveForget == null;
+			resolveUse = rev;
 		}
 
-		public void use(InputStream content) {
-			// TODO Auto-generated method stub
+		public void use(InputStream content) throws IOException {
+			if (content == null) {
+				throw new IllegalArgumentException();
+			}
+			assert resolveUse == null;
+			assert resolveForget == null;
+			try {
+				// cache new contents just to fail fast if there are troubles with content
+				final FileUtils fileUtils = new FileUtils(repo.getLog(), this);
+				resolveContent = fileUtils.createTempFile();
+				fileUtils.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() {
-			// TODO Auto-generated method stub
+			resolveMarkUnresolved = true;
 		}
 	}
 }