Mercurial > jhg
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; } } }