tikhomirov@10: /* tikhomirov@20: * Copyright (c) 2010, 2011 Artem Tikhomirov tikhomirov@1: */ tikhomirov@1: package com.tmate.hgkit.ll; tikhomirov@1: tikhomirov@22: import java.io.BufferedInputStream; tikhomirov@8: import java.io.BufferedReader; tikhomirov@1: import java.io.File; tikhomirov@8: import java.io.FileInputStream; tikhomirov@1: import java.io.IOException; tikhomirov@8: import java.io.InputStreamReader; tikhomirov@3: import java.lang.ref.SoftReference; tikhomirov@8: import java.util.Arrays; tikhomirov@57: import java.util.Collections; tikhomirov@3: import java.util.HashMap; tikhomirov@18: import java.util.LinkedList; tikhomirov@57: import java.util.Set; tikhomirov@8: import java.util.TreeSet; tikhomirov@1: tikhomirov@10: import com.tmate.hgkit.fs.DataAccessProvider; tikhomirov@10: tikhomirov@1: /** tikhomirov@1: * @author artem tikhomirov@1: */ tikhomirov@1: public class LocalHgRepo extends HgRepository { tikhomirov@1: tikhomirov@8: private File repoDir; // .hg folder tikhomirov@1: private final String repoLocation; tikhomirov@10: private final DataAccessProvider dataAccess; tikhomirov@1: tikhomirov@1: public LocalHgRepo(String repositoryPath) { tikhomirov@1: setInvalid(true); tikhomirov@1: repoLocation = repositoryPath; tikhomirov@10: dataAccess = null; tikhomirov@1: } tikhomirov@1: tikhomirov@1: public LocalHgRepo(File repositoryRoot) throws IOException { tikhomirov@1: assert ".hg".equals(repositoryRoot.getName()) && repositoryRoot.isDirectory(); tikhomirov@1: setInvalid(false); tikhomirov@1: repoDir = repositoryRoot; tikhomirov@1: repoLocation = repositoryRoot.getParentFile().getCanonicalPath(); tikhomirov@10: dataAccess = new DataAccessProvider(); tikhomirov@8: parseRequires(); tikhomirov@1: } tikhomirov@1: tikhomirov@1: @Override tikhomirov@1: public String getLocation() { tikhomirov@1: return repoLocation; tikhomirov@1: } tikhomirov@18: tikhomirov@57: public void statusLocal(int baseRevision, StatusCollector.Inspector inspector) { tikhomirov@57: LinkedList folders = new LinkedList(); tikhomirov@57: final File rootDir = repoDir.getParentFile(); tikhomirov@57: folders.add(rootDir); tikhomirov@57: final HgDirstate dirstate = loadDirstate(); tikhomirov@57: final HgIgnore hgignore = loadIgnore(); tikhomirov@57: TreeSet knownEntries = dirstate.all(); tikhomirov@57: final boolean isTipBase = baseRevision == TIP || baseRevision == getManifest().getRevisionCount(); tikhomirov@57: StatusCollector.ManifestRevisionInspector collect = null; tikhomirov@57: Set baseRevFiles = Collections.emptySet(); tikhomirov@57: if (!isTipBase) { tikhomirov@57: collect = new StatusCollector.ManifestRevisionInspector(baseRevision, baseRevision); tikhomirov@57: getManifest().walk(baseRevision, baseRevision, collect); tikhomirov@57: baseRevFiles = new TreeSet(collect.files(baseRevision)); tikhomirov@57: } tikhomirov@57: do { tikhomirov@57: File d = folders.removeFirst(); tikhomirov@57: for (File f : d.listFiles()) { tikhomirov@57: if (f.isDirectory()) { tikhomirov@57: if (!".hg".equals(f.getName())) { tikhomirov@57: folders.addLast(f); tikhomirov@57: } tikhomirov@57: } else { tikhomirov@57: // FIXME path relative to rootDir - need more robust approach tikhomirov@57: String fname = normalize(f.getPath().substring(rootDir.getPath().length() + 1)); tikhomirov@57: if (hgignore.isIgnored(fname)) { tikhomirov@57: inspector.ignored(fname); tikhomirov@57: } else { tikhomirov@57: if (knownEntries.remove(fname)) { tikhomirov@57: // modified, added, removed, clean tikhomirov@57: if (collect != null) { // need to check against base revision, not FS file tikhomirov@57: Nodeid nid1 = collect.nodeid(baseRevision, fname); tikhomirov@57: String flags = collect.flags(baseRevision, fname); tikhomirov@57: checkLocalStatusAgainstBaseRevision(baseRevFiles, nid1, flags, fname, f, dirstate, inspector); tikhomirov@57: baseRevFiles.remove(fname); tikhomirov@57: } else { tikhomirov@57: checkLocalStatusAgainstFile(fname, f, dirstate, inspector); tikhomirov@57: } tikhomirov@57: } else { tikhomirov@57: inspector.unknown(fname); tikhomirov@57: } tikhomirov@57: } tikhomirov@57: } tikhomirov@57: } tikhomirov@57: } while (!folders.isEmpty()); tikhomirov@57: if (collect != null) { tikhomirov@57: for (String r : baseRevFiles) { tikhomirov@57: inspector.removed(r); tikhomirov@57: } tikhomirov@57: } tikhomirov@57: for (String m : knownEntries) { tikhomirov@57: // removed from the repository and missing from working dir shall not be reported as 'deleted' tikhomirov@57: if (dirstate.checkRemoved(m) == null) { tikhomirov@57: inspector.missing(m); tikhomirov@57: } tikhomirov@57: } tikhomirov@57: } tikhomirov@57: tikhomirov@57: private static void checkLocalStatusAgainstFile(String fname, File f, HgDirstate dirstate, StatusCollector.Inspector inspector) { tikhomirov@57: HgDirstate.Record r; tikhomirov@57: if ((r = dirstate.checkNormal(fname)) != null) { tikhomirov@57: // either clean or modified tikhomirov@57: if (f.lastModified() / 1000 == r.time && r.size == f.length()) { tikhomirov@57: inspector.clean(fname); tikhomirov@57: } else { tikhomirov@57: // FIXME check actual content to avoid false modified files tikhomirov@57: inspector.modified(fname); tikhomirov@57: } tikhomirov@57: } else if ((r = dirstate.checkAdded(fname)) != null) { tikhomirov@57: if (r.name2 == null) { tikhomirov@57: inspector.added(fname); tikhomirov@57: } else { tikhomirov@57: inspector.copied(fname, r.name2); tikhomirov@57: } tikhomirov@57: } else if ((r = dirstate.checkRemoved(fname)) != null) { tikhomirov@57: inspector.removed(fname); tikhomirov@57: } else if ((r = dirstate.checkMerged(fname)) != null) { tikhomirov@57: inspector.modified(fname); tikhomirov@57: } tikhomirov@57: } tikhomirov@57: tikhomirov@57: // XXX refactor checkLocalStatus methods in more OO way tikhomirov@57: private void checkLocalStatusAgainstBaseRevision(Set baseRevNames, Nodeid nid1, String flags, String fname, File f, HgDirstate dirstate, StatusCollector.Inspector inspector) { tikhomirov@57: // fname is in the dirstate, either Normal, Added, Removed or Merged tikhomirov@57: HgDirstate.Record r; tikhomirov@57: if (nid1 == null) { tikhomirov@57: // normal: added? tikhomirov@57: // added: not known at the time of baseRevision, shall report tikhomirov@57: // merged: was not known, report as added? tikhomirov@57: if ((r = dirstate.checkAdded(fname)) != null) { tikhomirov@57: if (r.name2 != null && baseRevNames.contains(r.name2)) { tikhomirov@57: baseRevNames.remove(r.name2); tikhomirov@57: inspector.copied(r.name2, fname); tikhomirov@57: return; tikhomirov@57: } tikhomirov@57: // fall-through, report as added tikhomirov@57: } else if (dirstate.checkRemoved(fname) != null) { tikhomirov@57: // removed: removed file was not known at the time of baseRevision, and we should not report it as removed tikhomirov@57: return; tikhomirov@57: } tikhomirov@57: inspector.added(fname); tikhomirov@57: } else { tikhomirov@57: // was known; check whether clean or modified tikhomirov@57: // when added - seems to be the case of a file added once again, hence need to check if content is different tikhomirov@57: if ((r = dirstate.checkNormal(fname)) != null || (r = dirstate.checkMerged(fname)) != null || (r = dirstate.checkAdded(fname)) != null) { tikhomirov@57: // either clean or modified tikhomirov@57: HgDataFile fileNode = getFileNode(fname); tikhomirov@57: final int lengthAtRevision = fileNode.length(nid1); tikhomirov@57: if (r.size /* XXX File.length() ?! */ != lengthAtRevision || flags != todoGenerateFlags(fname /*java.io.File*/)) { tikhomirov@57: inspector.modified(fname); tikhomirov@57: } else { tikhomirov@57: // check actual content to see actual changes tikhomirov@57: // XXX consider adding HgDataDile.compare(File/byte[]/whatever) operation to optimize comparison tikhomirov@57: if (areTheSame(f, fileNode.content(nid1))) { tikhomirov@57: inspector.clean(fname); tikhomirov@57: } else { tikhomirov@57: inspector.modified(fname); tikhomirov@57: } tikhomirov@57: } tikhomirov@57: } tikhomirov@57: // only those left in idsMap after processing are reported as removed tikhomirov@57: } tikhomirov@57: tikhomirov@57: // TODO think over if content comparison may be done more effectively by e.g. calculating nodeid for a local file and comparing it with nodeid from manifest tikhomirov@57: // we don't need to tell exact difference, hash should be enough to detect difference, and it doesn't involve reading historical file content, and it's relatively tikhomirov@57: // cheap to calc hash on a file (no need to keep it completely in memory). OTOH, if I'm right that the next approach is used for nodeids: tikhomirov@57: // changeset nodeid + hash(actual content) => entry (Nodeid) in the next Manifest tikhomirov@57: // then it's sufficient to check parents from dirstate, and if they do not match parents from file's baseRevision (non matching parents means different nodeids). tikhomirov@57: // The question is whether original Hg treats this case (same content, different parents and hence nodeids) as 'modified' or 'clean' tikhomirov@57: } tikhomirov@22: tikhomirov@22: private static String todoGenerateFlags(String fname) { tikhomirov@22: // FIXME implement tikhomirov@22: return null; tikhomirov@22: } tikhomirov@22: private static boolean areTheSame(File f, byte[] data) { tikhomirov@22: try { tikhomirov@22: BufferedInputStream is = new BufferedInputStream(new FileInputStream(f)); tikhomirov@22: int i = 0; tikhomirov@22: while (i < data.length && data[i] == is.read()) { tikhomirov@22: i++; // increment only for successful match, otherwise won't tell last byte in data was the same as read from the stream tikhomirov@22: } tikhomirov@22: return i == data.length && is.read() == -1; // although data length is expected to be the same (see caller), check that we reached EOF, no more data left. tikhomirov@22: } catch (IOException ex) { tikhomirov@22: ex.printStackTrace(); // log warn tikhomirov@22: } tikhomirov@22: return false; tikhomirov@22: } tikhomirov@22: tikhomirov@10: // XXX package-local, unless there are cases when required from outside (guess, working dir/revision walkers may hide dirstate access and no public visibility needed) tikhomirov@10: public final HgDirstate loadDirstate() { tikhomirov@10: // XXX may cache in SoftReference if creation is expensive tikhomirov@10: return new HgDirstate(this, new File(repoDir, "dirstate")); tikhomirov@10: } tikhomirov@10: tikhomirov@15: // package-local, see comment for loadDirstate tikhomirov@15: public final HgIgnore loadIgnore() { tikhomirov@15: return new HgIgnore(this); tikhomirov@15: } tikhomirov@15: tikhomirov@10: /*package-local*/ DataAccessProvider getDataAccess() { tikhomirov@10: return dataAccess; tikhomirov@10: } tikhomirov@15: tikhomirov@15: /*package-local*/ File getRepositoryRoot() { tikhomirov@15: return repoDir; tikhomirov@15: } tikhomirov@10: tikhomirov@50: @Override tikhomirov@50: protected HgTags createTags() { tikhomirov@50: return new HgTags(); tikhomirov@50: } tikhomirov@50: tikhomirov@3: private final HashMap> streamsCache = new HashMap>(); tikhomirov@3: tikhomirov@3: /** tikhomirov@3: * path - repository storage path (i.e. one usually with .i or .d) tikhomirov@3: */ tikhomirov@3: @Override tikhomirov@3: protected RevlogStream resolve(String path) { tikhomirov@3: final SoftReference ref = streamsCache.get(path); tikhomirov@3: RevlogStream cached = ref == null ? null : ref.get(); tikhomirov@3: if (cached != null) { tikhomirov@3: return cached; tikhomirov@3: } tikhomirov@3: File f = new File(repoDir, path); tikhomirov@3: if (f.exists()) { tikhomirov@10: RevlogStream s = new RevlogStream(dataAccess, f); tikhomirov@3: streamsCache.put(path, new SoftReference(s)); tikhomirov@3: return s; tikhomirov@3: } tikhomirov@3: return null; tikhomirov@3: } tikhomirov@3: tikhomirov@3: @Override tikhomirov@3: public HgDataFile getFileNode(String path) { tikhomirov@3: String nPath = normalize(path); tikhomirov@8: String storagePath = toStoragePath(nPath, true); tikhomirov@3: RevlogStream content = resolve(storagePath); tikhomirov@3: // XXX no content when no file? or HgDataFile.exists() to detect that? How about files that were removed in previous releases? tikhomirov@3: return new HgDataFile(this, nPath, content); tikhomirov@3: } tikhomirov@8: tikhomirov@8: private boolean revlogv1; tikhomirov@8: private boolean store; tikhomirov@8: private boolean fncache; tikhomirov@8: private boolean dotencode; tikhomirov@3: tikhomirov@8: tikhomirov@8: private void parseRequires() { tikhomirov@8: File requiresFile = new File(repoDir, "requires"); tikhomirov@8: if (!requiresFile.exists()) { tikhomirov@8: return; tikhomirov@8: } tikhomirov@8: try { tikhomirov@8: BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(requiresFile))); tikhomirov@8: String line; tikhomirov@8: while ((line = br.readLine()) != null) { tikhomirov@8: revlogv1 |= "revlogv1".equals(line); tikhomirov@8: store |= "store".equals(line); tikhomirov@8: fncache |= "fncache".equals(line); tikhomirov@8: dotencode |= "dotencode".equals(line); tikhomirov@8: } tikhomirov@8: } catch (IOException ex) { tikhomirov@8: ex.printStackTrace(); // FIXME log tikhomirov@8: } tikhomirov@8: } tikhomirov@8: tikhomirov@9: // FIXME document what path argument is, whether it includes .i or .d, and whether it's 'normalized' (slashes) or not. tikhomirov@9: // since .hg/store keeps both .i files and files without extension (e.g. fncache), guees, for data == false tikhomirov@9: // we shall assume path has extension tikhomirov@3: // FIXME much more to be done, see store.py:_hybridencode tikhomirov@8: // @see http://mercurial.selenic.com/wiki/CaseFoldingPlan tikhomirov@9: @Override tikhomirov@8: protected String toStoragePath(String path, boolean data) { tikhomirov@8: path = normalize(path); tikhomirov@8: final String STR_STORE = "store/"; tikhomirov@8: final String STR_DATA = "data/"; tikhomirov@8: final String STR_DH = "dh/"; tikhomirov@8: if (!data) { tikhomirov@8: return this.store ? STR_STORE + path : path; tikhomirov@8: } tikhomirov@8: path = path.replace(".hg/", ".hg.hg/").replace(".i/", ".i.hg/").replace(".d/", ".d.hg/"); tikhomirov@8: StringBuilder sb = new StringBuilder(path.length() << 1); tikhomirov@8: if (store || fncache) { tikhomirov@8: // encodefilename tikhomirov@8: final String reservedChars = "\\:*?\"<>|"; tikhomirov@8: // in fact, \\ is unlikely to match, ever - we've replaced all of them already, above. Just regards to store.py tikhomirov@8: int x; tikhomirov@8: char[] hexByte = new char[2]; tikhomirov@8: for (int i = 0; i < path.length(); i++) { tikhomirov@8: final char ch = path.charAt(i); tikhomirov@8: if (ch >= 'a' && ch <= 'z') { tikhomirov@8: sb.append(ch); // POIRAE tikhomirov@8: } else if (ch >= 'A' && ch <= 'Z') { tikhomirov@8: sb.append('_'); tikhomirov@8: sb.append(Character.toLowerCase(ch)); // Perhaps, (char) (((int) ch) + 32)? Even better, |= 0x20? tikhomirov@8: } else if ( (x = reservedChars.indexOf(ch)) != -1) { tikhomirov@8: sb.append('~'); tikhomirov@8: sb.append(toHexByte(reservedChars.charAt(x), hexByte)); tikhomirov@8: } else if ((ch >= '~' /*126*/ && ch <= 255) || ch < ' ' /*32*/) { tikhomirov@8: sb.append('~'); tikhomirov@8: sb.append(toHexByte(ch, hexByte)); tikhomirov@8: } else if (ch == '_') { tikhomirov@8: // note, encoding from store.py:_buildencodefun and :_build_lower_encodefun tikhomirov@8: // differ in the way they process '_' (latter doesn't escape it) tikhomirov@8: sb.append('_'); tikhomirov@8: sb.append('_'); tikhomirov@8: } else { tikhomirov@8: sb.append(ch); tikhomirov@8: } tikhomirov@8: } tikhomirov@8: // auxencode tikhomirov@8: if (fncache) { tikhomirov@8: x = 0; // last segment start tikhomirov@8: final TreeSet windowsReservedFilenames = new TreeSet(); tikhomirov@8: windowsReservedFilenames.addAll(Arrays.asList("con prn aux nul com1 com2 com3 com4 com5 com6 com7 com8 com9 lpt1 lpt2 lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9".split(" "))); tikhomirov@8: do { tikhomirov@8: int i = sb.indexOf("/", x); tikhomirov@8: if (i == -1) { tikhomirov@8: i = sb.length(); tikhomirov@8: } tikhomirov@8: // windows reserved filenames are at least of length 3 tikhomirov@8: if (i - x >= 3) { tikhomirov@8: boolean found = false; tikhomirov@8: if (i-x == 3) { tikhomirov@8: found = windowsReservedFilenames.contains(sb.subSequence(x, i)); tikhomirov@8: } else if (sb.charAt(x+3) == '.') { // implicit i-x > 3 tikhomirov@8: found = windowsReservedFilenames.contains(sb.subSequence(x, x+3)); tikhomirov@8: } else if (i-x > 4 && sb.charAt(x+4) == '.') { tikhomirov@8: found = windowsReservedFilenames.contains(sb.subSequence(x, x+4)); tikhomirov@8: } tikhomirov@8: if (found) { tikhomirov@8: sb.setCharAt(x, '~'); tikhomirov@8: sb.insert(x+1, toHexByte(sb.charAt(x+2), hexByte)); tikhomirov@8: i += 2; tikhomirov@8: } tikhomirov@8: } tikhomirov@8: if (dotencode && (sb.charAt(x) == '.' || sb.charAt(x) == ' ')) { tikhomirov@8: sb.insert(x+1, toHexByte(sb.charAt(x), hexByte)); tikhomirov@8: sb.setCharAt(x, '~'); // setChar *after* charAt/insert to get ~2e, not ~7e for '.' tikhomirov@8: i += 2; tikhomirov@8: } tikhomirov@8: x = i+1; tikhomirov@8: } while (x < sb.length()); tikhomirov@8: } tikhomirov@8: } tikhomirov@8: final int MAX_PATH_LEN_IN_HGSTORE = 120; tikhomirov@8: if (fncache && (sb.length() + STR_DATA.length() > MAX_PATH_LEN_IN_HGSTORE)) { tikhomirov@8: throw HgRepository.notImplemented(); // FIXME digest and fncache use tikhomirov@8: } tikhomirov@8: if (this.store) { tikhomirov@8: sb.insert(0, STR_STORE + STR_DATA); tikhomirov@8: } tikhomirov@8: sb.append(".i"); tikhomirov@8: return sb.toString(); tikhomirov@8: } tikhomirov@8: tikhomirov@8: private static char[] toHexByte(int ch, char[] buf) { tikhomirov@8: assert buf.length > 1; tikhomirov@8: final String hexDigits = "0123456789abcdef"; tikhomirov@9: buf[0] = hexDigits.charAt((ch & 0x00F0) >>> 4); tikhomirov@8: buf[1] = hexDigits.charAt(ch & 0x0F); tikhomirov@8: return buf; tikhomirov@3: } tikhomirov@3: tikhomirov@9: // TODO handle . and .. (although unlikely to face them from GUI client) tikhomirov@3: private static String normalize(String path) { tikhomirov@8: path = path.replace('\\', '/').replace("//", "/"); tikhomirov@8: if (path.startsWith("/")) { tikhomirov@8: path = path.substring(1); tikhomirov@8: } tikhomirov@8: return path; tikhomirov@3: } tikhomirov@1: }