Mercurial > hg4j
comparison src/org/tmatesoft/hg/internal/RepositoryComparator.java @ 181:cd3371670f0b
Refactor incoming and outgoing code to be shared with RepositoryComparator. Placeholders for in/out commands. Refactor common remote lookup code
| author | Artem Tikhomirov <tikhomirov.artem@gmail.com> |
|---|---|
| date | Tue, 12 Apr 2011 19:10:38 +0200 |
| parents | |
| children | ec1820f64d2b |
comparison
equal
deleted
inserted
replaced
| 180:42fe9a94b9d0 | 181:cd3371670f0b |
|---|---|
| 1 /* | |
| 2 * Copyright (c) 2011 TMate Software Ltd | |
| 3 * | |
| 4 * This program is free software; you can redistribute it and/or modify | |
| 5 * it under the terms of the GNU General Public License as published by | |
| 6 * the Free Software Foundation; version 2 of the License. | |
| 7 * | |
| 8 * This program is distributed in the hope that it will be useful, | |
| 9 * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 11 * GNU General Public License for more details. | |
| 12 * | |
| 13 * For information on how to redistribute this software under | |
| 14 * the terms of a license other than GNU General Public License | |
| 15 * contact TMate Software at support@hg4j.com | |
| 16 */ | |
| 17 package org.tmatesoft.hg.internal; | |
| 18 | |
| 19 import static org.tmatesoft.hg.core.Nodeid.NULL; | |
| 20 | |
| 21 import java.util.Collections; | |
| 22 import java.util.HashMap; | |
| 23 import java.util.HashSet; | |
| 24 import java.util.LinkedList; | |
| 25 import java.util.List; | |
| 26 import java.util.Map; | |
| 27 import java.util.Map.Entry; | |
| 28 | |
| 29 import org.tmatesoft.hg.core.HgBadStateException; | |
| 30 import org.tmatesoft.hg.core.HgException; | |
| 31 import org.tmatesoft.hg.core.Nodeid; | |
| 32 import org.tmatesoft.hg.repo.HgChangelog; | |
| 33 import org.tmatesoft.hg.repo.HgRemoteRepository; | |
| 34 import org.tmatesoft.hg.repo.HgRemoteRepository.Range; | |
| 35 import org.tmatesoft.hg.repo.HgRemoteRepository.RemoteBranch; | |
| 36 import org.tmatesoft.hg.util.CancelSupport; | |
| 37 import org.tmatesoft.hg.util.CancelledException; | |
| 38 import org.tmatesoft.hg.util.ProgressSupport; | |
| 39 | |
| 40 /** | |
| 41 * | |
| 42 * @author Artem Tikhomirov | |
| 43 * @author TMate Software Ltd. | |
| 44 */ | |
| 45 public class RepositoryComparator { | |
| 46 | |
| 47 private final HgChangelog.ParentWalker localRepo; | |
| 48 private final HgRemoteRepository remoteRepo; | |
| 49 private List<Nodeid> common; | |
| 50 | |
| 51 public RepositoryComparator(HgChangelog.ParentWalker pwLocal, HgRemoteRepository hgRemote) { | |
| 52 localRepo = pwLocal; | |
| 53 remoteRepo = hgRemote; | |
| 54 } | |
| 55 | |
| 56 public RepositoryComparator compare(Object context) throws HgException, CancelledException { | |
| 57 ProgressSupport progressSupport = ProgressSupport.Factory.get(context); | |
| 58 CancelSupport cancelSupport = CancelSupport.Factory.get(context); | |
| 59 cancelSupport.checkCancelled(); | |
| 60 progressSupport.start(10); | |
| 61 common = Collections.unmodifiableList(findCommonWithRemote()); | |
| 62 progressSupport.done(); | |
| 63 return this; | |
| 64 } | |
| 65 | |
| 66 public List<Nodeid> getCommon() { | |
| 67 if (common == null) { | |
| 68 throw new HgBadStateException("Call #compare(Object) first"); | |
| 69 } | |
| 70 return common; | |
| 71 } | |
| 72 | |
| 73 private List<Nodeid> findCommonWithRemote() throws HgException { | |
| 74 List<Nodeid> remoteHeads = remoteRepo.heads(); | |
| 75 LinkedList<Nodeid> resultCommon = new LinkedList<Nodeid>(); // these remotes are known in local | |
| 76 LinkedList<Nodeid> toQuery = new LinkedList<Nodeid>(); // these need further queries to find common | |
| 77 for (Nodeid rh : remoteHeads) { | |
| 78 if (localRepo.knownNode(rh)) { | |
| 79 resultCommon.add(rh); | |
| 80 } else { | |
| 81 toQuery.add(rh); | |
| 82 } | |
| 83 } | |
| 84 if (toQuery.isEmpty()) { | |
| 85 return resultCommon; | |
| 86 } | |
| 87 LinkedList<RemoteBranch> checkUp2Head = new LinkedList<RemoteBranch>(); // branch.root and branch.head are of interest only. | |
| 88 // these are branches with unknown head but known root, which might not be the last common known, | |
| 89 // i.e. there might be children changeset that are also available at remote, [..?..common-head..remote-head] - need to | |
| 90 // scroll up to common head. | |
| 91 while (!toQuery.isEmpty()) { | |
| 92 List<RemoteBranch> remoteBranches = remoteRepo.branches(toQuery); //head, root, first parent, second parent | |
| 93 toQuery.clear(); | |
| 94 while(!remoteBranches.isEmpty()) { | |
| 95 RemoteBranch rb = remoteBranches.remove(0); | |
| 96 // I assume branches remote call gives branches with head equal to what I pass there, i.e. | |
| 97 // that I don't need to check whether rb.head is unknown. | |
| 98 if (localRepo.knownNode(rb.root)) { | |
| 99 // we known branch start, common head is somewhere in its descendants line | |
| 100 checkUp2Head.add(rb); | |
| 101 } else { | |
| 102 // dig deeper in the history, if necessary | |
| 103 if (!NULL.equals(rb.p1) && !localRepo.knownNode(rb.p1)) { | |
| 104 toQuery.add(rb.p1); | |
| 105 } | |
| 106 if (!NULL.equals(rb.p2) && !localRepo.knownNode(rb.p2)) { | |
| 107 toQuery.add(rb.p2); | |
| 108 } | |
| 109 } | |
| 110 } | |
| 111 } | |
| 112 // can't check nodes between checkUp2Head element and local heads, remote might have distinct descendants sequence | |
| 113 for (RemoteBranch rb : checkUp2Head) { | |
| 114 // rb.root is known locally | |
| 115 List<Nodeid> remoteRevisions = remoteRepo.between(rb.head, rb.root); | |
| 116 if (remoteRevisions.isEmpty()) { | |
| 117 // head is immediate child | |
| 118 resultCommon.add(rb.root); | |
| 119 } else { | |
| 120 // between gives result from head to root, I'd like to go in reverse direction | |
| 121 Collections.reverse(remoteRevisions); | |
| 122 Nodeid root = rb.root; | |
| 123 while(!remoteRevisions.isEmpty()) { | |
| 124 Nodeid n = remoteRevisions.remove(0); | |
| 125 if (localRepo.knownNode(n)) { | |
| 126 if (remoteRevisions.isEmpty()) { | |
| 127 // this is the last known node before an unknown | |
| 128 resultCommon.add(n); | |
| 129 break; | |
| 130 } | |
| 131 if (remoteRevisions.size() == 1) { | |
| 132 // there's only one left between known n and unknown head | |
| 133 // this check is to save extra between query, not really essential | |
| 134 Nodeid last = remoteRevisions.remove(0); | |
| 135 resultCommon.add(localRepo.knownNode(last) ? last : n); | |
| 136 break; | |
| 137 } | |
| 138 // might get handy for next between query, to narrow search down | |
| 139 root = n; | |
| 140 } else { | |
| 141 remoteRevisions = remoteRepo.between(n, root); | |
| 142 Collections.reverse(remoteRevisions); | |
| 143 if (remoteRevisions.isEmpty()) { | |
| 144 resultCommon.add(root); | |
| 145 } | |
| 146 } | |
| 147 } | |
| 148 } | |
| 149 } | |
| 150 // TODO ensure unique elements in the list | |
| 151 return resultCommon; | |
| 152 } | |
| 153 | |
| 154 // somewhat similar to Outgoing.findCommonWithRemote() | |
| 155 public List<BranchChain> calculateMissingBranches() throws HgException { | |
| 156 List<Nodeid> remoteHeads = remoteRepo.heads(); | |
| 157 LinkedList<Nodeid> common = new LinkedList<Nodeid>(); // these remotes are known in local | |
| 158 LinkedList<Nodeid> toQuery = new LinkedList<Nodeid>(); // these need further queries to find common | |
| 159 for (Nodeid rh : remoteHeads) { | |
| 160 if (localRepo.knownNode(rh)) { | |
| 161 common.add(rh); | |
| 162 } else { | |
| 163 toQuery.add(rh); | |
| 164 } | |
| 165 } | |
| 166 if (toQuery.isEmpty()) { | |
| 167 return Collections.emptyList(); // no incoming changes | |
| 168 } | |
| 169 LinkedList<BranchChain> branches2load = new LinkedList<BranchChain>(); // return value | |
| 170 // detailed comments are in Outgoing.findCommonWithRemote | |
| 171 LinkedList<RemoteBranch> checkUp2Head = new LinkedList<RemoteBranch>(); | |
| 172 // records relation between branch head and its parent branch, if any | |
| 173 HashMap<Nodeid, BranchChain> head2chain = new HashMap<Nodeid, BranchChain>(); | |
| 174 while (!toQuery.isEmpty()) { | |
| 175 List<RemoteBranch> remoteBranches = remoteRepo.branches(toQuery); //head, root, first parent, second parent | |
| 176 toQuery.clear(); | |
| 177 while(!remoteBranches.isEmpty()) { | |
| 178 RemoteBranch rb = remoteBranches.remove(0); | |
| 179 BranchChain chainElement = head2chain.get(rb.head); | |
| 180 if (chainElement == null) { | |
| 181 chainElement = new BranchChain(rb.head); | |
| 182 // record this unknown branch to download later | |
| 183 branches2load.add(chainElement); | |
| 184 } | |
| 185 if (localRepo.knownNode(rb.root)) { | |
| 186 // we known branch start, common head is somewhere in its descendants line | |
| 187 checkUp2Head.add(rb); | |
| 188 } else { | |
| 189 chainElement.branchRoot = rb.root; | |
| 190 // dig deeper in the history, if necessary | |
| 191 if (!NULL.equals(rb.p1) && !localRepo.knownNode(rb.p1)) { | |
| 192 toQuery.add(rb.p1); | |
| 193 head2chain.put(rb.p1, chainElement.p1 = new BranchChain(rb.p1)); | |
| 194 } | |
| 195 if (!NULL.equals(rb.p2) && !localRepo.knownNode(rb.p2)) { | |
| 196 toQuery.add(rb.p2); | |
| 197 head2chain.put(rb.p2, chainElement.p2 = new BranchChain(rb.p2)); | |
| 198 } | |
| 199 } | |
| 200 } | |
| 201 } | |
| 202 for (RemoteBranch rb : checkUp2Head) { | |
| 203 Nodeid h = rb.head; | |
| 204 Nodeid r = rb.root; | |
| 205 int watchdog = 1000; | |
| 206 BranchChain bc = head2chain.get(h); | |
| 207 assert bc != null; | |
| 208 // if we know branch root locally, there could be no parent branch chain elements. | |
| 209 assert bc.p1 == null; | |
| 210 assert bc.p2 == null; | |
| 211 do { | |
| 212 List<Nodeid> between = remoteRepo.between(h, r); | |
| 213 if (between.isEmpty()) { | |
| 214 bc.branchRoot = r; | |
| 215 break; | |
| 216 } else { | |
| 217 Collections.reverse(between); | |
| 218 for (Nodeid n : between) { | |
| 219 if (localRepo.knownNode(n)) { | |
| 220 r = n; | |
| 221 } else { | |
| 222 h = n; | |
| 223 break; | |
| 224 } | |
| 225 } | |
| 226 Nodeid lastInBetween = between.get(between.size() - 1); | |
| 227 if (r.equals(lastInBetween)) { | |
| 228 bc.branchRoot = r; | |
| 229 break; | |
| 230 } else if (h.equals(lastInBetween)) { // the only chance for current head pointer to point to the sequence tail | |
| 231 // is when r is second from the between list end (iow, head,1,[2],4,8...,root) | |
| 232 bc.branchRoot = r; | |
| 233 break; | |
| 234 } | |
| 235 } | |
| 236 } while(--watchdog > 0); | |
| 237 if (watchdog == 0) { | |
| 238 throw new HgBadStateException(String.format("Can't narrow down branch [%s, %s]", rb.head.shortNotation(), rb.root.shortNotation())); | |
| 239 } | |
| 240 } | |
| 241 return branches2load; | |
| 242 } | |
| 243 | |
| 244 public static final class BranchChain { | |
| 245 // when we construct a chain, we know head which is missing locally, hence init it right away. | |
| 246 // as for root (branch unknown start), we might happen to have one locally, and need further digging to find out right branch start | |
| 247 public final Nodeid branchHead; | |
| 248 public Nodeid branchRoot; | |
| 249 // either of these can be null, or both. | |
| 250 // although RemoteBranch has either both parents null, or both non-null, when we construct a chain | |
| 251 // we might encounter that we locally know one of branch's parent, hence in the chain corresponding field will be blank. | |
| 252 public BranchChain p1; | |
| 253 public BranchChain p2; | |
| 254 | |
| 255 public BranchChain(Nodeid head) { | |
| 256 assert head != null; | |
| 257 branchHead = head; | |
| 258 } | |
| 259 public boolean isTerminal() { | |
| 260 return p1 == null || p2 == null; | |
| 261 } | |
| 262 | |
| 263 @Override | |
| 264 public String toString() { | |
| 265 return String.format("BranchChain [%s, %s]", branchRoot, branchHead); | |
| 266 } | |
| 267 | |
| 268 public void dump() { | |
| 269 System.out.println(toString()); | |
| 270 internalDump(" "); | |
| 271 } | |
| 272 | |
| 273 private void internalDump(String prefix) { | |
| 274 if (p1 != null) { | |
| 275 System.out.println(prefix + p1.toString()); | |
| 276 } | |
| 277 if (p2 != null) { | |
| 278 System.out.println(prefix + p2.toString()); | |
| 279 } | |
| 280 prefix += " "; | |
| 281 if (p1 != null) { | |
| 282 p1.internalDump(prefix); | |
| 283 } | |
| 284 if (p2 != null) { | |
| 285 p2.internalDump(prefix); | |
| 286 } | |
| 287 } | |
| 288 } | |
| 289 | |
| 290 /** | |
| 291 * @return list of nodeids from branchRoot to branchHead, inclusive. IOW, first element of the list is always root of the branch | |
| 292 */ | |
| 293 public List<Nodeid> completeBranch(final Nodeid branchRoot, final Nodeid branchHead) throws HgException { | |
| 294 class DataEntry { | |
| 295 public final Nodeid queryHead; | |
| 296 public final int headIndex; | |
| 297 public List<Nodeid> entries; | |
| 298 | |
| 299 public DataEntry(Nodeid head, int index, List<Nodeid> data) { | |
| 300 queryHead = head; | |
| 301 headIndex = index; | |
| 302 entries = data; | |
| 303 } | |
| 304 }; | |
| 305 | |
| 306 List<Nodeid> initial = remoteRepo.between(branchHead, branchRoot); | |
| 307 Nodeid[] result = new Nodeid[1 + (1 << initial.size())]; | |
| 308 result[0] = branchHead; | |
| 309 int rootIndex = -1; // index in the result, where to place branche's root. | |
| 310 if (initial.isEmpty()) { | |
| 311 rootIndex = 1; | |
| 312 } else if (initial.size() == 1) { | |
| 313 rootIndex = 2; | |
| 314 } | |
| 315 LinkedList<DataEntry> datas = new LinkedList<DataEntry>(); | |
| 316 // DataEntry in datas has entries list filled with 'between' data, whereas | |
| 317 // DataEntry in toQuery keeps only nodeid and its index, with entries to be initialized before | |
| 318 // moving to datas. | |
| 319 LinkedList<DataEntry> toQuery = new LinkedList<DataEntry>(); | |
| 320 // | |
| 321 datas.add(new DataEntry(branchHead, 0, initial)); | |
| 322 int totalQueries = 1; | |
| 323 HashSet<Nodeid> queried = new HashSet<Nodeid>(); | |
| 324 while(!datas.isEmpty()) { | |
| 325 // keep record of those planned to be queried next time we call between() | |
| 326 // although may keep these in queried, if really don't want separate collection | |
| 327 HashSet<Nodeid> scheduled = new HashSet<Nodeid>(); | |
| 328 do { | |
| 329 DataEntry de = datas.removeFirst(); | |
| 330 // populate result with discovered elements between de.qiueryRoot and branch's head | |
| 331 for (int i = 1, j = 0; j < de.entries.size(); i = i << 1, j++) { | |
| 332 int idx = de.headIndex + i; | |
| 333 result[idx] = de.entries.get(j); | |
| 334 } | |
| 335 // form next query entries from new unknown elements | |
| 336 if (de.entries.size() > 1) { | |
| 337 /* when entries has only one element, it means de.queryRoot was at head-2 position, and thus | |
| 338 * no new information can be obtained. E.g. when it's 2, it might be case of [0..4] query with | |
| 339 * [1,2] result, and we need one more query to get element 3. | |
| 340 */ | |
| 341 for (int i =1, j = 0; j < de.entries.size(); i = i<<1, j++) { | |
| 342 int idx = de.headIndex + i; | |
| 343 Nodeid x = de.entries.get(j); | |
| 344 if (!queried.contains(x) && !scheduled.contains(x) && (rootIndex == -1 || rootIndex - de.headIndex > 1)) { | |
| 345 /*queries for elements right before head is senseless, but unless we know head's index, do it anyway*/ | |
| 346 toQuery.add(new DataEntry(x, idx, null)); | |
| 347 scheduled.add(x); | |
| 348 } | |
| 349 } | |
| 350 } | |
| 351 } while (!datas.isEmpty()); | |
| 352 if (!toQuery.isEmpty()) { | |
| 353 totalQueries++; | |
| 354 } | |
| 355 // for each query, create an between request range, keep record Range->DataEntry to know range's start index | |
| 356 LinkedList<HgRemoteRepository.Range> betweenBatch = new LinkedList<HgRemoteRepository.Range>(); | |
| 357 HashMap<HgRemoteRepository.Range, DataEntry> rangeToEntry = new HashMap<HgRemoteRepository.Range, DataEntry>(); | |
| 358 for (DataEntry de : toQuery) { | |
| 359 queried.add(de.queryHead); | |
| 360 HgRemoteRepository.Range r = new HgRemoteRepository.Range(branchRoot, de.queryHead); | |
| 361 betweenBatch.add(r); | |
| 362 rangeToEntry.put(r, de); | |
| 363 } | |
| 364 if (!betweenBatch.isEmpty()) { | |
| 365 Map<Range, List<Nodeid>> between = remoteRepo.between(betweenBatch); | |
| 366 for (Entry<Range, List<Nodeid>> e : between.entrySet()) { | |
| 367 DataEntry de = rangeToEntry.get(e.getKey()); | |
| 368 assert de != null; | |
| 369 de.entries = e.getValue(); | |
| 370 if (rootIndex == -1 && de.entries.size() == 1) { | |
| 371 // returned sequence of length 1 means we used element from [head-2] as root | |
| 372 int numberOfElementsExcludingRootAndHead = de.headIndex + 1; | |
| 373 rootIndex = numberOfElementsExcludingRootAndHead + 1; | |
| 374 System.out.printf("On query %d found out exact number of missing elements: %d\n", totalQueries, numberOfElementsExcludingRootAndHead); | |
| 375 } | |
| 376 datas.add(de); // queue up to record result and construct further requests | |
| 377 } | |
| 378 betweenBatch.clear(); | |
| 379 rangeToEntry.clear(); | |
| 380 } | |
| 381 toQuery.clear(); | |
| 382 } | |
| 383 if (rootIndex == -1) { | |
| 384 throw new HgBadStateException("Shall not happen, provided between output is correct"); // FIXME | |
| 385 } | |
| 386 result[rootIndex] = branchRoot; | |
| 387 boolean resultOk = true; | |
| 388 LinkedList<Nodeid> fromRootToHead = new LinkedList<Nodeid>(); | |
| 389 for (int i = 0; i <= rootIndex; i++) { | |
| 390 Nodeid n = result[i]; | |
| 391 if (n == null) { | |
| 392 System.out.printf("ERROR: element %d wasn't found\n",i); | |
| 393 resultOk = false; | |
| 394 } | |
| 395 fromRootToHead.addFirst(n); // reverse order | |
| 396 } | |
| 397 System.out.println("Total queries:" + totalQueries); | |
| 398 if (!resultOk) { | |
| 399 throw new HgBadStateException("See console for details"); // FIXME | |
| 400 } | |
| 401 return fromRootToHead; | |
| 402 } | |
| 403 } |
