Mercurial > jhg
comparison hg4j/src/main/java/org/tmatesoft/hg/repo/HgStatusCollector.java @ 213:6ec4af642ba8 gradle
Project uses Gradle for build - actual changes
author | Alexander Kitaev <kitaev@gmail.com> |
---|---|
date | Tue, 10 May 2011 10:52:53 +0200 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
212:edb2e2829352 | 213:6ec4af642ba8 |
---|---|
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.repo; | |
18 | |
19 import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION; | |
20 import static org.tmatesoft.hg.repo.HgRepository.TIP; | |
21 | |
22 import java.util.Collection; | |
23 import java.util.Collections; | |
24 import java.util.LinkedHashMap; | |
25 import java.util.LinkedList; | |
26 import java.util.List; | |
27 import java.util.Map; | |
28 import java.util.SortedMap; | |
29 import java.util.TreeMap; | |
30 import java.util.TreeSet; | |
31 | |
32 import org.tmatesoft.hg.core.HgDataStreamException; | |
33 import org.tmatesoft.hg.core.Nodeid; | |
34 import org.tmatesoft.hg.internal.Pool; | |
35 import org.tmatesoft.hg.util.Path; | |
36 import org.tmatesoft.hg.util.PathPool; | |
37 import org.tmatesoft.hg.util.PathRewrite; | |
38 | |
39 | |
40 /** | |
41 * RevisionWalker? | |
42 * | |
43 * @author Artem Tikhomirov | |
44 * @author TMate Software Ltd. | |
45 */ | |
46 public class HgStatusCollector { | |
47 | |
48 private final HgRepository repo; | |
49 private final SortedMap<Integer, ManifestRevisionInspector> cache; // sparse array, in fact | |
50 // with cpython repository, ~70 000 changes, complete Log (direct out, no reverse) output | |
51 // no cache limit, no nodeids and fname caching - OOME on changeset 1035 | |
52 // no cache limit, but with cached nodeids and filenames - 1730+ | |
53 // cache limit 100 - 19+ minutes to process 10000, and still working (too long, stopped) | |
54 private final int cacheMaxSize = 50; // do not keep too much manifest revisions | |
55 private PathPool pathPool; | |
56 private final Pool<Nodeid> cacheNodes; | |
57 private final Pool<String> cacheFilenames; // XXX in fact, need to think if use of PathPool directly instead is better solution | |
58 private final ManifestRevisionInspector emptyFakeState; | |
59 | |
60 | |
61 public HgStatusCollector(HgRepository hgRepo) { | |
62 this.repo = hgRepo; | |
63 cache = new TreeMap<Integer, ManifestRevisionInspector>(); | |
64 cacheNodes = new Pool<Nodeid>(); | |
65 cacheFilenames = new Pool<String>(); | |
66 | |
67 emptyFakeState = new ManifestRevisionInspector(null, null); | |
68 emptyFakeState.begin(-1, null); | |
69 emptyFakeState.end(-1); | |
70 } | |
71 | |
72 public HgRepository getRepo() { | |
73 return repo; | |
74 } | |
75 | |
76 private ManifestRevisionInspector get(int rev) { | |
77 ManifestRevisionInspector i = cache.get(rev); | |
78 if (i == null) { | |
79 if (rev == -1) { | |
80 return emptyFakeState; | |
81 } | |
82 while (cache.size() > cacheMaxSize) { | |
83 // assume usually we go from oldest to newest, hence remove oldest as most likely to be no longer necessary | |
84 cache.remove(cache.firstKey()); | |
85 } | |
86 i = new ManifestRevisionInspector(cacheNodes, cacheFilenames); | |
87 cache.put(rev, i); | |
88 repo.getManifest().walk(rev, rev, i); | |
89 } | |
90 return i; | |
91 } | |
92 | |
93 private boolean cached(int revision) { | |
94 return cache.containsKey(revision) || revision == -1; | |
95 } | |
96 | |
97 private void initCacheRange(int minRev, int maxRev) { | |
98 while (cache.size() > cacheMaxSize) { | |
99 // assume usually we go from oldest to newest, hence remove oldest as most likely to be no longer necessary | |
100 cache.remove(cache.firstKey()); | |
101 } | |
102 repo.getManifest().walk(minRev, maxRev, new HgManifest.Inspector() { | |
103 private ManifestRevisionInspector delegate; | |
104 private boolean cacheHit; // range may include revisions we already know about, do not re-create them | |
105 | |
106 public boolean begin(int revision, Nodeid nid) { | |
107 assert delegate == null; | |
108 if (cache.containsKey(revision)) { // don't need to check emptyFakeState hit as revision never -1 here | |
109 cacheHit = true; | |
110 } else { | |
111 cache.put(revision, delegate = new ManifestRevisionInspector(cacheNodes, cacheFilenames)); | |
112 // cache may grow bigger than max size here, but it's ok as present simplistic cache clearing mechanism may | |
113 // otherwise remove entries we just added | |
114 delegate.begin(revision, nid); | |
115 cacheHit = false; | |
116 } | |
117 return true; | |
118 } | |
119 | |
120 public boolean next(Nodeid nid, String fname, String flags) { | |
121 if (!cacheHit) { | |
122 delegate.next(nid, fname, flags); | |
123 } | |
124 return true; | |
125 } | |
126 | |
127 public boolean end(int revision) { | |
128 if (!cacheHit) { | |
129 delegate.end(revision); | |
130 } | |
131 cacheHit = false; | |
132 delegate = null; | |
133 return true; | |
134 } | |
135 }); | |
136 } | |
137 | |
138 /*package-local*/ ManifestRevisionInspector raw(int rev) { | |
139 return get(rev); | |
140 } | |
141 /*package-local*/ PathPool getPathPool() { | |
142 if (pathPool == null) { | |
143 pathPool = new PathPool(new PathRewrite.Empty()); | |
144 } | |
145 return pathPool; | |
146 } | |
147 | |
148 /** | |
149 * Allows sharing of a common path cache | |
150 */ | |
151 public void setPathPool(PathPool pathPool) { | |
152 this.pathPool = pathPool; | |
153 } | |
154 | |
155 | |
156 // hg status --change <rev> | |
157 public void change(int rev, HgStatusInspector inspector) { | |
158 int[] parents = new int[2]; | |
159 repo.getChangelog().parents(rev, parents, null, null); | |
160 walk(parents[0], rev, inspector); | |
161 } | |
162 | |
163 // I assume revision numbers are the same for changelog and manifest - here | |
164 // user would like to pass changelog revision numbers, and I use them directly to walk manifest. | |
165 // if this assumption is wrong, fix this (lookup manifest revisions from changeset). | |
166 // rev1 and rev2 may be -1 to indicate comparison to empty repository | |
167 // argument order matters | |
168 public void walk(int rev1, int rev2, HgStatusInspector inspector) { | |
169 if (rev1 == rev2) { | |
170 throw new IllegalArgumentException(); | |
171 } | |
172 if (inspector == null) { | |
173 throw new IllegalArgumentException(); | |
174 } | |
175 if (inspector instanceof Record) { | |
176 ((Record) inspector).init(rev1, rev2, this); | |
177 } | |
178 final int lastManifestRevision = repo.getManifest().getLastRevision(); | |
179 if (rev1 == TIP) { | |
180 rev1 = lastManifestRevision; | |
181 } | |
182 if (rev2 == TIP) { | |
183 rev2 = lastManifestRevision; | |
184 } | |
185 // in fact, rev1 and rev2 are often next (or close) to each other, | |
186 // thus, we can optimize Manifest reads here (manifest.walk(rev1, rev2)) | |
187 ManifestRevisionInspector r1, r2 ; | |
188 boolean need1 = !cached(rev1), need2 = !cached(rev2); | |
189 if (need1 || need2) { | |
190 int minRev, maxRev; | |
191 if (need1 && need2 && Math.abs(rev1 - rev2) < 5 /*subjective equivalent of 'close enough'*/) { | |
192 minRev = rev1 < rev2 ? rev1 : rev2; | |
193 maxRev = minRev == rev1 ? rev2 : rev1; | |
194 if (minRev > 0) { | |
195 minRev--; // expand range a bit | |
196 } | |
197 initCacheRange(minRev, maxRev); | |
198 need1 = need2 = false; | |
199 } | |
200 // either both unknown and far from each other, or just one of them. | |
201 // read with neighbors to save potential subsequent calls for neighboring elements | |
202 // XXX perhaps, if revlog.baseRevision is cheap, shall expand minRev up to baseRevision | |
203 // which going to be read anyway | |
204 if (need1) { | |
205 minRev = rev1; | |
206 maxRev = rev1 < lastManifestRevision-5 ? rev1+5 : lastManifestRevision; | |
207 initCacheRange(minRev, maxRev); | |
208 } | |
209 if (need2) { | |
210 minRev = rev2; | |
211 maxRev = rev2 < lastManifestRevision-5 ? rev2+5 : lastManifestRevision; | |
212 initCacheRange(minRev, maxRev); | |
213 } | |
214 } | |
215 r1 = get(rev1); | |
216 r2 = get(rev2); | |
217 | |
218 PathPool pp = getPathPool(); | |
219 | |
220 TreeSet<String> r1Files = new TreeSet<String>(r1.files()); | |
221 for (String fname : r2.files()) { | |
222 if (r1Files.remove(fname)) { | |
223 Nodeid nidR1 = r1.nodeid(fname); | |
224 Nodeid nidR2 = r2.nodeid(fname); | |
225 String flagsR1 = r1.flags(fname); | |
226 String flagsR2 = r2.flags(fname); | |
227 if (nidR1.equals(nidR2) && ((flagsR2 == null && flagsR1 == null) || flagsR2.equals(flagsR1))) { | |
228 inspector.clean(pp.path(fname)); | |
229 } else { | |
230 inspector.modified(pp.path(fname)); | |
231 } | |
232 } else { | |
233 try { | |
234 Path copyTarget = pp.path(fname); | |
235 Path copyOrigin = getOriginIfCopy(repo, copyTarget, r1Files, rev1); | |
236 if (copyOrigin != null) { | |
237 inspector.copied(pp.path(copyOrigin) /*pipe through pool, just in case*/, copyTarget); | |
238 } else { | |
239 inspector.added(copyTarget); | |
240 } | |
241 } catch (HgDataStreamException ex) { | |
242 ex.printStackTrace(); | |
243 // FIXME perhaps, shall record this exception to dedicated mediator and continue | |
244 // for a single file not to be irresolvable obstacle for a status operation | |
245 } | |
246 } | |
247 } | |
248 for (String left : r1Files) { | |
249 inspector.removed(pp.path(left)); | |
250 } | |
251 } | |
252 | |
253 public Record status(int rev1, int rev2) { | |
254 Record rv = new Record(); | |
255 walk(rev1, rev2, rv); | |
256 return rv; | |
257 } | |
258 | |
259 /*package-local*/static Path getOriginIfCopy(HgRepository hgRepo, Path fname, Collection<String> originals, int originalChangelogRevision) throws HgDataStreamException { | |
260 HgDataFile df = hgRepo.getFileNode(fname); | |
261 while (df.isCopy()) { | |
262 Path original = df.getCopySourceName(); | |
263 if (originals.contains(original.toString())) { | |
264 df = hgRepo.getFileNode(original); | |
265 int changelogRevision = df.getChangesetLocalRevision(0); | |
266 if (changelogRevision <= originalChangelogRevision) { | |
267 // copy/rename source was known prior to rev1 | |
268 // (both r1Files.contains is true and original was created earlier than rev1) | |
269 // without r1Files.contains changelogRevision <= rev1 won't suffice as the file | |
270 // might get removed somewhere in between (changelogRevision < R < rev1) | |
271 return original; | |
272 } | |
273 break; // copy/rename done later | |
274 } | |
275 df = hgRepo.getFileNode(original); // try more steps away | |
276 } | |
277 return null; | |
278 } | |
279 | |
280 // XXX for r1..r2 status, only modified, added, removed (and perhaps, clean) make sense | |
281 // XXX Need to specify whether copy targets are in added or not (@see Inspector#copied above) | |
282 public static class Record implements HgStatusInspector { | |
283 private List<Path> modified, added, removed, clean, missing, unknown, ignored; | |
284 private Map<Path, Path> copied; | |
285 | |
286 private int startRev, endRev; | |
287 private HgStatusCollector statusHelper; | |
288 | |
289 // XXX StatusCollector may additionally initialize Record instance to speed lookup of changed file revisions | |
290 // here I need access to ManifestRevisionInspector via #raw(). Perhaps, non-static class (to get | |
291 // implicit reference to StatusCollector) may be better? | |
292 // Since users may want to reuse Record instance we've once created (and initialized), we need to | |
293 // ensure functionality is correct for each/any call (#walk checks instanceof Record and fixes it up) | |
294 // Perhaps, distinct helper (sc.getRevisionHelper().nodeid(fname)) would be better, just not clear | |
295 // how to supply [start..end] values there easily | |
296 /*package-local*/void init(int startRevision, int endRevision, HgStatusCollector self) { | |
297 startRev = startRevision; | |
298 endRev = endRevision; | |
299 statusHelper = self; | |
300 } | |
301 | |
302 public Nodeid nodeidBeforeChange(Path fname) { | |
303 if (statusHelper == null || startRev == BAD_REVISION) { | |
304 return null; | |
305 } | |
306 if ((modified == null || !modified.contains(fname)) && (removed == null || !removed.contains(fname))) { | |
307 return null; | |
308 } | |
309 return statusHelper.raw(startRev).nodeid(fname.toString()); | |
310 } | |
311 public Nodeid nodeidAfterChange(Path fname) { | |
312 if (statusHelper == null || endRev == BAD_REVISION) { | |
313 return null; | |
314 } | |
315 if ((modified == null || !modified.contains(fname)) && (added == null || !added.contains(fname))) { | |
316 return null; | |
317 } | |
318 return statusHelper.raw(endRev).nodeid(fname.toString()); | |
319 } | |
320 | |
321 public List<Path> getModified() { | |
322 return proper(modified); | |
323 } | |
324 | |
325 public List<Path> getAdded() { | |
326 return proper(added); | |
327 } | |
328 | |
329 public List<Path> getRemoved() { | |
330 return proper(removed); | |
331 } | |
332 | |
333 public Map<Path,Path> getCopied() { | |
334 if (copied == null) { | |
335 return Collections.emptyMap(); | |
336 } | |
337 return Collections.unmodifiableMap(copied); | |
338 } | |
339 | |
340 public List<Path> getClean() { | |
341 return proper(clean); | |
342 } | |
343 | |
344 public List<Path> getMissing() { | |
345 return proper(missing); | |
346 } | |
347 | |
348 public List<Path> getUnknown() { | |
349 return proper(unknown); | |
350 } | |
351 | |
352 public List<Path> getIgnored() { | |
353 return proper(ignored); | |
354 } | |
355 | |
356 private List<Path> proper(List<Path> l) { | |
357 if (l == null) { | |
358 return Collections.emptyList(); | |
359 } | |
360 return Collections.unmodifiableList(l); | |
361 } | |
362 | |
363 // | |
364 // | |
365 | |
366 public void modified(Path fname) { | |
367 modified = doAdd(modified, fname); | |
368 } | |
369 | |
370 public void added(Path fname) { | |
371 added = doAdd(added, fname); | |
372 } | |
373 | |
374 public void copied(Path fnameOrigin, Path fnameAdded) { | |
375 if (copied == null) { | |
376 copied = new LinkedHashMap<Path, Path>(); | |
377 } | |
378 added(fnameAdded); | |
379 copied.put(fnameAdded, fnameOrigin); | |
380 } | |
381 | |
382 public void removed(Path fname) { | |
383 removed = doAdd(removed, fname); | |
384 } | |
385 | |
386 public void clean(Path fname) { | |
387 clean = doAdd(clean, fname); | |
388 } | |
389 | |
390 public void missing(Path fname) { | |
391 missing = doAdd(missing, fname); | |
392 } | |
393 | |
394 public void unknown(Path fname) { | |
395 unknown = doAdd(unknown, fname); | |
396 } | |
397 | |
398 public void ignored(Path fname) { | |
399 ignored = doAdd(ignored, fname); | |
400 } | |
401 | |
402 private static List<Path> doAdd(List<Path> l, Path p) { | |
403 if (l == null) { | |
404 l = new LinkedList<Path>(); | |
405 } | |
406 l.add(p); | |
407 return l; | |
408 } | |
409 } | |
410 | |
411 /*package-local*/ static final class ManifestRevisionInspector implements HgManifest.Inspector { | |
412 private final TreeMap<String, Nodeid> idsMap; | |
413 private final TreeMap<String, String> flagsMap; | |
414 private final Pool<Nodeid> idsPool; | |
415 private final Pool<String> namesPool; | |
416 | |
417 // optional pools for effective management of nodeids and filenames (they are likely | |
418 // to be duplicated among different manifest revisions | |
419 public ManifestRevisionInspector(Pool<Nodeid> nodeidPool, Pool<String> filenamePool) { | |
420 idsPool = nodeidPool; | |
421 namesPool = filenamePool; | |
422 idsMap = new TreeMap<String, Nodeid>(); | |
423 flagsMap = new TreeMap<String, String>(); | |
424 } | |
425 | |
426 public Collection<String> files() { | |
427 return idsMap.keySet(); | |
428 } | |
429 | |
430 public Nodeid nodeid(String fname) { | |
431 return idsMap.get(fname); | |
432 } | |
433 | |
434 public String flags(String fname) { | |
435 return flagsMap.get(fname); | |
436 } | |
437 | |
438 // | |
439 | |
440 public boolean next(Nodeid nid, String fname, String flags) { | |
441 if (namesPool != null) { | |
442 fname = namesPool.unify(fname); | |
443 } | |
444 if (idsPool != null) { | |
445 nid = idsPool.unify(nid); | |
446 } | |
447 idsMap.put(fname, nid); | |
448 if (flags != null) { | |
449 // TreeMap$Entry takes 32 bytes. No reason to keep null for such price | |
450 // Perhaps, Map<String, Pair<Nodeid, String>> might be better solution | |
451 flagsMap.put(fname, flags); | |
452 } | |
453 return true; | |
454 } | |
455 | |
456 public boolean end(int revision) { | |
457 // in fact, this class cares about single revision | |
458 return false; | |
459 } | |
460 | |
461 public boolean begin(int revision, Nodeid nid) { | |
462 return true; | |
463 } | |
464 } | |
465 | |
466 } |