tikhomirov@10: /* tikhomirov@526: * Copyright (c) 2010-2013 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@0: */ tikhomirov@74: package org.tmatesoft.hg.repo; tikhomirov@74: tikhomirov@482: import static org.tmatesoft.hg.repo.HgRepositoryFiles.*; tikhomirov@456: import static org.tmatesoft.hg.util.LogFacility.Severity.*; tikhomirov@456: tikhomirov@74: import java.io.File; tikhomirov@481: import java.io.FileReader; tikhomirov@74: import java.io.IOException; tikhomirov@234: import java.io.StringReader; tikhomirov@74: import java.lang.ref.SoftReference; tikhomirov@481: import java.nio.CharBuffer; tikhomirov@114: import java.util.ArrayList; tikhomirov@114: import java.util.Collections; tikhomirov@74: import java.util.HashMap; tikhomirov@114: import java.util.List; tikhomirov@0: tikhomirov@235: import org.tmatesoft.hg.core.Nodeid; tikhomirov@295: import org.tmatesoft.hg.core.SessionContext; tikhomirov@234: import org.tmatesoft.hg.internal.ByteArrayChannel; tikhomirov@114: import org.tmatesoft.hg.internal.ConfigFile; tikhomirov@526: import org.tmatesoft.hg.internal.DirstateReader; tikhomirov@486: import org.tmatesoft.hg.internal.Experimental; tikhomirov@114: import org.tmatesoft.hg.internal.Filter; tikhomirov@407: import org.tmatesoft.hg.internal.Internals; tikhomirov@504: import org.tmatesoft.hg.internal.PropertyMarshal; tikhomirov@77: import org.tmatesoft.hg.internal.RevlogStream; tikhomirov@239: import org.tmatesoft.hg.internal.SubrepoManager; tikhomirov@501: import org.tmatesoft.hg.repo.ext.HgExtensionsManager; tikhomirov@234: import org.tmatesoft.hg.util.CancelledException; tikhomirov@235: import org.tmatesoft.hg.util.Pair; tikhomirov@133: import org.tmatesoft.hg.util.Path; tikhomirov@64: import org.tmatesoft.hg.util.PathRewrite; tikhomirov@220: import org.tmatesoft.hg.util.ProgressSupport; tikhomirov@64: tikhomirov@1: tikhomirov@74: tikhomirov@0: /** tikhomirov@64: * Shall be as state-less as possible, all the caching happens outside the repo, in commands/walkers tikhomirov@74: * tikhomirov@74: * @author Artem Tikhomirov tikhomirov@74: * @author TMate Software Ltd. tikhomirov@0: */ tikhomirov@490: public final class HgRepository implements SessionContext.Source { tikhomirov@0: tikhomirov@405: // IMPORTANT: if new constants added, consider fixing HgInternals#wrongRevisionIndex and HgInvalidRevisionException#getMessage tikhomirov@405: tikhomirov@405: /** tikhomirov@405: * Revision index constant to indicate most recent revision tikhomirov@405: */ tikhomirov@405: public static final int TIP = -3; // XXX TIP_REVISION? tikhomirov@405: tikhomirov@405: /** tikhomirov@405: * Revision index constant to indicate invalid revision index value. tikhomirov@405: * Primary use is default/uninitialized values where user input is expected and as return value where tikhomirov@405: * an exception (e.g. {@link HgInvalidRevisionException}) is not desired tikhomirov@405: */ tikhomirov@403: public static final int BAD_REVISION = Integer.MIN_VALUE; // XXX INVALID_REVISION? tikhomirov@405: tikhomirov@405: /** tikhomirov@405: * Revision index constant to indicate working copy tikhomirov@405: */ tikhomirov@405: public static final int WORKING_COPY = -2; // XXX WORKING_COPY_REVISION? tikhomirov@252: tikhomirov@405: /** tikhomirov@423: * Constant ({@value #NO_REVISION}) to indicate revision absence or a fictitious revision of an empty repository. tikhomirov@423: * tikhomirov@423: *
Revision absence is vital e.g. for missing parent from {@link HgChangelog#parents(int, int[], byte[], byte[])} call and tikhomirov@423: * to report cases when changeset records no corresponding manifest tikhomirov@423: * revision {@link HgManifest#walk(int, int, org.tmatesoft.hg.repo.HgManifest.Inspector)}. tikhomirov@423: * tikhomirov@423: *
 Use as imaginary revision/empty repository is handy as an argument (contrary to {@link #BAD_REVISION})
tikhomirov@423: 	 * e.g in a status operation to visit changes from the very beginning of a repository.
tikhomirov@405: 	 */
tikhomirov@405: 	public static final int NO_REVISION = -1;
tikhomirov@405: 	
tikhomirov@405: 	/**
tikhomirov@405: 	 * Name of the primary branch, "default".
tikhomirov@405: 	 */
tikhomirov@252: 	public static final String DEFAULT_BRANCH_NAME = "default";
tikhomirov@5: 
tikhomirov@74: 	private final File repoDir; // .hg folder
tikhomirov@237: 	private final File workingDir; // .hg/../
tikhomirov@74: 	private final String repoLocation;
tikhomirov@493: 	/*
tikhomirov@493: 	 * normalized slashes but otherwise regular file names
tikhomirov@493: 	 * the only front-end path rewrite, kept here as rest of the library shall
tikhomirov@493: 	 * not bother with names normalization.
tikhomirov@493: 	 */
tikhomirov@493: 	private final PathRewrite normalizePath;
tikhomirov@295: 	private final SessionContext sessionContext;
tikhomirov@74: 
tikhomirov@97: 	private HgChangelog changelog;
tikhomirov@2: 	private HgManifest manifest;
tikhomirov@50: 	private HgTags tags;
tikhomirov@220: 	private HgBranches branches;
tikhomirov@231: 	private HgMergeState mergeState;
tikhomirov@239: 	private SubrepoManager subRepos;
tikhomirov@484: 	private HgBookmarks bookmarks;
tikhomirov@501: 	private HgExtensionsManager extManager;
tikhomirov@220: 
tikhomirov@74: 	// XXX perhaps, shall enable caching explicitly
tikhomirov@74: 	private final HashMap  It's important to understand this is purely descriptive attribute, it's kept as close as possible to 
tikhomirov@491: 	 * original value users supply to {@link HgLookup}. To get actual repository location, use methods that 
tikhomirov@491: 	 * provide {@link File}, e.g. {@link #getWorkingDir()}
tikhomirov@491: 	 * 
tikhomirov@491: 	 * @return repository location information, never true) and serves 
tikhomirov@491: 	 * as an extra description of the failure.
tikhomirov@491: 	 * 
tikhomirov@491: 	 * null
tikhomirov@491: 	 */
tikhomirov@74: 	public String getLocation() {
tikhomirov@74: 		return repoLocation;
tikhomirov@74: 	}
tikhomirov@74: 
tikhomirov@74: 	public boolean isInvalid() {
tikhomirov@490: 		return impl == null || impl.isInvalid();
tikhomirov@74: 	}
tikhomirov@74: 	
tikhomirov@97: 	public HgChangelog getChangelog() {
tikhomirov@388: 		if (changelog == null) {
tikhomirov@493: 			File chlogFile = impl.getFileFromStoreDir("00changelog.i");
tikhomirov@493: 			RevlogStream content = new RevlogStream(impl.getDataAccess(), chlogFile);
tikhomirov@388: 			changelog = new HgChangelog(this, content);
tikhomirov@0: 		}
tikhomirov@388: 		return changelog;
tikhomirov@0: 	}
tikhomirov@2: 	
tikhomirov@74: 	public HgManifest getManifest() {
tikhomirov@388: 		if (manifest == null) {
tikhomirov@493: 			File manifestFile = impl.getFileFromStoreDir("00manifest.i");
tikhomirov@493: 			RevlogStream content = new RevlogStream(impl.getDataAccess(), manifestFile);
tikhomirov@412: 			manifest = new HgManifest(this, content, impl.buildFileNameEncodingHelper());
tikhomirov@2: 		}
tikhomirov@388: 		return manifest;
tikhomirov@2: 	}
tikhomirov@50: 	
tikhomirov@482: 	/**
tikhomirov@482: 	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception
tikhomirov@482: 	 */
tikhomirov@318: 	public HgTags getTags() throws HgInvalidControlFileException {
tikhomirov@50: 		if (tags == null) {
tikhomirov@234: 			tags = new HgTags(this);
tikhomirov@482: 			HgDataFile hgTags = getFileNode(HgTags.getPath());
tikhomirov@318: 			if (hgTags.exists()) {
tikhomirov@418: 				for (int i = 0; i <= hgTags.getLastRevision(); i++) { // TODO post-1.0 in fact, would be handy to have walk(start,end) 
tikhomirov@318: 					// method for data files as well, though it looks odd.
tikhomirov@318: 					try {
tikhomirov@318: 						ByteArrayChannel sink = new ByteArrayChannel();
tikhomirov@318: 						hgTags.content(i, sink);
tikhomirov@318: 						final String content = new String(sink.toArray(), "UTF8");
tikhomirov@318: 						tags.readGlobal(new StringReader(content));
tikhomirov@318: 					} catch (CancelledException ex) {
tikhomirov@318: 						 // IGNORE, can't happen, we did not configure cancellation
tikhomirov@490: 						getSessionContext().getLog().dump(getClass(), Debug, ex, null);
tikhomirov@318: 					} catch (IOException ex) {
tikhomirov@318: 						// UnsupportedEncodingException can't happen (UTF8)
tikhomirov@318: 						// only from readGlobal. Need to reconsider exceptions thrown from there:
tikhomirov@318: 						// BufferedReader wraps String and unlikely to throw IOException, perhaps, log is enough?
tikhomirov@490: 						getSessionContext().getLog().dump(getClass(), Error, ex, null);
tikhomirov@318: 						// XXX need to decide what to do this. failure to read single revision shall not break complete cycle
tikhomirov@234: 					}
tikhomirov@234: 				}
tikhomirov@318: 			}
tikhomirov@318: 			File file2read = null;
tikhomirov@318: 			try {
tikhomirov@482: 				file2read = new File(getWorkingDir(), HgTags.getPath());
tikhomirov@318: 				tags.readGlobal(file2read); // XXX replace with HgDataFile.workingCopy
tikhomirov@490: 				file2read = impl.getFileFromRepoDir(HgLocalTags.getName()); // XXX pass internalrepo to readLocal, keep filename there
tikhomirov@318: 				tags.readLocal(file2read);
tikhomirov@104: 			} catch (IOException ex) {
tikhomirov@490: 				getSessionContext().getLog().dump(getClass(), Error, ex, null);
tikhomirov@318: 				throw new HgInvalidControlFileException("Failed to read tags", ex, file2read);
tikhomirov@104: 			}
tikhomirov@50: 		}
tikhomirov@50: 		return tags;
tikhomirov@50: 	}
tikhomirov@50: 	
tikhomirov@482: 	/**
tikhomirov@484: 	 * Access branch information
tikhomirov@484: 	 * @return branch manager instance, never null
tikhomirov@482: 	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception
tikhomirov@482: 	 */
tikhomirov@366: 	public HgBranches getBranches() throws HgInvalidControlFileException {
tikhomirov@220: 		if (branches == null) {
tikhomirov@490: 			branches = new HgBranches(impl);
tikhomirov@220: 			branches.collect(ProgressSupport.Factory.get(null));
tikhomirov@220: 		}
tikhomirov@220: 		return branches;
tikhomirov@220: 	}
tikhomirov@231: 
tikhomirov@484: 	/**
tikhomirov@484: 	 * Access state of the recent merge
tikhomirov@484: 	 * @return merge state facility, never null 
tikhomirov@484: 	 */
tikhomirov@231: 	public HgMergeState getMergeState() {
tikhomirov@231: 		if (mergeState == null) {
tikhomirov@490: 			mergeState = new HgMergeState(impl);
tikhomirov@231: 		}
tikhomirov@231: 		return mergeState;
tikhomirov@231: 	}
tikhomirov@220: 	
tikhomirov@74: 	public HgDataFile getFileNode(String path) {
tikhomirov@292: 		CharSequence nPath = normalizePath.rewrite(path);
tikhomirov@115: 		Path p = Path.create(nPath);
tikhomirov@493: 		return getFileNode(p);
tikhomirov@74: 	}
tikhomirov@1: 
tikhomirov@74: 	public HgDataFile getFileNode(Path path) {
tikhomirov@493: 		RevlogStream content = resolveStoreFile(path);
tikhomirov@115: 		if (content == null) {
tikhomirov@115: 			return new HgDataFile(this, path);
tikhomirov@115: 		}
tikhomirov@74: 		return new HgDataFile(this, path, content);
tikhomirov@74: 	}
tikhomirov@2: 
tikhomirov@142: 	/* clients need to rewrite path from their FS to a repository-friendly paths, and, perhaps, vice versa*/
tikhomirov@142: 	public PathRewrite getToRepoPathHelper() {
tikhomirov@74: 		return normalizePath;
tikhomirov@74: 	}
tikhomirov@284: 
tikhomirov@284: 	/**
tikhomirov@348: 	 * @return pair of values, {@link Pair#first()} and {@link Pair#second()} are respective parents, never null.
tikhomirov@348: 	 * @throws HgInvalidControlFileException if attempt to read information about working copy parents from dirstate failed 
tikhomirov@284: 	 */
tikhomirov@348: 	public Pairnull.
tikhomirov@348: 	 * @throws HgInvalidControlFileException if attempt to read branch name failed.
tikhomirov@252: 	 */
tikhomirov@348: 	public String getWorkingCopyBranchName() throws HgInvalidControlFileException {
tikhomirov@430: 		if (wcBranch == null) {
tikhomirov@526: 			wcBranch = DirstateReader.readBranch(impl);
tikhomirov@430: 		}
tikhomirov@430: 		return wcBranch;
tikhomirov@252: 	}
tikhomirov@2: 
tikhomirov@237: 	/**
tikhomirov@237: 	 * @return location where user files (shall) reside
tikhomirov@237: 	 */
tikhomirov@237: 	public File getWorkingDir() {
tikhomirov@237: 		return workingDir;
tikhomirov@237: 	}
tikhomirov@239: 	
tikhomirov@239: 	/**
tikhomirov@239: 	 * Provides access to sub-repositories defined in this repository. Enumerated  sub-repositories are those directly
tikhomirov@239: 	 * known, not recursive collection of all nested sub-repositories.
tikhomirov@239: 	 * @return list of all known sub-repositories in this repository, or empty list if none found.
tikhomirov@482: 	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception
tikhomirov@239: 	 */
tikhomirov@348: 	public Listnull if none
tikhomirov@482: 	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception
tikhomirov@481: 	 */
tikhomirov@482: 	public String getCommitLastMessage() throws HgInvalidControlFileException {
tikhomirov@490: 		File lastMessage = impl.getFileFromRepoDir(LastMessage.getPath());
tikhomirov@481: 		if (!lastMessage.canRead()) {
tikhomirov@481: 			return null;
tikhomirov@481: 		}
tikhomirov@481: 		FileReader fr = null;
tikhomirov@481: 		try {
tikhomirov@481: 			fr = new FileReader(lastMessage);
tikhomirov@481: 			CharBuffer cb = CharBuffer.allocate(Internals.ltoi(lastMessage.length()));
tikhomirov@481: 			fr.read(cb);
tikhomirov@481: 			return cb.flip().toString();
tikhomirov@481: 		} catch (IOException ex) {
tikhomirov@481: 			throw new HgInvalidControlFileException("Can't retrieve message of last commit attempt", ex, lastMessage);
tikhomirov@481: 		} finally {
tikhomirov@481: 			if (fr != null) {
tikhomirov@481: 				try {
tikhomirov@481: 					fr.close();
tikhomirov@481: 				} catch (IOException ex) {
tikhomirov@490: 					getSessionContext().getLog().dump(getClass(), Warn, "Failed to close %s after read", lastMessage);
tikhomirov@481: 				}
tikhomirov@481: 			}
tikhomirov@481: 		}
tikhomirov@481: 	}
tikhomirov@486: 
tikhomirov@487: 	private HgRepositoryLock wdLock, storeLock;
tikhomirov@486: 
tikhomirov@486: 	/**
tikhomirov@486: 	 * PROVISIONAL CODE, DO NOT USE
tikhomirov@486: 	 * 
tikhomirov@486: 	 * Access repository lock that covers non-store parts of the repository (dirstate, branches, etc - 
tikhomirov@486: 	 * everything that has to do with working directory state).
tikhomirov@486: 	 * 
tikhomirov@486: 	 * Note, the lock object returned merely gives access to lock mechanism. NO ACTUAL LOCKING IS DONE.
tikhomirov@487: 	 * Use {@link HgRepositoryLock#acquire()} to actually lock the repository.  
tikhomirov@486: 	 *   
tikhomirov@486: 	 * @return lock object, never null
tikhomirov@486: 	 */
tikhomirov@486: 	@Experimental(reason="WORK IN PROGRESS")
tikhomirov@487: 	public HgRepositoryLock getWorkingDirLock() {
tikhomirov@486: 		if (wdLock == null) {
tikhomirov@487: 			int timeout = getLockTimeout();
tikhomirov@490: 			File lf = impl.getFileFromRepoDir("wlock");
tikhomirov@486: 			synchronized (this) {
tikhomirov@486: 				if (wdLock == null) {
tikhomirov@488: 					wdLock = new HgRepositoryLock(lf, timeout);
tikhomirov@486: 				}
tikhomirov@486: 			}
tikhomirov@486: 		}
tikhomirov@486: 		return wdLock;
tikhomirov@486: 	}
tikhomirov@486: 
tikhomirov@486: 	@Experimental(reason="WORK IN PROGRESS")
tikhomirov@487: 	public HgRepositoryLock getStoreLock() {
tikhomirov@486: 		if (storeLock == null) {
tikhomirov@487: 			int timeout = getLockTimeout();
tikhomirov@493: 			File fl = impl.getFileFromStoreDir("lock");
tikhomirov@486: 			synchronized (this) {
tikhomirov@486: 				if (storeLock == null) {
tikhomirov@488: 					storeLock = new HgRepositoryLock(fl, timeout);
tikhomirov@486: 				}
tikhomirov@486: 			}
tikhomirov@486: 		}
tikhomirov@486: 		return storeLock;
tikhomirov@486: 	}
tikhomirov@486: 
tikhomirov@484: 	/**
tikhomirov@484: 	 * Access bookmarks-related functionality
tikhomirov@484: 	 * @return facility to manage bookmarks, never null
tikhomirov@484: 	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception
tikhomirov@484: 	 */
tikhomirov@484: 	public HgBookmarks getBookmarks() throws HgInvalidControlFileException {
tikhomirov@484: 		if (bookmarks == null) {
tikhomirov@490: 			bookmarks = new HgBookmarks(impl);
tikhomirov@484: 			bookmarks.read();
tikhomirov@484: 		}
tikhomirov@484: 		return bookmarks;
tikhomirov@484: 	}
tikhomirov@501: 	
tikhomirov@501: 	public HgExtensionsManager getExtensions() {
tikhomirov@501: 		if (extManager == null) {
tikhomirov@501: 			class EM extends HgExtensionsManager {
tikhomirov@501: 				EM() {
tikhomirov@501: 					super(HgRepository.this.getImplHelper());
tikhomirov@501: 				}
tikhomirov@501: 			}
tikhomirov@501: 			extManager = new EM();
tikhomirov@501: 		}
tikhomirov@501: 		return extManager;
tikhomirov@501: 	}
tikhomirov@74: 
tikhomirov@490: 	/**
tikhomirov@490: 	 * @return session environment of the repository
tikhomirov@490: 	 */
tikhomirov@490: 	public SessionContext getSessionContext() {
tikhomirov@490: 		return sessionContext;
tikhomirov@74: 	}
tikhomirov@74: 
tikhomirov@2: 	/**
tikhomirov@2: 	 * Perhaps, should be separate interface, like ContentLookup
tikhomirov@493: 	 * @param path - normalized file name
tikhomirov@493: 	 * @return null if path doesn't resolve to a existing file
tikhomirov@2: 	 */
tikhomirov@493: 	/*package-local*/ RevlogStream resolveStoreFile(Path path) {
tikhomirov@74: 		final SoftReference