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@481: import java.nio.CharBuffer; tikhomirov@114: import java.util.ArrayList; tikhomirov@114: import java.util.Collections; 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@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@388: private final org.tmatesoft.hg.internal.Internals impl;
tikhomirov@91: private HgIgnore ignore;
tikhomirov@331: private HgRepoConfig repoConfig;
tikhomirov@220:
tikhomirov@430: /*
tikhomirov@430: * TODO [post-1.0] move to a better place, e.g. WorkingCopy container that tracks both dirstate and branches
tikhomirov@430: * (and, perhaps, undo, lastcommit and other similar information), and is change listener so that we don't need to
tikhomirov@430: * worry about this cached value become stale
tikhomirov@430: */
tikhomirov@430: private String wcBranch;
tikhomirov@430:
tikhomirov@430:
tikhomirov@74: HgRepository(String repositoryPath) {
tikhomirov@237: workingDir = null;
tikhomirov@74: repoLocation = repositoryPath;
tikhomirov@142: normalizePath = null;
tikhomirov@295: sessionContext = null;
tikhomirov@388: impl = null;
tikhomirov@1: }
tikhomirov@1:
tikhomirov@425: /**
tikhomirov@425: * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception
tikhomirov@425: */
tikhomirov@425: HgRepository(SessionContext ctx, String repositoryPath, File repositoryRoot) throws HgRuntimeException {
tikhomirov@74: assert ".hg".equals(repositoryRoot.getName()) && repositoryRoot.isDirectory();
tikhomirov@148: assert repositoryPath != null;
tikhomirov@148: assert repositoryRoot != null;
tikhomirov@295: assert ctx != null;
tikhomirov@591: workingDir = repositoryRoot.getParentFile();
tikhomirov@237: if (workingDir == null) {
tikhomirov@591: throw new IllegalArgumentException(repositoryRoot.toString());
tikhomirov@237: }
tikhomirov@148: repoLocation = repositoryPath;
tikhomirov@295: sessionContext = ctx;
tikhomirov@591: impl = new Internals(this, repositoryRoot, new Internals.ImplAccess() {
tikhomirov@591:
tikhomirov@591: public RevlogStream getStream(HgDataFile df) {
tikhomirov@591: return df.content;
tikhomirov@591: }
tikhomirov@591: public RevlogStream getManifestStream() {
tikhomirov@591: return HgRepository.this.getManifest().content;
tikhomirov@591: }
tikhomirov@591: public RevlogStream getChangelogStream() {
tikhomirov@591: return HgRepository.this.getChangelog().content;
tikhomirov@591: }
tikhomirov@591: });
tikhomirov@388: normalizePath = impl.buildNormalizePathRewrite();
tikhomirov@1: }
tikhomirov@0:
tikhomirov@145: @Override
tikhomirov@145: public String toString() {
tikhomirov@145: return getClass().getSimpleName() + "[" + getLocation() + (isInvalid() ? "(BAD)" : "") + "]";
tikhomirov@145: }
tikhomirov@74:
tikhomirov@491: /**
tikhomirov@491: * Path to repository which has been used to initialize this instance. The value is always present, even
tikhomirov@491: * if no repository has been found at that location ({@link #isInvalid()} is true
) and serves
tikhomirov@491: * as an extra description of the failure.
tikhomirov@491: *
tikhomirov@491: *
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 null
tikhomirov@491: */
tikhomirov@74: public String getLocation() {
tikhomirov@591: return repoLocation; // XXX field to keep this is bit too much
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@591: RevlogStream content = impl.createChangelogStream();
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@591: RevlogStream content = impl.createManifestStream();
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@571: Path p = sessionContext.getPathFactory().path(nPath);
tikhomirov@493: return getFileNode(p);
tikhomirov@74: }
tikhomirov@1:
tikhomirov@74: public HgDataFile getFileNode(Path path) {
tikhomirov@591: RevlogStream content = impl.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
tikhomirov@579: */
tikhomirov@331: public HgRepoConfig getConfiguration() /* XXX throws HgInvalidControlFileException? Description of the exception suggests it is only for files under ./hg/*/ {
tikhomirov@331: if (repoConfig == null) {
tikhomirov@331: try {
tikhomirov@490: ConfigFile configFile = impl.readConfiguration();
tikhomirov@331: repoConfig = new HgRepoConfig(configFile);
tikhomirov@331: } catch (IOException ex) {
tikhomirov@331: String m = "Errors while reading user configuration file";
tikhomirov@490: getSessionContext().getLog().dump(getClass(), Warn, ex, m);
tikhomirov@490: return new HgRepoConfig(new ConfigFile(getSessionContext())); // empty config, do not cache, allow to try once again
tikhomirov@331: //throw new HgInvalidControlFileException(m, ex, null);
tikhomirov@331: }
tikhomirov@331: }
tikhomirov@331: return repoConfig;
tikhomirov@331: }
tikhomirov@331:
tikhomirov@526: // There seem to be no cases when access to HgDirstate is required from outside
tikhomirov@526: // (guess, working dir/revision walkers may hide dirstate access and no public visibility needed)
tikhomirov@431: /*package-local*/ final HgDirstate loadDirstate(Path.Source pathFactory) throws HgInvalidControlFileException {
tikhomirov@292: PathRewrite canonicalPath = null;
tikhomirov@388: if (!impl.isCaseSensitiveFileSystem()) {
tikhomirov@292: canonicalPath = new PathRewrite() {
tikhomirov@292:
tikhomirov@292: public CharSequence rewrite(CharSequence path) {
tikhomirov@292: return path.toString().toLowerCase();
tikhomirov@292: }
tikhomirov@292: };
tikhomirov@292: }
tikhomirov@490: HgDirstate ds = new HgDirstate(impl, pathFactory, canonicalPath);
tikhomirov@490: ds.read();
tikhomirov@348: return ds;
tikhomirov@74: }
tikhomirov@74:
tikhomirov@289: /**
tikhomirov@289: * Access to configured set of ignored files.
tikhomirov@289: * @see HgIgnore#isIgnored(Path)
tikhomirov@482: * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception
tikhomirov@289: */
tikhomirov@482: public HgIgnore getIgnore() throws HgInvalidControlFileException {
tikhomirov@91: // TODO read config for additional locations
tikhomirov@91: if (ignore == null) {
tikhomirov@409: ignore = new HgIgnore(getToRepoPathHelper());
tikhomirov@482: File ignoreFile = new File(getWorkingDir(), HgIgnore.getPath());
tikhomirov@91: try {
tikhomirov@335: final 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@114:
tikhomirov@114: /*package-local*/ List