Mercurial > hg4j
changeset 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 |
files | cmdline/org/tmatesoft/hg/console/Merge.java src/org/tmatesoft/hg/core/HgAddRemoveCommand.java src/org/tmatesoft/hg/core/HgCheckoutCommand.java src/org/tmatesoft/hg/core/HgCommitCommand.java src/org/tmatesoft/hg/core/HgMergeCommand.java src/org/tmatesoft/hg/core/HgPullCommand.java src/org/tmatesoft/hg/core/HgRevertCommand.java src/org/tmatesoft/hg/internal/CommitFacility.java src/org/tmatesoft/hg/internal/DirstateBuilder.java src/org/tmatesoft/hg/internal/FileUtils.java src/org/tmatesoft/hg/internal/Internals.java src/org/tmatesoft/hg/internal/MergeStateBuilder.java src/org/tmatesoft/hg/internal/WorkingDirFileWriter.java src/org/tmatesoft/hg/repo/HgMergeState.java src/org/tmatesoft/hg/util/Path.java |
diffstat | 15 files changed, 478 insertions(+), 107 deletions(-) [+] |
line wrap: on
line diff
--- a/cmdline/org/tmatesoft/hg/console/Merge.java Wed Aug 14 20:07:26 2013 +0200 +++ b/cmdline/org/tmatesoft/hg/console/Merge.java Thu Aug 15 18:43:50 2013 +0200 @@ -56,8 +56,8 @@ static class Dump implements HgMergeCommand.Mediator { - public void same(HgFileRevision first, HgFileRevision second, Resolver resolver) throws HgCallbackTargetException { - System.out.printf("Unchanged %s:%s", first.getPath(), first.getRevision().shortNotation()); + public void same(HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException { + System.out.printf("Unchanged %s:%s", rev.getPath(), rev.getRevision().shortNotation()); } public void onlyA(HgFileRevision base, HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException {
--- a/src/org/tmatesoft/hg/core/HgAddRemoveCommand.java Wed Aug 14 20:07:26 2013 +0200 +++ b/src/org/tmatesoft/hg/core/HgAddRemoveCommand.java Thu Aug 15 18:43:50 2013 +0200 @@ -18,7 +18,6 @@ import java.util.LinkedHashSet; -import org.tmatesoft.hg.internal.COWTransaction; import org.tmatesoft.hg.internal.DirstateBuilder; import org.tmatesoft.hg.internal.DirstateReader; import org.tmatesoft.hg.internal.Internals; @@ -123,7 +122,7 @@ progress.worked(1); cancellation.checkCancelled(); } - Transaction.Factory trFactory = new COWTransaction.Factory(); + Transaction.Factory trFactory = implRepo.getTransactionFactory(); Transaction tr = trFactory.create(repo); try { dirstateBuilder.serialize(tr);
--- a/src/org/tmatesoft/hg/core/HgCheckoutCommand.java Wed Aug 14 20:07:26 2013 +0200 +++ b/src/org/tmatesoft/hg/core/HgCheckoutCommand.java Thu Aug 15 18:43:50 2013 +0200 @@ -231,8 +231,8 @@ lastFileMode = workingDirWriter.fmode(); lastFileModificationTime = workingDirWriter.mtime(); return true; - } catch (IOException ex) { - failure = new HgIOException("Failed to write down file revision", ex, workingDirWriter.getDestinationFile()); + } catch (HgIOException ex) { + failure = ex; } catch (HgRuntimeException ex) { failure = new HgLibraryFailureException(ex); }
--- a/src/org/tmatesoft/hg/core/HgCommitCommand.java Wed Aug 14 20:07:26 2013 +0200 +++ b/src/org/tmatesoft/hg/core/HgCommitCommand.java Thu Aug 15 18:43:50 2013 +0200 @@ -20,7 +20,6 @@ import java.io.IOException; -import org.tmatesoft.hg.internal.COWTransaction; import org.tmatesoft.hg.internal.CommitFacility; import org.tmatesoft.hg.internal.CompleteRepoLock; import org.tmatesoft.hg.internal.FileContentSupplier; @@ -112,7 +111,8 @@ newRevision = Nodeid.NULL; return new Outcome(Kind.Failure, "nothing to add"); } - CommitFacility cf = new CommitFacility(Internals.getInstance(repo), parentRevs[0], parentRevs[1]); + final Internals implRepo = Internals.getInstance(repo); + CommitFacility cf = new CommitFacility(implRepo, parentRevs[0], parentRevs[1]); for (Path m : status.getModified()) { HgDataFile df = repo.getFileNode(m); cf.add(df, new WorkingCopyContent(df)); @@ -131,7 +131,7 @@ } cf.branch(detectBranch()); cf.user(detectUser()); - Transaction.Factory trFactory = new COWTransaction.Factory(); + Transaction.Factory trFactory = implRepo.getTransactionFactory(); Transaction tr = trFactory.create(repo); try { newRevision = cf.commit(message, tr);
--- 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; } } }
--- a/src/org/tmatesoft/hg/core/HgPullCommand.java Wed Aug 14 20:07:26 2013 +0200 +++ b/src/org/tmatesoft/hg/core/HgPullCommand.java Thu Aug 15 18:43:50 2013 +0200 @@ -21,7 +21,6 @@ import java.util.List; import org.tmatesoft.hg.internal.AddRevInspector; -import org.tmatesoft.hg.internal.COWTransaction; import org.tmatesoft.hg.internal.Internals; import org.tmatesoft.hg.internal.PhasesHelper; import org.tmatesoft.hg.internal.RepositoryComparator; @@ -78,7 +77,7 @@ // add revisions to changelog, manifest, files final Internals implRepo = HgInternals.getImplementationRepo(repo); final AddRevInspector insp; - Transaction.Factory trFactory = new COWTransaction.Factory(); + Transaction.Factory trFactory = implRepo.getTransactionFactory(); Transaction tr = trFactory.create(repo); try { incoming.inspectAll(insp = new AddRevInspector(implRepo, tr));
--- a/src/org/tmatesoft/hg/core/HgRevertCommand.java Wed Aug 14 20:07:26 2013 +0200 +++ b/src/org/tmatesoft/hg/core/HgRevertCommand.java Thu Aug 15 18:43:50 2013 +0200 @@ -21,7 +21,6 @@ import java.util.LinkedHashSet; import java.util.Set; -import org.tmatesoft.hg.internal.COWTransaction; import org.tmatesoft.hg.internal.CsetParamKeeper; import org.tmatesoft.hg.internal.DirstateBuilder; import org.tmatesoft.hg.internal.DirstateReader; @@ -160,7 +159,7 @@ progress.worked(1); cancellation.checkCancelled(); } - Transaction.Factory trFactory = new COWTransaction.Factory(); + Transaction.Factory trFactory = implRepo.getTransactionFactory(); Transaction tr = trFactory.create(repo); try { // TODO same code in HgAddRemoveCommand and similar in HgCommitCommand
--- a/src/org/tmatesoft/hg/internal/CommitFacility.java Wed Aug 14 20:07:26 2013 +0200 +++ b/src/org/tmatesoft/hg/internal/CommitFacility.java Thu Aug 15 18:43:50 2013 +0200 @@ -212,7 +212,7 @@ } // bring dirstate up to commit state, TODO share this code with HgAddRemoveCommand final DirstateBuilder dirstateBuilder = new DirstateBuilder(repo); - dirstateBuilder.fillFrom(new DirstateReader(repo, new Path.SimpleSource())); + dirstateBuilder.fillFrom(new DirstateReader(repo, repo.getSessionContext().getPathFactory())); for (Path p : removals) { dirstateBuilder.recordRemoved(p); }
--- a/src/org/tmatesoft/hg/internal/DirstateBuilder.java Wed Aug 14 20:07:26 2013 +0200 +++ b/src/org/tmatesoft/hg/internal/DirstateBuilder.java Thu Aug 15 18:43:50 2013 +0200 @@ -92,6 +92,22 @@ removed.put(fname, n); } + public void recordMerged(Path fname, int fmode, int mtime, int bytesWritten) { + forget(fname); + merged.put(fname, new HgDirstate.Record(fmode, bytesWritten,mtime, fname, null)); + } + + /** + * From DirState wiki: + * <p>"size is ... when the dirstate is in a merge state: -2 will *always* return dirty, it is used to mark a file that was cleanly picked from p2" + * and + * <p>"Additional meta status...'np2': merged from other parent (status == 'n', size == -2)" + */ + public void recordMergedFromP2(Path fname) { + forget(fname); + normal.put(fname, new HgDirstate.Record(0, -2, -1, fname, null)); + } + private HgDirstate.Record forget(Path fname) { HgDirstate.Record r; if ((r = normal.remove(fname)) != null) {
--- a/src/org/tmatesoft/hg/internal/FileUtils.java Wed Aug 14 20:07:26 2013 +0200 +++ b/src/org/tmatesoft/hg/internal/FileUtils.java Thu Aug 15 18:43:50 2013 +0200 @@ -105,7 +105,7 @@ fos.flush(); fos.close(); } - + public void closeQuietly(Closeable stream) { closeQuietly(stream, null); } @@ -126,4 +126,9 @@ } } } + + // nothing special, just a single place with common prefix + public File createTempFile() throws IOException { + return File.createTempFile("hg4j-", null); + } }
--- a/src/org/tmatesoft/hg/internal/Internals.java Wed Aug 14 20:07:26 2013 +0200 +++ b/src/org/tmatesoft/hg/internal/Internals.java Thu Aug 15 18:43:50 2013 +0200 @@ -513,6 +513,10 @@ public RevlogStream resolveStoreFile(Path path) { return streamProvider.getStoreFile(path, false); } + + public Transaction.Factory getTransactionFactory() { + return new COWTransaction.Factory(); + } // marker method public static IllegalStateException notImplemented() {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/MergeStateBuilder.java Thu Aug 15 18:43:50 2013 +0200 @@ -0,0 +1,48 @@ +/* + * 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.internal; + +import org.tmatesoft.hg.core.HgIOException; +import org.tmatesoft.hg.repo.HgMergeState; +import org.tmatesoft.hg.util.Path; + +/** + * Constructs merge/state file + * + * @see HgMergeState + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class MergeStateBuilder { + + private final Internals repo; + + public MergeStateBuilder(Internals implRepo) { + repo = implRepo; + } + + public void resolved() { + throw Internals.notImplemented(); + } + + public void unresolved(Path file) { + throw Internals.notImplemented(); + } + + public void serialize(Transaction tr) throws HgIOException { + } +}
--- a/src/org/tmatesoft/hg/internal/WorkingDirFileWriter.java Wed Aug 14 20:07:26 2013 +0200 +++ b/src/org/tmatesoft/hg/internal/WorkingDirFileWriter.java Thu Aug 15 18:43:50 2013 +0200 @@ -21,9 +21,12 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; +import org.tmatesoft.hg.core.HgFileRevision; +import org.tmatesoft.hg.core.HgIOException; import org.tmatesoft.hg.repo.HgDataFile; import org.tmatesoft.hg.repo.HgManifest; import org.tmatesoft.hg.repo.HgRuntimeException; @@ -65,45 +68,94 @@ * Executable bit is set if specified and filesystem supports it. * @throws HgRuntimeException */ - public void processFile(HgDataFile df, int fileRevIndex, HgManifest.Flags flags) throws IOException, HgRuntimeException { - try { - prepare(df.getPath()); - if (flags != HgManifest.Flags.Link) { - destChannel = new FileOutputStream(dest).getChannel(); - } else { - linkChannel = new ByteArrayChannel(); - } - df.contentWithFilters(fileRevIndex, this); - } catch (CancelledException ex) { - hgRepo.getSessionContext().getLog().dump(getClass(), Severity.Error, ex, "Our impl doesn't throw cancellation"); - } finally { - if (flags != HgManifest.Flags.Link) { - destChannel.close(); - destChannel = null; - // leave dest in case anyone enquires with #getDestinationFile + public void processFile(final HgDataFile df, final int fileRevIndex, HgManifest.Flags flags) throws HgIOException, HgRuntimeException { + processFile(df.getPath(), new Fetch() { + public void readInto(ByteChannel ch) { + try { + df.contentWithFilters(fileRevIndex, ch); + } catch (CancelledException ex) { + handleUnexpectedCancel(ex); + } } - } - if (linkChannel != null && symlinkCap) { - assert flags == HgManifest.Flags.Link; - fileFlagsHelper.createSymlink(dest.getParentFile(), dest.getName(), linkChannel.toArray()); - } else if (flags == HgManifest.Flags.Exec && execCap) { - fileFlagsHelper.setExecutableBit(dest.getParentFile(), dest.getName()); - } - // Although HgWCStatusCollector treats 644 (`hg manifest -v`) and 664 (my fs) the same, it's better - // to detect actual flags here - fmode = flags.fsMode(); // default to one from manifest - if (fileFlagsHelper != null) { - // if neither execBit nor link is supported by fs, it's unlikely file mode is supported, too. + }, flags); + } + + public void processFile(final HgFileRevision fr) throws HgIOException, HgRuntimeException { + processFile(fr.getPath(), new Fetch() { + + public void readInto(ByteChannel ch) throws IOException, HgRuntimeException { + try { + fr.putContentTo(ch); + } catch (CancelledException ex) { + handleUnexpectedCancel(ex); + } + } + }, fr.getFileFlags()); + } + + /** + * Closes supplied content stream + */ + public void processFile(Path fname, final InputStream content, HgManifest.Flags flags) throws HgIOException, HgRuntimeException { + processFile(fname, new Fetch() { + + public void readInto(ByteChannel ch) throws IOException, HgRuntimeException { + try { + ByteBuffer bb = ByteBuffer.wrap(new byte[8*1024]); + int r; + while ((r = content.read(bb.array())) != -1) { + bb.position(0).limit(r); + for (int wrote = 0; wrote < r; ) { + r -= wrote; + wrote = ch.write(bb); + assert bb.remaining() == r - wrote; + } + } + } catch (CancelledException ex) { + handleUnexpectedCancel(ex); + } + } + }, flags); + } + + private interface Fetch { + void readInto(ByteChannel ch) throws IOException, HgRuntimeException; + } + + private void processFile(Path fname, Fetch fetch, HgManifest.Flags flags) throws HgIOException, HgRuntimeException { + try { + byte[] symlinkContent = null; try { - fmode = fileFlagsHelper.getFileMode(dest, fmode); - } catch (IOException ex) { - // Warn, we've got default value and can live with it - hgRepo.getSessionContext().getLog().dump(getClass(), Warn, ex, "Failed get file access rights"); + prepare(fname, flags); + fetch.readInto(this); + } finally { + symlinkContent = close(fname, flags); } + if (flags == HgManifest.Flags.Link && symlinkCap) { + assert symlinkContent != null; + fileFlagsHelper.createSymlink(dest.getParentFile(), dest.getName(), symlinkContent); + } else if (flags == HgManifest.Flags.Exec && execCap) { + fileFlagsHelper.setExecutableBit(dest.getParentFile(), dest.getName()); + } + // Although HgWCStatusCollector treats 644 (`hg manifest -v`) and 664 (my fs) the same, it's better + // to detect actual flags here + fmode = flags.fsMode(); // default to one from manifest + if (fileFlagsHelper != null) { + // if neither execBit nor link is supported by fs, it's unlikely file mode is supported, too. + try { + fmode = fileFlagsHelper.getFileMode(dest, fmode); + } catch (IOException ex) { + // Warn, we've got default value and can live with it + hgRepo.getSessionContext().getLog().dump(getClass(), Warn, ex, "Failed get file access rights"); + } + } + } catch (IOException ex) { + String msg = String.format("Failed to write file %s to the working directory", fname); + throw new HgIOException(msg, ex, dest); } } - public void prepare(Path fname) throws IOException { + private void prepare(Path fname, HgManifest.Flags flags) throws IOException { String fpath = fname.toString(); dest = new File(hgRepo.getRepo().getWorkingDir(), fpath); if (fpath.indexOf('/') != -1) { @@ -113,6 +165,25 @@ linkChannel = null; totalBytesWritten = 0; fmode = 0; + if (flags != HgManifest.Flags.Link) { + destChannel = new FileOutputStream(dest).getChannel(); + } else { + linkChannel = new ByteArrayChannel(); + } + } + + private byte[] close(Path fname, HgManifest.Flags flags) throws IOException { + if (flags != HgManifest.Flags.Link) { + destChannel.close(); + destChannel = null; + // leave dest in case anyone enquires with #getDestinationFile + } + if (linkChannel != null) { + final byte[] rv = linkChannel.toArray(); + linkChannel = null; + return rv; + } + return null; } public int write(ByteBuffer buffer) throws IOException, CancelledException { @@ -144,4 +215,8 @@ public int mtime() { return (int) (dest.lastModified() / 1000); } + + private void handleUnexpectedCancel(CancelledException ex) { + hgRepo.getSessionContext().getLog().dump(WorkingDirFileWriter.class, Severity.Error, ex, "Our impl doesn't throw cancellation"); + } }
--- a/src/org/tmatesoft/hg/repo/HgMergeState.java Wed Aug 14 20:07:26 2013 +0200 +++ b/src/org/tmatesoft/hg/repo/HgMergeState.java Thu Aug 15 18:43:50 2013 +0200 @@ -218,7 +218,7 @@ /** * List of conflicts as recorded in the merge state information file. - * Note, this information is valid unless {@link #isStale()} is <code>true</code>. + * Note, this information is not valid unless {@link #isStale()} is <code>true</code>. * * @return non-<code>null</code> list with both resolved and unresolved conflicts. */
--- a/src/org/tmatesoft/hg/util/Path.java Wed Aug 14 20:07:26 2013 +0200 +++ b/src/org/tmatesoft/hg/util/Path.java Thu Aug 15 18:43:50 2013 +0200 @@ -214,6 +214,7 @@ public static class SimpleSource implements Source { private final PathRewrite normalizer; private final Convertor<Path> convertor; + private final Path.Source delegate; public SimpleSource() { this(new PathRewrite.Empty(), null); @@ -224,12 +225,30 @@ } public SimpleSource(PathRewrite pathRewrite, Convertor<Path> pathConvertor) { + assert pathRewrite != null; normalizer = pathRewrite; convertor = pathConvertor; + delegate = null; + } + + public SimpleSource(Path.Source actual, Convertor<Path> pathConvertor) { + assert actual != null; + normalizer = null; + delegate = actual; + convertor = pathConvertor; } public Path path(CharSequence p) { - Path rv = Path.create(normalizer.rewrite(p)); + // in fact, it's nicer to have sequence of sources, and a bunch of small + // Source implementations each responsible for specific aspect, like Convertor + // or delegation to another Source. However, these classes are just too small + // to justify their existence + Path rv; + if (delegate != null) { + rv = delegate.path(p); + } else { + rv = Path.create(normalizer.rewrite(p)); + } if (convertor != null) { return convertor.mangle(rv); }