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: if (!chlogFile.exists()) {
tikhomirov@493: // fake its existence
tikhomirov@493: chlogFile = fakeNonExistentFile(chlogFile);
tikhomirov@493: }
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: if (!manifestFile.exists()) {
tikhomirov@493: manifestFile = fakeNonExistentFile(manifestFile);
tikhomirov@493: }
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