comparison 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
comparison
equal deleted inserted replaced
704:7743a9c10bfa 705:b4242b7e7dfe
16 */ 16 */
17 package org.tmatesoft.hg.core; 17 package org.tmatesoft.hg.core;
18 18
19 import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION; 19 import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION;
20 20
21 import java.io.File;
22 import java.io.FileInputStream;
23 import java.io.IOException;
21 import java.io.InputStream; 24 import java.io.InputStream;
22 25
23 import org.tmatesoft.hg.internal.Callback; 26 import org.tmatesoft.hg.internal.Callback;
24 import org.tmatesoft.hg.internal.CsetParamKeeper; 27 import org.tmatesoft.hg.internal.CsetParamKeeper;
28 import org.tmatesoft.hg.internal.DirstateBuilder;
29 import org.tmatesoft.hg.internal.DirstateReader;
25 import org.tmatesoft.hg.internal.Experimental; 30 import org.tmatesoft.hg.internal.Experimental;
31 import org.tmatesoft.hg.internal.FileUtils;
32 import org.tmatesoft.hg.internal.Internals;
26 import org.tmatesoft.hg.internal.ManifestRevision; 33 import org.tmatesoft.hg.internal.ManifestRevision;
34 import org.tmatesoft.hg.internal.MergeStateBuilder;
27 import org.tmatesoft.hg.internal.Pool; 35 import org.tmatesoft.hg.internal.Pool;
36 import org.tmatesoft.hg.internal.Transaction;
37 import org.tmatesoft.hg.internal.WorkingDirFileWriter;
28 import org.tmatesoft.hg.repo.HgChangelog; 38 import org.tmatesoft.hg.repo.HgChangelog;
29 import org.tmatesoft.hg.repo.HgParentChildMap; 39 import org.tmatesoft.hg.repo.HgParentChildMap;
30 import org.tmatesoft.hg.repo.HgRepository; 40 import org.tmatesoft.hg.repo.HgRepository;
31 import org.tmatesoft.hg.repo.HgRepositoryLock; 41 import org.tmatesoft.hg.repo.HgRepositoryLock;
32 import org.tmatesoft.hg.repo.HgRevisionMap; 42 import org.tmatesoft.hg.repo.HgRevisionMap;
60 public HgMergeCommand changeset(int revisionIndex) throws HgBadArgumentException { 70 public HgMergeCommand changeset(int revisionIndex) throws HgBadArgumentException {
61 initHeadsAndAncestor(new CsetParamKeeper(repo).set(revisionIndex).get()); 71 initHeadsAndAncestor(new CsetParamKeeper(repo).set(revisionIndex).get());
62 return this; 72 return this;
63 } 73 }
64 74
65 public void execute(Mediator mediator) throws HgCallbackTargetException, HgRepositoryLockException, HgLibraryFailureException, CancelledException { 75 public void execute(Mediator mediator) throws HgCallbackTargetException, HgRepositoryLockException, HgIOException, HgLibraryFailureException, CancelledException {
66 if (firstCset == BAD_REVISION || secondCset == BAD_REVISION || ancestorCset == BAD_REVISION) { 76 if (firstCset == BAD_REVISION || secondCset == BAD_REVISION || ancestorCset == BAD_REVISION) {
67 throw new IllegalArgumentException("Merge heads and their ancestors are not initialized"); 77 throw new IllegalArgumentException("Merge heads and their ancestors are not initialized");
68 } 78 }
69 final HgRepositoryLock wdLock = repo.getWorkingDirLock(); 79 final HgRepositoryLock wdLock = repo.getWorkingDirLock();
70 wdLock.acquire(); 80 wdLock.acquire();
71 try { 81 try {
72 Pool<Nodeid> cacheRevs = new Pool<Nodeid>(); 82 Pool<Nodeid> cacheRevs = new Pool<Nodeid>();
73 Pool<Path> cacheFiles = new Pool<Path>(); 83 Pool<Path> cacheFiles = new Pool<Path>();
84
85 Internals implRepo = Internals.getInstance(repo);
86 final DirstateBuilder dirstateBuilder = new DirstateBuilder(implRepo);
87 dirstateBuilder.fillFrom(new DirstateReader(implRepo, new Path.SimpleSource(repo.getSessionContext().getPathFactory(), cacheFiles)));
88 final HgChangelog clog = repo.getChangelog();
89 dirstateBuilder.parents(clog.getRevision(firstCset), clog.getRevision(secondCset));
90 //
91 MergeStateBuilder mergeStateBuilder = new MergeStateBuilder(implRepo);
92
74 ManifestRevision m1, m2, ma; 93 ManifestRevision m1, m2, ma;
75 m1 = new ManifestRevision(cacheRevs, cacheFiles).init(repo, firstCset); 94 m1 = new ManifestRevision(cacheRevs, cacheFiles).init(repo, firstCset);
76 m2 = new ManifestRevision(cacheRevs, cacheFiles).init(repo, secondCset); 95 m2 = new ManifestRevision(cacheRevs, cacheFiles).init(repo, secondCset);
77 ma = new ManifestRevision(cacheRevs, cacheFiles).init(repo, ancestorCset); 96 ma = new ManifestRevision(cacheRevs, cacheFiles).init(repo, ancestorCset);
78 ResolverImpl resolver = new ResolverImpl(); 97 Transaction transaction = implRepo.getTransactionFactory().create(repo);
79 for (Path f : m1.files()) { 98 ResolverImpl resolver = new ResolverImpl(implRepo, dirstateBuilder, mergeStateBuilder);
80 Nodeid fileRevBase, fileRevA, fileRevB; 99 try {
81 if (m2.contains(f)) { 100 for (Path f : m1.files()) {
82 fileRevA = m1.nodeid(f); 101 Nodeid fileRevBase, fileRevA, fileRevB;
83 fileRevB = m2.nodeid(f); 102 if (m2.contains(f)) {
84 fileRevBase = ma.contains(f) ? ma.nodeid(f) : null; 103 fileRevA = m1.nodeid(f);
85 if (fileRevA.equals(fileRevB)) { 104 fileRevB = m2.nodeid(f);
86 HgFileRevision fr = new HgFileRevision(repo, fileRevA, m1.flags(f), f); 105 fileRevBase = ma.contains(f) ? ma.nodeid(f) : null;
87 mediator.same(fr, fr, resolver); 106 if (fileRevA.equals(fileRevB)) {
88 } else if (fileRevBase == fileRevA) { 107 HgFileRevision fr = new HgFileRevision(repo, fileRevA, m1.flags(f), f);
89 assert fileRevBase != null; 108 resolver.presentState(f, fr, fr);
90 HgFileRevision frBase = new HgFileRevision(repo, fileRevBase, ma.flags(f), f); 109 mediator.same(fr, resolver);
91 HgFileRevision frSecond= new HgFileRevision(repo, fileRevB, m2.flags(f), f); 110 } else if (fileRevBase == fileRevA) {
92 mediator.fastForwardB(frBase, frSecond, resolver); 111 assert fileRevBase != null;
93 } else if (fileRevBase == fileRevB) { 112 HgFileRevision frBase = new HgFileRevision(repo, fileRevBase, ma.flags(f), f);
94 assert fileRevBase != null; 113 HgFileRevision frSecond= new HgFileRevision(repo, fileRevB, m2.flags(f), f);
95 HgFileRevision frBase = new HgFileRevision(repo, fileRevBase, ma.flags(f), f); 114 resolver.presentState(f, frBase, frSecond);
96 HgFileRevision frFirst = new HgFileRevision(repo, fileRevA, m1.flags(f), f); 115 mediator.fastForwardB(frBase, frSecond, resolver);
97 mediator.fastForwardA(frBase, frFirst, resolver); 116 } else if (fileRevBase == fileRevB) {
117 assert fileRevBase != null;
118 HgFileRevision frBase = new HgFileRevision(repo, fileRevBase, ma.flags(f), f);
119 HgFileRevision frFirst = new HgFileRevision(repo, fileRevA, m1.flags(f), f);
120 resolver.presentState(f, frFirst, frBase);
121 mediator.fastForwardA(frBase, frFirst, resolver);
122 } else {
123 HgFileRevision frBase = fileRevBase == null ? null : new HgFileRevision(repo, fileRevBase, ma.flags(f), f);
124 HgFileRevision frFirst = new HgFileRevision(repo, fileRevA, m1.flags(f), f);
125 HgFileRevision frSecond= new HgFileRevision(repo, fileRevB, m2.flags(f), f);
126 resolver.presentState(f, frFirst, frSecond);
127 mediator.resolve(frBase, frFirst, frSecond, resolver);
128 }
98 } else { 129 } else {
99 HgFileRevision frBase = fileRevBase == null ? null : new HgFileRevision(repo, fileRevBase, ma.flags(f), f); 130 // m2 doesn't contain the file, either new in m1, or deleted in m2
100 HgFileRevision frFirst = new HgFileRevision(repo, fileRevA, m1.flags(f), f); 131 HgFileRevision frFirst = new HgFileRevision(repo, m1.nodeid(f), m1.flags(f), f);
101 HgFileRevision frSecond= new HgFileRevision(repo, fileRevB, m2.flags(f), f); 132 resolver.presentState(f, frFirst, null);
102 mediator.resolve(frBase, frFirst, frSecond, resolver); 133 if (ma.contains(f)) {
134 // deleted in m2
135 HgFileRevision frBase = new HgFileRevision(repo, ma.nodeid(f), ma.flags(f), f);
136 mediator.onlyA(frBase, frFirst, resolver);
137 } else {
138 // new in m1
139 mediator.newInA(frFirst, resolver);
140 }
103 } 141 }
104 } else { 142 resolver.apply();
105 // m2 doesn't contain the file, either new in m1, or deleted in m2 143 } // for m1 files
106 HgFileRevision frFirst = new HgFileRevision(repo, m1.nodeid(f), m1.flags(f), f); 144 for (Path f : m2.files()) {
145 if (m1.contains(f)) {
146 continue;
147 }
148 HgFileRevision frSecond= new HgFileRevision(repo, m2.nodeid(f), m2.flags(f), f);
149 // file in m2 is either new or deleted in m1
150 resolver.presentState(f, null, frSecond);
107 if (ma.contains(f)) { 151 if (ma.contains(f)) {
108 // deleted in m2 152 // deleted in m1
109 HgFileRevision frBase = new HgFileRevision(repo, ma.nodeid(f), ma.flags(f), f); 153 HgFileRevision frBase = new HgFileRevision(repo, ma.nodeid(f), ma.flags(f), f);
110 mediator.onlyA(frBase, frFirst, resolver); 154 mediator.onlyB(frBase, frSecond, resolver);
111 } else { 155 } else {
112 // new in m1 156 // new in m2
113 mediator.newInA(frFirst, resolver); 157 mediator.newInB(frSecond, resolver);
114 } 158 }
159 resolver.apply();
115 } 160 }
116 resolver.apply(); 161 resolver.serializeChanged(transaction);
117 } // for m1 files 162 transaction.commit();
118 for (Path f : m2.files()) { 163 } catch (HgRuntimeException ex) {
119 if (m1.contains(f)) { 164 transaction.rollback();
120 continue; 165 throw ex;
121 } 166 } catch (HgIOException ex) {
122 HgFileRevision frSecond= new HgFileRevision(repo, m2.nodeid(f), m2.flags(f), f); 167 transaction.rollback();
123 // file in m2 is either new or deleted in m1 168 throw ex;
124 if (ma.contains(f)) {
125 // deleted in m1
126 HgFileRevision frBase = new HgFileRevision(repo, ma.nodeid(f), ma.flags(f), f);
127 mediator.onlyB(frBase, frSecond, resolver);
128 } else {
129 // new in m2
130 mediator.newInB(frSecond, resolver);
131 }
132 resolver.apply();
133 } 169 }
134 } catch (HgRuntimeException ex) { 170 } catch (HgRuntimeException ex) {
135 throw new HgLibraryFailureException(ex); 171 throw new HgLibraryFailureException(ex);
136 } finally { 172 } finally {
137 wdLock.release(); 173 wdLock.release();
158 secondCset = csetIndexB; 194 secondCset = csetIndexB;
159 ancestorCset = rmap.revisionIndex(ancestor); 195 ancestorCset = rmap.revisionIndex(ancestor);
160 } 196 }
161 197
162 /** 198 /**
163 * This is the way client code takes part in the merge process 199 * This is the way client code takes part in the merge process.
200 * It's advised to subclass {@link MediatorBase} unless special treatment for regular cases is desired
164 */ 201 */
165 @Experimental(reason="Provisional API. Work in progress") 202 @Experimental(reason="Provisional API. Work in progress")
166 @Callback 203 @Callback
167 public interface Mediator { 204 public interface Mediator {
168 public void same(HgFileRevision first, HgFileRevision second, Resolver resolver) throws HgCallbackTargetException; 205 /**
206 * file revisions are identical in both heads
207 */
208 public void same(HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException;
209 /**
210 * file left in first/left/A trunk only, deleted in second/right/B trunk
211 */
169 public void onlyA(HgFileRevision base, HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException; 212 public void onlyA(HgFileRevision base, HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException;
213 /**
214 * file left in second/right/B trunk only, deleted in first/left/A trunk
215 */
170 public void onlyB(HgFileRevision base, HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException; 216 public void onlyB(HgFileRevision base, HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException;
217 /**
218 * file is missing in ancestor revision and second/right/B trunk, introduced in first/left/A trunk
219 */
171 public void newInA(HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException; 220 public void newInA(HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException;
221 /**
222 * file is missing in ancestor revision and first/left/A trunk, introduced in second/right/B trunk
223 */
172 public void newInB(HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException; 224 public void newInB(HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException;
225 /**
226 * file was changed in first/left/A trunk, unchanged in second/right/B trunk
227 */
173 public void fastForwardA(HgFileRevision base, HgFileRevision first, Resolver resolver) throws HgCallbackTargetException; 228 public void fastForwardA(HgFileRevision base, HgFileRevision first, Resolver resolver) throws HgCallbackTargetException;
229 /**
230 * file was changed in second/right/B trunk, unchanged in first/left/A trunk
231 */
174 public void fastForwardB(HgFileRevision base, HgFileRevision second, Resolver resolver) throws HgCallbackTargetException; 232 public void fastForwardB(HgFileRevision base, HgFileRevision second, Resolver resolver) throws HgCallbackTargetException;
233 /**
234 * File changed (or added, if base is <code>null</code>) in both trunks
235 */
175 public void resolve(HgFileRevision base, HgFileRevision first, HgFileRevision second, Resolver resolver) throws HgCallbackTargetException; 236 public void resolve(HgFileRevision base, HgFileRevision first, HgFileRevision second, Resolver resolver) throws HgCallbackTargetException;
176 } 237 }
177 238
178 /** 239 /**
179 * Clients shall not implement this interface. 240 * Clients shall not implement this interface.
180 * They use this API from inside {@link Mediator#resolve(HgFileRevision, HgFileRevision, HgFileRevision, Resolver)} 241 * They use this API from inside {@link Mediator#resolve(HgFileRevision, HgFileRevision, HgFileRevision, Resolver)}
181 */ 242 */
182 @Experimental(reason="Provisional API. Work in progress") 243 @Experimental(reason="Provisional API. Work in progress")
183 public interface Resolver { 244 public interface Resolver {
184 public void use(HgFileRevision rev); 245 public void use(HgFileRevision rev);
185 public void use(InputStream content); 246 /**
247 * Replace current revision with stream content.
248 * Note, callers are not expected to {@link InputStream#close()} this stream.
249 * It will be {@link InputStream#close() closed} at <b>Hg4J</b>'s discretion
250 * not necessarily during invocation of this method. IOW, the library may decide to
251 * use this stream not right away, at some point of time later, and streams supplied
252 * shall respect this.
253 *
254 * @param content New content to replace current revision, shall not be <code>null</code>
255 * @throws IOException propagated exceptions from content
256 */
257 public void use(InputStream content) throws IOException;
258 public void forget(HgFileRevision rev);
186 public void unresolved(); // record the file for later processing by 'hg resolve' 259 public void unresolved(); // record the file for later processing by 'hg resolve'
187 } 260 }
188 261
262 /**
263 * Base mediator implementation, with regular resolution
264 */
265 @Experimental(reason="Provisional API. Work in progress")
266 public abstract class MediatorBase implements Mediator {
267 public void same(HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException {
268 resolver.use(rev);
269 }
270 public void onlyA(HgFileRevision base, HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException {
271 resolver.use(rev);
272 }
273 public void onlyB(HgFileRevision base, HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException {
274 resolver.use(rev);
275 }
276 public void newInA(HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException {
277 resolver.use(rev);
278 }
279 public void newInB(HgFileRevision rev, Resolver resolver) throws HgCallbackTargetException {
280 resolver.use(rev);
281 }
282 public void fastForwardA(HgFileRevision base, HgFileRevision first, Resolver resolver) throws HgCallbackTargetException {
283 resolver.use(first);
284 }
285 public void fastForwardB(HgFileRevision base, HgFileRevision second, Resolver resolver) throws HgCallbackTargetException {
286 resolver.use(second);
287 }
288 }
289
189 private static class ResolverImpl implements Resolver { 290 private static class ResolverImpl implements Resolver {
190 void apply() { 291
292 private final Internals repo;
293 private final DirstateBuilder dirstateBuilder;
294 private final MergeStateBuilder mergeStateBuilder;
295 private boolean changedDirstate;
296 private HgFileRevision revA;
297 private HgFileRevision revB;
298 private Path file;
299 // resolutions:
300 private HgFileRevision resolveUse, resolveForget;
301 private File resolveContent;
302 private boolean resolveMarkUnresolved;
303
304 public ResolverImpl(Internals implRepo, DirstateBuilder dirstateBuilder, MergeStateBuilder mergeStateBuilder) {
305 repo = implRepo;
306 this.dirstateBuilder = dirstateBuilder;
307 this.mergeStateBuilder = mergeStateBuilder;
308 changedDirstate = false;
309 }
310
311 void serializeChanged(Transaction tr) throws HgIOException {
312 if (changedDirstate) {
313 dirstateBuilder.serialize(tr);
314 }
315 mergeStateBuilder.serialize(tr);
316 }
317
318 void presentState(Path p, HgFileRevision revA, HgFileRevision revB) {
319 assert revA != null || revB != null;
320 file = p;
321 this.revA = revA;
322 this.revB = revB;
323 resolveUse = resolveForget = null;
324 resolveContent = null;
325 resolveMarkUnresolved = false;
326 }
327
328 void apply() throws HgIOException, HgRuntimeException {
329 if (resolveMarkUnresolved) {
330 mergeStateBuilder.unresolved(file);
331 } else if (resolveForget != null) {
332 if (resolveForget == revA) {
333 changedDirstate = true;
334 dirstateBuilder.recordRemoved(file);
335 }
336 } else if (resolveUse != null) {
337 if (resolveUse != revA) {
338 changedDirstate = true;
339 final WorkingDirFileWriter fw = new WorkingDirFileWriter(repo);
340 fw.processFile(resolveUse);
341 if (resolveUse == revB) {
342 dirstateBuilder.recordMergedFromP2(file);
343 } else {
344 dirstateBuilder.recordMerged(file, fw.fmode(), fw.mtime(), fw.bytesWritten());
345 }
346 } // if resolution is to use revA, nothing to do
347 } else if (resolveContent != null) {
348 changedDirstate = true;
349 // FIXME write content to file using transaction?
350 InputStream is;
351 try {
352 is = new FileInputStream(resolveContent);
353 } catch (IOException ex) {
354 throw new HgIOException("Failed to read temporary content", ex, resolveContent);
355 }
356 final WorkingDirFileWriter fw = new WorkingDirFileWriter(repo);
357 fw.processFile(file, is, revA == null ? revB.getFileFlags() : revA.getFileFlags());
358 // XXX if presentState(null, fileOnlyInB), and use(InputStream) - i.e.
359 // resolution is to add file with supplied content - shall I put 'Merged', MergedFromP2 or 'Added' into dirstate?
360 if (revA == null && revB != null) {
361 dirstateBuilder.recordMergedFromP2(file);
362 } else {
363 dirstateBuilder.recordMerged(file, fw.fmode(), fw.mtime(), fw.bytesWritten());
364 }
365 } else {
366 assert false;
367 }
191 } 368 }
192 369
193 public void use(HgFileRevision rev) { 370 public void use(HgFileRevision rev) {
194 // TODO Auto-generated method stub 371 if (rev == null) {
195 } 372 throw new IllegalArgumentException();
196 373 }
197 public void use(InputStream content) { 374 assert resolveContent == null;
198 // TODO Auto-generated method stub 375 assert resolveForget == null;
376 resolveUse = rev;
377 }
378
379 public void use(InputStream content) throws IOException {
380 if (content == null) {
381 throw new IllegalArgumentException();
382 }
383 assert resolveUse == null;
384 assert resolveForget == null;
385 try {
386 // cache new contents just to fail fast if there are troubles with content
387 final FileUtils fileUtils = new FileUtils(repo.getLog(), this);
388 resolveContent = fileUtils.createTempFile();
389 fileUtils.write(content, resolveContent);
390 } finally {
391 content.close();
392 }
393 // do not care deleting file in case of failure to allow analyze of the issue
394 }
395
396 public void forget(HgFileRevision rev) {
397 if (rev == null) {
398 throw new IllegalArgumentException();
399 }
400 if (rev != revA || rev != revB) {
401 throw new IllegalArgumentException("Can't forget revision which doesn't represent actual state in either merged trunk");
402 }
403 assert resolveUse == null;
404 assert resolveContent == null;
405 resolveForget = rev;
199 } 406 }
200 407
201 public void unresolved() { 408 public void unresolved() {
202 // TODO Auto-generated method stub 409 resolveMarkUnresolved = true;
203 } 410 }
204 } 411 }
205 } 412 }