view src/org/tmatesoft/hg/repo/HgRepository.java @ 709:497e697636fc

Report merged lines as changed block if possible, not as a sequence of added/deleted blocks. To facilitate access to merge parent lines AddBlock got mergeLineAt() method that reports index of the line in the second parent (if any), while insertedAt() has been changed to report index in the first parent always
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Wed, 21 Aug 2013 16:23:27 +0200
parents 12a4f60ea972
children
line wrap: on
line source
/*
 * Copyright (c) 2010-2013 TMate Software Ltd
 *  
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * For information on how to redistribute this software under
 * the terms of a license other than GNU General Public License
 * contact TMate Software at support@hg4j.com
 */
package org.tmatesoft.hg.repo;

import static org.tmatesoft.hg.repo.HgRepositoryFiles.*;
import static org.tmatesoft.hg.util.LogFacility.Severity.Warn;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.CharBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.tmatesoft.hg.core.HgIOException;
import org.tmatesoft.hg.core.Nodeid;
import org.tmatesoft.hg.core.SessionContext;
import org.tmatesoft.hg.internal.ConfigFile;
import org.tmatesoft.hg.internal.DirstateReader;
import org.tmatesoft.hg.internal.FileUtils;
import org.tmatesoft.hg.internal.Filter;
import org.tmatesoft.hg.internal.Internals;
import org.tmatesoft.hg.internal.PropertyMarshal;
import org.tmatesoft.hg.internal.RevlogStream;
import org.tmatesoft.hg.internal.SubrepoManager;
import org.tmatesoft.hg.repo.ext.HgExtensionsManager;
import org.tmatesoft.hg.util.Pair;
import org.tmatesoft.hg.util.Path;
import org.tmatesoft.hg.util.PathRewrite;
import org.tmatesoft.hg.util.ProgressSupport;



/**
 * Shall be as state-less as possible, all the caching happens outside the repo, in commands/walkers
 * 
 * @author Artem Tikhomirov
 * @author TMate Software Ltd.
 */
public final class HgRepository implements SessionContext.Source {

	// IMPORTANT: if new constants added, consider fixing HgInternals#wrongRevisionIndex and HgInvalidRevisionException#getMessage

	/**
	 * Revision index constant to indicate most recent revision
	 */
	public static final int TIP = -3; // XXX TIP_REVISION?

	/**
	 * Revision index constant to indicate invalid revision index value. 
	 * Primary use is default/uninitialized values where user input is expected and as return value where 
	 * an exception (e.g. {@link HgInvalidRevisionException}) is not desired
	 */
	public static final int BAD_REVISION = Integer.MIN_VALUE; // XXX INVALID_REVISION?

	/**
	 * Revision index constant to indicate working copy
	 */
	public static final int WORKING_COPY = -2; // XXX WORKING_COPY_REVISION?
	
	/**
	 * Constant ({@value #NO_REVISION}) to indicate revision absence or a fictitious revision of an empty repository.
	 * 
	 * <p>Revision absence is vital e.g. for missing parent from {@link HgChangelog#parents(int, int[], byte[], byte[])} call and
	 * to report cases when changeset records no corresponding manifest 
	 * revision {@link HgManifest#walk(int, int, org.tmatesoft.hg.repo.HgManifest.Inspector)}.
	 * 
	 * <p> Use as imaginary revision/empty repository is handy as an argument (contrary to {@link #BAD_REVISION})
	 * e.g in a status operation to visit changes from the very beginning of a repository.
	 */
	public static final int NO_REVISION = -1;
	
	/**
	 * Name of the primary branch, "default".
	 */
	public static final String DEFAULT_BRANCH_NAME = "default";

	private final File workingDir; // .hg/../
	private final String repoLocation;
	/*
	 * normalized slashes but otherwise regular file names
	 * the only front-end path rewrite, kept here as rest of the library shall
	 * not bother with names normalization.
	 */
	private final PathRewrite normalizePath;
	private final SessionContext sessionContext;

	private HgChangelog changelog;
	private HgManifest manifest;
	private HgTags tags;
	private HgBranches branches;
	private HgMergeState mergeState;
	private SubrepoManager subRepos;
	private HgBookmarks bookmarks;
	private HgExtensionsManager extManager;
	private HgIgnore ignore;
	private HgRepoConfig repoConfig;
	
	private HgRepositoryLock wdLock, storeLock;

	private final org.tmatesoft.hg.internal.Internals impl;
	
	HgRepository(String repositoryPath) {
		workingDir = null;
		repoLocation = repositoryPath;
		normalizePath = null;
		sessionContext = null;
		impl = null;
	}
	
	/**
	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. <em>Runtime exception</em>
	 */
	HgRepository(SessionContext ctx, String repositoryPath, File repositoryRoot) throws HgRuntimeException {
		assert ".hg".equals(repositoryRoot.getName()) && repositoryRoot.isDirectory();
		assert repositoryPath != null; 
		assert repositoryRoot != null;
		assert ctx != null;
		workingDir = repositoryRoot.getParentFile();
		if (workingDir == null) {
			throw new IllegalArgumentException(repositoryRoot.toString());
		}
		repoLocation = repositoryPath;
		sessionContext = ctx;
		impl = new Internals(this, repositoryRoot, new Internals.ImplAccess() {
			
			public RevlogStream getStream(HgDataFile df) {
				return df.content;
			}
			public RevlogStream getManifestStream() {
				return HgRepository.this.getManifest().content;
			}
			public RevlogStream getChangelogStream() {
				return HgRepository.this.getChangelog().content;
			}
		});
		normalizePath = impl.buildNormalizePathRewrite();
	}

	@Override
	public String toString() {
		return getClass().getSimpleName() + "[" + getLocation() + (isInvalid() ? "(BAD)" : "") + "]";
	}                         
	
	/**
	 * Path to repository which has been used to initialize this instance. The value is always present, even 
	 * if no repository has been found at that location ({@link #isInvalid()} is <code>true</code>) and serves 
	 * as an extra description of the failure.
	 * 
	 * <p> It's important to understand this is purely descriptive attribute, it's kept as close as possible to 
	 * original value users supply to {@link HgLookup}. To get actual repository location, use methods that 
	 * provide {@link File}, e.g. {@link #getWorkingDir()}
	 * 
	 * @return repository location information, never <code>null</code>
	 */
	public String getLocation() {
		return repoLocation; // XXX field to keep this is bit too much 
	}

	public boolean isInvalid() {
		return impl == null || impl.isInvalid();
	}
	
	public HgChangelog getChangelog() {
		if (changelog == null) {
			RevlogStream content = impl.createChangelogStream();
			changelog = new HgChangelog(this, content);
		}
		return changelog;
	}
	
	public HgManifest getManifest() {
		if (manifest == null) {
			RevlogStream content = impl.createManifestStream();
			manifest = new HgManifest(this, content, impl.buildFileNameEncodingHelper());
		}
		return manifest;
	}
	
	/**
	 * Access snapshot of repository tags.
	 * 
	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. <em>Runtime exception</em>
	 */
	public HgTags getTags() throws HgRuntimeException {
		if (tags == null) {
			tags = new HgTags(impl);
			tags.read();
		} else {
			tags.reloadIfChanged();
		}
		return tags;
	}
	
	/**
	 * Access branch information. Returns a snapshot of branch information as it's available at the time of the call.
	 * If repository get changed, use this method to obtain an up-to-date state. 
	 * 
	 * @return branch manager instance, never <code>null</code>
	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. <em>Runtime exception</em>
	 */
	public HgBranches getBranches() throws HgRuntimeException {
		final ProgressSupport ps = ProgressSupport.Factory.get(null);
		if (branches == null) {
			branches = new HgBranches(impl);
			branches.collect(ps);
		} else {
			branches.reloadIfChanged(ps);
		}
		return branches;
	}

	/**
	 * Access state of the recent merge
	 * @return merge state facility, never <code>null</code> 
	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. <em>Runtime exception</em>
	 */
	public HgMergeState getMergeState() throws HgRuntimeException {
		if (mergeState == null) {
			mergeState = new HgMergeState(impl);
			mergeState.refresh();
		}
		return mergeState;
	}
	
	public HgDataFile getFileNode(String path) {
		CharSequence nPath = normalizePath.rewrite(path);
		Path p = sessionContext.getPathFactory().path(nPath);
		return getFileNode(p);
	}

	public HgDataFile getFileNode(Path path) {
		RevlogStream content = impl.resolveStoreFile(path);
		assert content != null;
		return new HgDataFile(this, path, content);
	}

	/* clients need to rewrite path from their FS to a repository-friendly paths, and, perhaps, vice versa*/
	public PathRewrite getToRepoPathHelper() {
		return normalizePath;
	}

	/**
	 * @return pair of values, {@link Pair#first()} and {@link Pair#second()} are respective parents, never <code>null</code>.
	 * @throws HgInvalidControlFileException if attempt to read information about working copy parents from dirstate failed 
	 */
	public Pair<Nodeid,Nodeid> getWorkingCopyParents() throws HgInvalidControlFileException {
		return DirstateReader.readParents(impl);
	}
	
	/**
	 * @return name of the branch associated with working directory, never <code>null</code>.
	 * @throws HgInvalidControlFileException if attempt to read branch name failed.
	 */
	public String getWorkingCopyBranchName() throws HgInvalidControlFileException {
		/*
		 * TODO [post-1.1] 1) cache value (now if cached, is not updated after commit)
		 * 2) move to a better place, e.g. WorkingCopy container that tracks both dirstate and branches 
		 * (and, perhaps, undo, lastcommit and other similar information), and is change listener so that we don't need to
		 * worry about this cached value become stale
		 */
		String wcBranch = DirstateReader.readBranch(impl);
		return wcBranch;
	}

	/**
	 * @return location where user files (shall) reside
	 */
	public File getWorkingDir() {
		return workingDir;
	}
	
	/**
	 * Provides access to sub-repositories defined in this repository. Enumerated  sub-repositories are those directly
	 * known, not recursive collection of all nested sub-repositories.
	 * @return list of all known sub-repositories in this repository, or empty list if none found.
	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. <em>Runtime exception</em>
	 */
	public List<HgSubrepoLocation> getSubrepositories() throws HgInvalidControlFileException {
		if (subRepos == null) {
			subRepos = new SubrepoManager(this);
			subRepos.read();
		}
		return subRepos.all();
	}

	
	/**
	 * Repository-specific configuration.
	 * @return access to configuration options, never <code>null</code>
	 */
	public HgRepoConfig getConfiguration() /* XXX throws HgInvalidControlFileException? Description of the exception suggests it is only for files under ./hg/*/ {
		if (repoConfig == null) {
			try {
				ConfigFile configFile = impl.readConfiguration();
				repoConfig = new HgRepoConfig(configFile);
			} catch (HgIOException ex) {
				String m = "Errors while reading user configuration file";
				getSessionContext().getLog().dump(getClass(), Warn, ex, m);
				return new HgRepoConfig(new ConfigFile(getSessionContext())); // empty config, do not cache, allow to try once again
				//throw new HgInvalidControlFileException(m, ex, null);
			}
		}
		return repoConfig;
	}

	// There seem to be no cases when access to HgDirstate is required from outside 
	// (guess, working dir/revision walkers may hide dirstate access and no public visibility needed)
	/*package-local*/ final HgDirstate loadDirstate(Path.Source pathFactory) throws HgInvalidControlFileException {
		PathRewrite canonicalPath = null;
		if (!impl.isCaseSensitiveFileSystem()) {
			canonicalPath = new PathRewrite() {

				public CharSequence rewrite(CharSequence path) {
					return path.toString().toLowerCase();
				}
			};
		}
		HgDirstate ds = new HgDirstate(impl, pathFactory, canonicalPath);
		ds.read();
		return ds;
	}

	/**
	 * Access to configured set of ignored files.
	 * @see HgIgnore#isIgnored(Path)
	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. <em>Runtime exception</em>
	 */
	public HgIgnore getIgnore() throws HgInvalidControlFileException {
		// TODO read config for additional locations
		if (ignore == null) {
			ignore = new HgIgnore(getToRepoPathHelper());
			ignore.read(impl);
		} else {
			ignore.reloadIfChanged(impl);
		}
		return ignore;
	}

	/**
	 * Mercurial saves message user has supplied for a commit to facilitate message re-use in case commit fails.
	 * This method provides this saved message.
	 *  
	 * @return message used for last commit attempt, or <code>null</code> if none
	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. <em>Runtime exception</em>
	 */
	public String getCommitLastMessage() throws HgInvalidControlFileException {
		File lastMessage = impl.getRepositoryFile(LastMessage);
		if (!lastMessage.canRead()) {
			return null;
		}
		FileReader fr = null;
		try {
			fr = new FileReader(lastMessage);
			CharBuffer cb = CharBuffer.allocate(Internals.ltoi(lastMessage.length()));
			fr.read(cb);
			return cb.flip().toString();
		} catch (IOException ex) {
			throw new HgInvalidControlFileException("Can't retrieve message of last commit attempt", ex, lastMessage);
		} finally {
			new FileUtils(getSessionContext().getLog(), this).closeQuietly(fr, lastMessage);
		}
	}

	/**
	 * Access repository lock that covers non-store parts of the repository (dirstate, branches, etc - 
	 * everything that has to do with working directory state).
	 * 
	 * Note, the lock object returned merely gives access to lock mechanism. NO ACTUAL LOCKING IS DONE.
	 * Use {@link HgRepositoryLock#acquire()} to actually lock the repository.  
	 *   
	 * @return lock object, never <code>null</code>
	 */
	public HgRepositoryLock getWorkingDirLock() {
		if (wdLock == null) {
			int timeout = getLockTimeout();
			File lf = impl.getRepositoryFile(WorkingCopyLock);
			synchronized (this) {
				if (wdLock == null) {
					wdLock = new HgRepositoryLock(lf, timeout);
				}
			}
		}
		return wdLock;
	}

	/**
	 * Access repository lock that covers repository intrinsic files, unrelated to 
	 * the state of working directory
	 * @return lock object, never <code>null</code>
	 */
	public HgRepositoryLock getStoreLock() {
		if (storeLock == null) {
			int timeout = getLockTimeout();
			File fl = impl.getRepositoryFile(StoreLock);
			synchronized (this) {
				if (storeLock == null) {
					storeLock = new HgRepositoryLock(fl, timeout);
				}
			}
		}
		return storeLock;
	}

	/**
	 * Access bookmarks-related functionality
	 * @return facility to manage bookmarks, never <code>null</code>
	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. <em>Runtime exception</em>
	 */
	public HgBookmarks getBookmarks() throws HgRuntimeException {
		if (bookmarks == null) {
			bookmarks = new HgBookmarks(impl);
			bookmarks.read();
		} else {
			bookmarks.reloadIfChanged();
		}
		return bookmarks;
	}
	
	public HgExtensionsManager getExtensions() {
		if (extManager == null) {
			class EM extends HgExtensionsManager {
				EM() {
					super(HgRepository.this.getImplHelper());
				}
			}
			extManager = new EM();
		}
		return extManager;
	}

	/**
	 * @return session environment of the repository
	 */
	public SessionContext getSessionContext() {
		return sessionContext;
	}
	
	/*package-local*/ List<Filter> getFiltersFromRepoToWorkingDir(Path p) {
		return instantiateFilters(p, new Filter.Options(Filter.Direction.FromRepo));
	}

	/*package-local*/ List<Filter> getFiltersFromWorkingDirToRepo(Path p) {
		return instantiateFilters(p, new Filter.Options(Filter.Direction.ToRepo));
	}
	
	/*package-local*/ File getFile(HgDataFile dataFile) {
		return new File(getWorkingDir(), dataFile.getPath().toString());
	}
	
	/*package-local*/ Internals getImplHelper() {
		return impl;
	}

	private List<Filter> instantiateFilters(Path p, Filter.Options opts) {
		List<Filter.Factory> factories = impl.getFilters();
		if (factories.isEmpty()) {
			return Collections.emptyList();
		}
		ArrayList<Filter> rv = new ArrayList<Filter>(factories.size());
		for (Filter.Factory ff : factories) {
			Filter f = ff.create(p, opts);
			if (f != null) {
				rv.add(f);
			}
		}
		return rv;
	}

	private int getLockTimeout() {
		int cfgValue = getConfiguration().getIntegerValue("ui", "timeout", 600);
		if (getSessionContext().getConfigurationProperty(Internals.CFG_PROPERTY_FS_LOCK_TIMEOUT, null) != null) {
			return new PropertyMarshal(sessionContext).getInt(Internals.CFG_PROPERTY_FS_LOCK_TIMEOUT, cfgValue);
		}
		return cfgValue;
	}
}