tikhomirov@10: /* tikhomirov@412: * Copyright (c) 2010-2012 TMate Software Ltd tikhomirov@74: * tikhomirov@74: * This program is free software; you can redistribute it and/or modify tikhomirov@74: * it under the terms of the GNU General Public License as published by tikhomirov@74: * the Free Software Foundation; version 2 of the License. tikhomirov@74: * tikhomirov@74: * This program is distributed in the hope that it will be useful, tikhomirov@74: * but WITHOUT ANY WARRANTY; without even the implied warranty of tikhomirov@74: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the tikhomirov@74: * GNU General Public License for more details. tikhomirov@74: * tikhomirov@74: * For information on how to redistribute this software under tikhomirov@74: * the terms of a license other than GNU General Public License tikhomirov@102: * contact TMate Software at support@hg4j.com tikhomirov@10: */ tikhomirov@74: package org.tmatesoft.hg.repo; tikhomirov@10: tikhomirov@284: import static org.tmatesoft.hg.core.Nodeid.NULL; tikhomirov@490: import static org.tmatesoft.hg.repo.HgRepositoryFiles.Dirstate; tikhomirov@456: import static org.tmatesoft.hg.util.LogFacility.Severity.Debug; tikhomirov@284: tikhomirov@252: import java.io.BufferedReader; tikhomirov@10: import java.io.File; tikhomirov@348: import java.io.FileNotFoundException; tikhomirov@252: import java.io.FileReader; tikhomirov@10: import java.io.IOException; tikhomirov@10: import java.util.Collections; tikhomirov@293: import java.util.HashMap; tikhomirov@18: import java.util.LinkedHashMap; tikhomirov@18: import java.util.Map; tikhomirov@18: import java.util.TreeSet; tikhomirov@10: tikhomirov@231: import org.tmatesoft.hg.core.Nodeid; tikhomirov@74: import org.tmatesoft.hg.internal.DataAccess; tikhomirov@412: import org.tmatesoft.hg.internal.EncodingHelper; tikhomirov@490: import org.tmatesoft.hg.internal.Internals; tikhomirov@284: import org.tmatesoft.hg.util.Pair; tikhomirov@141: import org.tmatesoft.hg.util.Path; tikhomirov@291: import org.tmatesoft.hg.util.PathRewrite; tikhomirov@456: import org.tmatesoft.hg.util.LogFacility.Severity; tikhomirov@74: tikhomirov@10: tikhomirov@10: /** tikhomirov@10: * @see http://mercurial.selenic.com/wiki/DirState tikhomirov@10: * @see http://mercurial.selenic.com/wiki/FileFormats#dirstate tikhomirov@74: * tikhomirov@74: * @author Artem Tikhomirov tikhomirov@74: * @author TMate Software Ltd. tikhomirov@10: */ tikhomirov@290: public final class HgDirstate /* XXX RepoChangeListener */{ tikhomirov@290: tikhomirov@290: public enum EntryKind { tikhomirov@290: Normal, Added, Removed, Merged, // order is being used in code of this class, don't change unless any use is checked tikhomirov@290: } tikhomirov@10: tikhomirov@490: private final Internals repo; tikhomirov@431: private final Path.Source pathPool; tikhomirov@291: private final PathRewrite canonicalPathRewrite; tikhomirov@284: private Map normal; tikhomirov@284: private Map added; tikhomirov@284: private Map removed; tikhomirov@284: private Map merged; tikhomirov@293: /* map of canonicalized file names to their originals from dirstate file. tikhomirov@293: * Note, only those canonical names that differ from their dirstate counterpart are recorded here tikhomirov@293: */ tikhomirov@293: private Map canonical2dirstateName; tikhomirov@284: private Pair parents; tikhomirov@252: tikhomirov@291: // canonicalPath may be null if we don't need to check for names other than in dirstate tikhomirov@490: /*package-local*/ HgDirstate(Internals hgRepo, Path.Source pathSource, PathRewrite canonicalPath) { tikhomirov@252: repo = hgRepo; tikhomirov@431: pathPool = pathSource; tikhomirov@291: canonicalPathRewrite = canonicalPath; tikhomirov@10: } tikhomirov@10: tikhomirov@490: /*package-local*/ void read() throws HgInvalidControlFileException { tikhomirov@490: EncodingHelper encodingHelper = repo.buildFileNameEncodingHelper(); tikhomirov@284: normal = added = removed = merged = Collections.emptyMap(); tikhomirov@371: parents = new Pair(Nodeid.NULL, Nodeid.NULL); tikhomirov@332: if (canonicalPathRewrite != null) { tikhomirov@332: canonical2dirstateName = new HashMap(); tikhomirov@332: } else { tikhomirov@332: canonical2dirstateName = Collections.emptyMap(); tikhomirov@332: } tikhomirov@490: File dirstateFile = getDirstateFile(repo); tikhomirov@59: if (dirstateFile == null || !dirstateFile.exists()) { tikhomirov@10: return; tikhomirov@10: } tikhomirov@252: DataAccess da = repo.getDataAccess().create(dirstateFile); tikhomirov@10: try { tikhomirov@421: if (da.isEmpty()) { tikhomirov@421: return; tikhomirov@421: } tikhomirov@421: // not sure linked is really needed here, just for ease of debug tikhomirov@421: normal = new LinkedHashMap(); tikhomirov@421: added = new LinkedHashMap(); tikhomirov@421: removed = new LinkedHashMap(); tikhomirov@421: merged = new LinkedHashMap(); tikhomirov@421: tikhomirov@284: parents = internalReadParents(da); tikhomirov@227: // hg init; hg up produces an empty repository where dirstate has parents (40 bytes) only tikhomirov@227: while (!da.isEmpty()) { tikhomirov@10: final byte state = da.readByte(); tikhomirov@10: final int fmode = da.readInt(); tikhomirov@10: final int size = da.readInt(); tikhomirov@10: final int time = da.readInt(); tikhomirov@10: final int nameLen = da.readInt(); tikhomirov@10: String fn1 = null, fn2 = null; tikhomirov@10: byte[] name = new byte[nameLen]; tikhomirov@10: da.readBytes(name, 0, nameLen); tikhomirov@10: for (int i = 0; i < nameLen; i++) { tikhomirov@10: if (name[i] == 0) { tikhomirov@412: fn1 = encodingHelper.fromDirstate(name, 0, i); tikhomirov@412: fn2 = encodingHelper.fromDirstate(name, i+1, nameLen - i - 1); tikhomirov@10: break; tikhomirov@10: } tikhomirov@10: } tikhomirov@10: if (fn1 == null) { tikhomirov@412: fn1 = encodingHelper.fromDirstate(name, 0, nameLen); tikhomirov@10: } tikhomirov@284: Record r = new Record(fmode, size, time, pathPool.path(fn1), fn2 == null ? null : pathPool.path(fn2)); tikhomirov@293: if (canonicalPathRewrite != null) { tikhomirov@293: Path canonicalPath = pathPool.path(canonicalPathRewrite.rewrite(fn1).toString()); tikhomirov@293: if (canonicalPath != r.name()) { // == as they come from the same pool tikhomirov@293: assert !canonical2dirstateName.containsKey(canonicalPath); // otherwise there's already a file with same canonical name tikhomirov@293: // which can't happen for case-insensitive file system (or there's erroneous PathRewrite, perhaps doing smth else) tikhomirov@293: canonical2dirstateName.put(canonicalPath, r.name()); tikhomirov@293: } tikhomirov@293: if (fn2 != null) { tikhomirov@293: // not sure I need copy origin in the map, I don't seem to use it anywhere, tikhomirov@293: // but I guess I'll have to use it some day. tikhomirov@293: canonicalPath = pathPool.path(canonicalPathRewrite.rewrite(fn2).toString()); tikhomirov@293: if (canonicalPath != r.copySource()) { tikhomirov@293: canonical2dirstateName.put(canonicalPath, r.copySource()); tikhomirov@293: } tikhomirov@293: } tikhomirov@293: } tikhomirov@10: if (state == 'n') { tikhomirov@18: normal.put(r.name1, r); tikhomirov@10: } else if (state == 'a') { tikhomirov@18: added.put(r.name1, r); tikhomirov@10: } else if (state == 'r') { tikhomirov@18: removed.put(r.name1, r); tikhomirov@10: } else if (state == 'm') { tikhomirov@18: merged.put(r.name1, r); tikhomirov@10: } else { tikhomirov@501: repo.getSessionContext().getLog().dump(getClass(), Severity.Warn, "Dirstate record for file %s (size: %d, tstamp:%d) has unknown state '%c'", r.name1, r.size(), r.time, state); tikhomirov@10: } tikhomirov@227: } tikhomirov@10: } catch (IOException ex) { tikhomirov@348: throw new HgInvalidControlFileException("Dirstate read failed", ex, dirstateFile); tikhomirov@10: } finally { tikhomirov@10: da.done(); tikhomirov@10: } tikhomirov@10: } tikhomirov@10: tikhomirov@284: private static Pair internalReadParents(DataAccess da) throws IOException { tikhomirov@284: byte[] parents = new byte[40]; tikhomirov@284: da.readBytes(parents, 0, 40); tikhomirov@284: Nodeid n1 = Nodeid.fromBinary(parents, 0); tikhomirov@284: Nodeid n2 = Nodeid.fromBinary(parents, 20); tikhomirov@284: parents = null; tikhomirov@284: return new Pair(n1, n2); tikhomirov@284: } tikhomirov@284: tikhomirov@284: /** tikhomirov@290: * @return pair of working copy parents, with {@link Nodeid#NULL} for missing values. tikhomirov@284: */ tikhomirov@284: public Pair parents() { tikhomirov@348: assert parents != null; // instance not initialized with #read() tikhomirov@284: return parents; tikhomirov@284: } tikhomirov@284: tikhomirov@490: private static File getDirstateFile(Internals repo) { tikhomirov@490: return repo.getFileFromRepoDir(Dirstate.getName()); tikhomirov@490: } tikhomirov@490: tikhomirov@284: /** tikhomirov@284: * @return pair of parents, both {@link Nodeid#NULL} if dirstate is not available tikhomirov@284: */ tikhomirov@490: /*package-local*/ static Pair readParents(Internals internalRepo) throws HgInvalidControlFileException { tikhomirov@284: // do not read whole dirstate if all we need is WC parent information tikhomirov@490: File dirstateFile = getDirstateFile(internalRepo); tikhomirov@231: if (dirstateFile == null || !dirstateFile.exists()) { tikhomirov@284: return new Pair(NULL, NULL); tikhomirov@231: } tikhomirov@490: DataAccess da = internalRepo.getDataAccess().create(dirstateFile); tikhomirov@231: try { tikhomirov@421: if (da.isEmpty()) { tikhomirov@421: return new Pair(NULL, NULL); tikhomirov@421: } tikhomirov@284: return internalReadParents(da); tikhomirov@231: } catch (IOException ex) { tikhomirov@348: throw new HgInvalidControlFileException("Error reading working copy parents from dirstate", ex, dirstateFile); tikhomirov@231: } finally { tikhomirov@231: da.done(); tikhomirov@231: } tikhomirov@231: } tikhomirov@231: tikhomirov@231: /** tikhomirov@430: * TODO [post-1.0] it's really not a proper place for the method, need WorkingCopyContainer or similar tikhomirov@252: * @return branch associated with the working directory tikhomirov@252: */ tikhomirov@490: /*package-local*/ static String readBranch(Internals internalRepo) throws HgInvalidControlFileException { tikhomirov@490: File branchFile = internalRepo.getFileFromRepoDir("branch"); tikhomirov@284: String branch = HgRepository.DEFAULT_BRANCH_NAME; tikhomirov@284: if (branchFile.exists()) { tikhomirov@284: try { tikhomirov@284: BufferedReader r = new BufferedReader(new FileReader(branchFile)); tikhomirov@284: String b = r.readLine(); tikhomirov@284: if (b != null) { tikhomirov@284: b = b.trim().intern(); tikhomirov@284: } tikhomirov@284: branch = b == null || b.length() == 0 ? HgRepository.DEFAULT_BRANCH_NAME : b; tikhomirov@284: r.close(); tikhomirov@348: } catch (FileNotFoundException ex) { tikhomirov@501: internalRepo.getSessionContext().getLog().dump(HgDirstate.class, Debug, ex, null); // log verbose debug, exception might be legal here tikhomirov@348: // IGNORE tikhomirov@284: } catch (IOException ex) { tikhomirov@348: throw new HgInvalidControlFileException("Error reading file with branch information", ex, branchFile); tikhomirov@284: } tikhomirov@284: } tikhomirov@284: return branch; tikhomirov@284: } tikhomirov@231: tikhomirov@18: // new, modifiable collection tikhomirov@284: /*package-local*/ TreeSet all() { tikhomirov@348: assert normal != null; tikhomirov@284: TreeSet rv = new TreeSet(); tikhomirov@18: @SuppressWarnings("unchecked") tikhomirov@284: Map[] all = new Map[] { normal, added, removed, merged }; tikhomirov@18: for (int i = 0; i < all.length; i++) { tikhomirov@18: for (Record r : all[i].values()) { tikhomirov@18: rv.add(r.name1); tikhomirov@18: } tikhomirov@18: } tikhomirov@18: return rv; tikhomirov@18: } tikhomirov@18: tikhomirov@141: /*package-local*/ Record checkNormal(Path fname) { tikhomirov@293: return internalCheck(normal, fname); tikhomirov@18: } tikhomirov@18: tikhomirov@141: /*package-local*/ Record checkAdded(Path fname) { tikhomirov@293: return internalCheck(added, fname); tikhomirov@141: } tikhomirov@141: /*package-local*/ Record checkRemoved(Path fname) { tikhomirov@293: return internalCheck(removed, fname); tikhomirov@18: } tikhomirov@141: /*package-local*/ Record checkMerged(Path fname) { tikhomirov@293: return internalCheck(merged, fname); tikhomirov@18: } tikhomirov@18: tikhomirov@293: tikhomirov@293: // return non-null if fname is known, either as is, or its canonical form. in latter case, this canonical form is return value tikhomirov@293: /*package-local*/ Path known(Path fname) { tikhomirov@293: Path fnameCanonical = null; tikhomirov@293: if (canonicalPathRewrite != null) { tikhomirov@293: fnameCanonical = pathPool.path(canonicalPathRewrite.rewrite(fname).toString()); tikhomirov@293: if (fnameCanonical != fname && canonical2dirstateName.containsKey(fnameCanonical)) { tikhomirov@293: // we know right away there's name in dirstate with alternative canonical form tikhomirov@293: return canonical2dirstateName.get(fnameCanonical); tikhomirov@293: } tikhomirov@293: } tikhomirov@293: @SuppressWarnings("unchecked") tikhomirov@293: Map[] all = new Map[] { normal, added, removed, merged }; tikhomirov@293: for (int i = 0; i < all.length; i++) { tikhomirov@293: if (all[i].containsKey(fname)) { tikhomirov@293: return fname; tikhomirov@293: } tikhomirov@293: if (fnameCanonical != null && all[i].containsKey(fnameCanonical)) { tikhomirov@293: return fnameCanonical; tikhomirov@293: } tikhomirov@293: } tikhomirov@293: return null; tikhomirov@293: } tikhomirov@18: tikhomirov@293: private Record internalCheck(Map map, Path fname) { tikhomirov@293: Record rv = map.get(fname); tikhomirov@293: if (rv != null || canonicalPathRewrite == null) { tikhomirov@293: return rv; tikhomirov@293: } tikhomirov@293: Path fnameCanonical = pathPool.path(canonicalPathRewrite.rewrite(fname).toString()); tikhomirov@293: if (fnameCanonical != fname) { tikhomirov@293: // case when fname = /a/B/c, and dirstate is /a/b/C tikhomirov@293: if (canonical2dirstateName.containsKey(fnameCanonical)) { tikhomirov@293: return map.get(canonical2dirstateName.get(fnameCanonical)); tikhomirov@293: } tikhomirov@293: // try canonical directly, fname = /a/B/C, dirstate has /a/b/c tikhomirov@293: if ((rv = map.get(fnameCanonical)) != null) { tikhomirov@293: return rv; tikhomirov@293: } tikhomirov@293: } tikhomirov@293: return null; tikhomirov@293: } tikhomirov@18: tikhomirov@290: public void walk(Inspector inspector) { tikhomirov@348: assert normal != null; tikhomirov@290: @SuppressWarnings("unchecked") tikhomirov@290: Map[] all = new Map[] { normal, added, removed, merged }; tikhomirov@290: for (int i = 0; i < all.length; i++) { tikhomirov@290: EntryKind k = EntryKind.values()[i]; tikhomirov@290: for (Record r : all[i].values()) { tikhomirov@290: if (!inspector.next(k, r)) { tikhomirov@290: return; tikhomirov@290: } tikhomirov@290: } tikhomirov@290: } tikhomirov@290: } tikhomirov@290: tikhomirov@290: public interface Inspector { tikhomirov@293: /** tikhomirov@293: * Invoked for each entry in the directory state file tikhomirov@293: * @param kind file record kind tikhomirov@293: * @param entry file record. Note, do not cache instance as it may be reused between the calls tikhomirov@293: * @return true to indicate further records are still of interest, false to stop iteration tikhomirov@293: */ tikhomirov@290: boolean next(EntryKind kind, Record entry); tikhomirov@290: } tikhomirov@290: tikhomirov@293: public static final class Record implements Cloneable { tikhomirov@290: private final int mode, size, time; tikhomirov@290: // Dirstate keeps local file size (i.e. that with any filters already applied). tikhomirov@280: // Thus, can't compare directly to HgDataFile.length() tikhomirov@290: private final Path name1, name2; tikhomirov@10: tikhomirov@290: /*package-local*/ Record(int fmode, int fsize, int ftime, Path name1, Path name2) { tikhomirov@10: mode = fmode; tikhomirov@10: size = fsize; tikhomirov@10: time = ftime; tikhomirov@10: this.name1 = name1; tikhomirov@10: this.name2 = name2; tikhomirov@10: tikhomirov@10: } tikhomirov@290: tikhomirov@290: public Path name() { tikhomirov@290: return name1; tikhomirov@290: } tikhomirov@290: tikhomirov@290: /** tikhomirov@290: * @return non-null for copy/move tikhomirov@290: */ tikhomirov@290: public Path copySource() { tikhomirov@290: return name2; tikhomirov@290: } tikhomirov@290: tikhomirov@290: public int modificationTime() { tikhomirov@290: return time; tikhomirov@290: } tikhomirov@290: tikhomirov@290: public int size() { tikhomirov@290: return size; tikhomirov@290: } tikhomirov@293: tikhomirov@348: public int mode() { tikhomirov@348: return mode; tikhomirov@348: } tikhomirov@348: tikhomirov@293: @Override tikhomirov@293: public Record clone() { tikhomirov@293: try { tikhomirov@293: return (Record) super.clone(); tikhomirov@293: } catch (CloneNotSupportedException ex) { tikhomirov@293: throw new InternalError(ex.toString()); tikhomirov@293: } tikhomirov@293: } tikhomirov@10: } tikhomirov@10: }