tikhomirov@10: /* tikhomirov@20: * Copyright (c) 2010, 2011 Artem Tikhomirov tikhomirov@1: */ tikhomirov@1: package com.tmate.hgkit.ll; tikhomirov@1: 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@3: import java.util.HashMap; tikhomirov@8: import java.util.TreeSet; tikhomirov@1: tikhomirov@64: import org.tmatesoft.hg.core.Path; tikhomirov@64: import org.tmatesoft.hg.util.PathRewrite; tikhomirov@64: tikhomirov@10: import com.tmate.hgkit.fs.DataAccessProvider; tikhomirov@58: import com.tmate.hgkit.fs.FileWalker; 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@64: private final PathRewrite normalizePath = new PathRewrite() { tikhomirov@64: tikhomirov@64: public String rewrite(String path) { tikhomirov@64: return normalize(path); tikhomirov@64: } tikhomirov@64: }; tikhomirov@1: tikhomirov@1: public LocalHgRepo(String repositoryPath) { tikhomirov@1: setInvalid(true); tikhomirov@1: repoLocation = repositoryPath; tikhomirov@58: 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@58: public FileWalker createWorkingDirWalker() { tikhomirov@58: return new FileWalker(repoDir.getParentFile()); tikhomirov@57: } tikhomirov@57: 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@59: final HgDirstate loadDirstate() { tikhomirov@59: return new HgDirstate(getDataAccess(), 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@64: @Override tikhomirov@64: public HgDataFile getFileNode(Path path) { tikhomirov@64: return getFileNode(path.toString()); tikhomirov@64: } tikhomirov@64: tikhomirov@64: @Override tikhomirov@64: public PathRewrite getPathHelper() { tikhomirov@64: return normalizePath; tikhomirov@64: } tikhomirov@64: 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: }