| /* |
| * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com> |
| * Copyright (C) 2008-2010, Google Inc. |
| * Copyright (C) 2006-2010, Robin Rosenberg <robin.rosenberg@dewire.com> |
| * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> |
| * and other copyright owners as documented in the project's IP log. |
| * |
| * This program and the accompanying materials are made available |
| * under the terms of the Eclipse Distribution License v1.0 which |
| * accompanies this distribution, is reproduced below, and is |
| * available at http://www.eclipse.org/org/documents/edl-v10.php |
| * |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or |
| * without modification, are permitted provided that the following |
| * conditions are met: |
| * |
| * - Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * |
| * - Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following |
| * disclaimer in the documentation and/or other materials provided |
| * with the distribution. |
| * |
| * - Neither the name of the Eclipse Foundation, Inc. nor the |
| * names of its contributors may be used to endorse or promote |
| * products derived from this software without specific prior |
| * written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
| * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
| * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
| * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
| * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
| * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
| * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| package org.eclipse.jgit.lib; |
| |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.Vector; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| import org.eclipse.jgit.JGitText; |
| import org.eclipse.jgit.dircache.DirCache; |
| import org.eclipse.jgit.errors.IncorrectObjectTypeException; |
| import org.eclipse.jgit.errors.RevisionSyntaxException; |
| import org.eclipse.jgit.revwalk.RevBlob; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevObject; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.eclipse.jgit.util.FS; |
| import org.eclipse.jgit.util.IO; |
| import org.eclipse.jgit.util.RawParseUtils; |
| |
| /** |
| * Represents a Git repository. |
| * <p> |
| * A repository holds all objects and refs used for managing source code (could |
| * be any type of file, but source code is what SCM's are typically used for). |
| * <p> |
| * This class is thread-safe. |
| */ |
| public abstract class Repository { |
| private final AtomicInteger useCnt = new AtomicInteger(1); |
| |
| /** Metadata directory holding the repository's critical files. */ |
| protected File gitDir; |
| |
| /** File abstraction used to resolve paths. */ |
| protected FS fs; |
| |
| private GitIndex index; |
| |
| private final List<RepositoryListener> listeners = new Vector<RepositoryListener>(); // thread safe |
| static private final List<RepositoryListener> allListeners = new Vector<RepositoryListener>(); // thread safe |
| |
| /** If not bare, the top level directory of the working files. */ |
| protected File workDir; |
| |
| /** If not bare, the index file caching the working file states. */ |
| protected File indexFile; |
| |
| /** Initialize a new repository instance. */ |
| protected Repository() { |
| // Empty constructor, defined protected to require subclassing. |
| } |
| |
| /** |
| * Create a new Git repository. |
| * <p> |
| * Repository with working tree is created using this method. This method is |
| * the same as {@code create(false)}. |
| * |
| * @throws IOException |
| * @see #create(boolean) |
| */ |
| public void create() throws IOException { |
| create(false); |
| } |
| |
| /** |
| * Create a new Git repository initializing the necessary files and |
| * directories. |
| * |
| * @param bare |
| * if true, a bare repository is created. |
| * |
| * @throws IOException |
| * in case of IO problem |
| */ |
| public abstract void create(boolean bare) throws IOException; |
| |
| /** |
| * @return GIT_DIR |
| */ |
| public File getDirectory() { |
| return gitDir; |
| } |
| |
| /** |
| * @return the directory containing the objects owned by this repository. |
| */ |
| public abstract File getObjectsDirectory(); |
| |
| /** |
| * @return the object database which stores this repository's data. |
| */ |
| public abstract ObjectDatabase getObjectDatabase(); |
| |
| /** @return a new inserter to create objects in {@link #getObjectDatabase()} */ |
| public ObjectInserter newObjectInserter() { |
| return getObjectDatabase().newInserter(); |
| } |
| |
| /** @return the reference database which stores the reference namespace. */ |
| public abstract RefDatabase getRefDatabase(); |
| |
| /** |
| * @return the configuration of this repository |
| */ |
| public abstract Config getConfig(); |
| |
| /** |
| * @return the used file system abstraction |
| */ |
| public FS getFS() { |
| return fs; |
| } |
| |
| /** |
| * Construct a filename where the loose object having a specified SHA-1 |
| * should be stored. If the object is stored in a shared repository the path |
| * to the alternative repo will be returned. If the object is not yet store |
| * a usable path in this repo will be returned. It is assumed that callers |
| * will look for objects in a pack first. |
| * |
| * @param objectId |
| * @return suggested file name |
| */ |
| public abstract File toFile(AnyObjectId objectId); |
| |
| /** |
| * @param objectId |
| * @return true if the specified object is stored in this repo or any of the |
| * known shared repositories. |
| */ |
| public boolean hasObject(AnyObjectId objectId) { |
| return getObjectDatabase().hasObject(objectId); |
| } |
| |
| /** |
| * @param id |
| * SHA-1 of an object. |
| * |
| * @return a {@link ObjectLoader} for accessing the data of the named |
| * object, or null if the object does not exist. |
| * @throws IOException |
| */ |
| public ObjectLoader openObject(final AnyObjectId id) |
| throws IOException { |
| final WindowCursor wc = new WindowCursor(); |
| try { |
| return openObject(wc, id); |
| } finally { |
| wc.release(); |
| } |
| } |
| |
| /** |
| * @param curs |
| * temporary working space associated with the calling thread. |
| * @param id |
| * SHA-1 of an object. |
| * |
| * @return a {@link ObjectLoader} for accessing the data of the named |
| * object, or null if the object does not exist. |
| * @throws IOException |
| */ |
| public ObjectLoader openObject(WindowCursor curs, AnyObjectId id) |
| throws IOException { |
| return getObjectDatabase().openObject(curs, id); |
| } |
| |
| /** |
| * Open object in all packs containing specified object. |
| * |
| * @param objectId |
| * id of object to search for |
| * @param curs |
| * temporary working space associated with the calling thread. |
| * @return collection of loaders for this object, from all packs containing |
| * this object |
| * @throws IOException |
| */ |
| public abstract Collection<PackedObjectLoader> openObjectInAllPacks( |
| AnyObjectId objectId, WindowCursor curs) |
| throws IOException; |
| |
| /** |
| * Open object in all packs containing specified object. |
| * |
| * @param objectId |
| * id of object to search for |
| * @param resultLoaders |
| * result collection of loaders for this object, filled with |
| * loaders from all packs containing specified object |
| * @param curs |
| * temporary working space associated with the calling thread. |
| * @throws IOException |
| */ |
| abstract void openObjectInAllPacks(final AnyObjectId objectId, |
| final Collection<PackedObjectLoader> resultLoaders, |
| final WindowCursor curs) throws IOException; |
| |
| /** |
| * @param id |
| * SHA'1 of a blob |
| * @return an {@link ObjectLoader} for accessing the data of a named blob |
| * @throws IOException |
| */ |
| public ObjectLoader openBlob(final ObjectId id) throws IOException { |
| return openObject(id); |
| } |
| |
| /** |
| * @param id |
| * SHA'1 of a tree |
| * @return an {@link ObjectLoader} for accessing the data of a named tree |
| * @throws IOException |
| */ |
| public ObjectLoader openTree(final ObjectId id) throws IOException { |
| return openObject(id); |
| } |
| |
| /** |
| * Access a Commit object using a symbolic reference. This reference may |
| * be a SHA-1 or ref in combination with a number of symbols translating |
| * from one ref or SHA1-1 to another, such as HEAD^ etc. |
| * |
| * @param revstr a reference to a git commit object |
| * @return a Commit named by the specified string |
| * @throws IOException for I/O error or unexpected object type. |
| * |
| * @see #resolve(String) |
| */ |
| public Commit mapCommit(final String revstr) throws IOException { |
| final ObjectId id = resolve(revstr); |
| return id != null ? mapCommit(id) : null; |
| } |
| |
| /** |
| * Access any type of Git object by id and |
| * |
| * @param id |
| * SHA-1 of object to read |
| * @param refName optional, only relevant for simple tags |
| * @return The Git object if found or null |
| * @throws IOException |
| */ |
| public Object mapObject(final ObjectId id, final String refName) throws IOException { |
| final ObjectLoader or = openObject(id); |
| if (or == null) |
| return null; |
| final byte[] raw = or.getBytes(); |
| switch (or.getType()) { |
| case Constants.OBJ_TREE: |
| return makeTree(id, raw); |
| |
| case Constants.OBJ_COMMIT: |
| return makeCommit(id, raw); |
| |
| case Constants.OBJ_TAG: |
| return makeTag(id, refName, raw); |
| |
| case Constants.OBJ_BLOB: |
| return raw; |
| |
| default: |
| throw new IncorrectObjectTypeException(id, |
| JGitText.get().incorrectObjectType_COMMITnorTREEnorBLOBnorTAG); |
| } |
| } |
| |
| /** |
| * Access a Commit by SHA'1 id. |
| * @param id |
| * @return Commit or null |
| * @throws IOException for I/O error or unexpected object type. |
| */ |
| public Commit mapCommit(final ObjectId id) throws IOException { |
| final ObjectLoader or = openObject(id); |
| if (or == null) |
| return null; |
| final byte[] raw = or.getBytes(); |
| if (Constants.OBJ_COMMIT == or.getType()) |
| return new Commit(this, id, raw); |
| throw new IncorrectObjectTypeException(id, Constants.TYPE_COMMIT); |
| } |
| |
| private Commit makeCommit(final ObjectId id, final byte[] raw) { |
| Commit ret = new Commit(this, id, raw); |
| return ret; |
| } |
| |
| /** |
| * Access a Tree object using a symbolic reference. This reference may |
| * be a SHA-1 or ref in combination with a number of symbols translating |
| * from one ref or SHA1-1 to another, such as HEAD^{tree} etc. |
| * |
| * @param revstr a reference to a git commit object |
| * @return a Tree named by the specified string |
| * @throws IOException |
| * |
| * @see #resolve(String) |
| */ |
| public Tree mapTree(final String revstr) throws IOException { |
| final ObjectId id = resolve(revstr); |
| return id != null ? mapTree(id) : null; |
| } |
| |
| /** |
| * Access a Tree by SHA'1 id. |
| * @param id |
| * @return Tree or null |
| * @throws IOException for I/O error or unexpected object type. |
| */ |
| public Tree mapTree(final ObjectId id) throws IOException { |
| final ObjectLoader or = openObject(id); |
| if (or == null) |
| return null; |
| final byte[] raw = or.getBytes(); |
| switch (or.getType()) { |
| case Constants.OBJ_TREE: |
| return new Tree(this, id, raw); |
| |
| case Constants.OBJ_COMMIT: |
| return mapTree(ObjectId.fromString(raw, 5)); |
| |
| default: |
| throw new IncorrectObjectTypeException(id, Constants.TYPE_TREE); |
| } |
| } |
| |
| private Tree makeTree(final ObjectId id, final byte[] raw) throws IOException { |
| Tree ret = new Tree(this, id, raw); |
| return ret; |
| } |
| |
| private Tag makeTag(final ObjectId id, final String refName, final byte[] raw) { |
| Tag ret = new Tag(this, id, refName, raw); |
| return ret; |
| } |
| |
| /** |
| * Access a tag by symbolic name. |
| * |
| * @param revstr |
| * @return a Tag or null |
| * @throws IOException on I/O error or unexpected type |
| */ |
| public Tag mapTag(String revstr) throws IOException { |
| final ObjectId id = resolve(revstr); |
| return id != null ? mapTag(revstr, id) : null; |
| } |
| |
| /** |
| * Access a Tag by SHA'1 id |
| * @param refName |
| * @param id |
| * @return Commit or null |
| * @throws IOException for I/O error or unexpected object type. |
| */ |
| public Tag mapTag(final String refName, final ObjectId id) throws IOException { |
| final ObjectLoader or = openObject(id); |
| if (or == null) |
| return null; |
| final byte[] raw = or.getBytes(); |
| if (Constants.OBJ_TAG == or.getType()) |
| return new Tag(this, id, refName, raw); |
| return new Tag(this, id, refName, null); |
| } |
| |
| /** |
| * Create a command to update, create or delete a ref in this repository. |
| * |
| * @param ref |
| * name of the ref the caller wants to modify. |
| * @return an update command. The caller must finish populating this command |
| * and then invoke one of the update methods to actually make a |
| * change. |
| * @throws IOException |
| * a symbolic ref was passed in and could not be resolved back |
| * to the base ref, as the symbolic ref could not be read. |
| */ |
| public RefUpdate updateRef(final String ref) throws IOException { |
| return updateRef(ref, false); |
| } |
| |
| /** |
| * Create a command to update, create or delete a ref in this repository. |
| * |
| * @param ref |
| * name of the ref the caller wants to modify. |
| * @param detach |
| * true to create a detached head |
| * @return an update command. The caller must finish populating this command |
| * and then invoke one of the update methods to actually make a |
| * change. |
| * @throws IOException |
| * a symbolic ref was passed in and could not be resolved back |
| * to the base ref, as the symbolic ref could not be read. |
| */ |
| public RefUpdate updateRef(final String ref, final boolean detach) throws IOException { |
| return getRefDatabase().newUpdate(ref, detach); |
| } |
| |
| /** |
| * Create a command to rename a ref in this repository |
| * |
| * @param fromRef |
| * name of ref to rename from |
| * @param toRef |
| * name of ref to rename to |
| * @return an update command that knows how to rename a branch to another. |
| * @throws IOException |
| * the rename could not be performed. |
| * |
| */ |
| public RefRename renameRef(final String fromRef, final String toRef) throws IOException { |
| return getRefDatabase().newRename(fromRef, toRef); |
| } |
| |
| /** |
| * Parse a git revision string and return an object id. |
| * |
| * Currently supported is combinations of these. |
| * <ul> |
| * <li>SHA-1 - a SHA-1</li> |
| * <li>refs/... - a ref name</li> |
| * <li>ref^n - nth parent reference</li> |
| * <li>ref~n - distance via parent reference</li> |
| * <li>ref@{n} - nth version of ref</li> |
| * <li>ref^{tree} - tree references by ref</li> |
| * <li>ref^{commit} - commit references by ref</li> |
| * </ul> |
| * |
| * Not supported is: |
| * <ul> |
| * <li>timestamps in reflogs, ref@{full or relative timestamp}</li> |
| * <li>abbreviated SHA-1's</li> |
| * </ul> |
| * |
| * @param revstr |
| * A git object references expression |
| * @return an ObjectId or null if revstr can't be resolved to any ObjectId |
| * @throws IOException |
| * on serious errors |
| */ |
| public ObjectId resolve(final String revstr) throws IOException { |
| char[] rev = revstr.toCharArray(); |
| RevObject ref = null; |
| RevWalk rw = new RevWalk(this); |
| for (int i = 0; i < rev.length; ++i) { |
| switch (rev[i]) { |
| case '^': |
| if (ref == null) { |
| ref = parseSimple(rw, new String(rev, 0, i)); |
| if (ref == null) |
| return null; |
| } |
| if (i + 1 < rev.length) { |
| switch (rev[i + 1]) { |
| case '0': |
| case '1': |
| case '2': |
| case '3': |
| case '4': |
| case '5': |
| case '6': |
| case '7': |
| case '8': |
| case '9': |
| int j; |
| ref = rw.parseCommit(ref); |
| for (j = i + 1; j < rev.length; ++j) { |
| if (!Character.isDigit(rev[j])) |
| break; |
| } |
| String parentnum = new String(rev, i + 1, j - i - 1); |
| int pnum; |
| try { |
| pnum = Integer.parseInt(parentnum); |
| } catch (NumberFormatException e) { |
| throw new RevisionSyntaxException( |
| JGitText.get().invalidCommitParentNumber, |
| revstr); |
| } |
| if (pnum != 0) { |
| RevCommit commit = (RevCommit) ref; |
| if (pnum > commit.getParentCount()) |
| ref = null; |
| else |
| ref = commit.getParent(pnum - 1); |
| } |
| i = j - 1; |
| break; |
| case '{': |
| int k; |
| String item = null; |
| for (k = i + 2; k < rev.length; ++k) { |
| if (rev[k] == '}') { |
| item = new String(rev, i + 2, k - i - 2); |
| break; |
| } |
| } |
| i = k; |
| if (item != null) |
| if (item.equals("tree")) { |
| ref = rw.parseTree(ref); |
| } else if (item.equals("commit")) { |
| ref = rw.parseCommit(ref); |
| } else if (item.equals("blob")) { |
| ref = rw.peel(ref); |
| if (!(ref instanceof RevBlob)) |
| throw new IncorrectObjectTypeException(ref, |
| Constants.TYPE_BLOB); |
| } else if (item.equals("")) { |
| ref = rw.peel(ref); |
| } else |
| throw new RevisionSyntaxException(revstr); |
| else |
| throw new RevisionSyntaxException(revstr); |
| break; |
| default: |
| ref = rw.parseAny(ref); |
| if (ref instanceof RevCommit) { |
| RevCommit commit = ((RevCommit) ref); |
| if (commit.getParentCount() == 0) |
| ref = null; |
| else |
| ref = commit.getParent(0); |
| } else |
| throw new IncorrectObjectTypeException(ref, |
| Constants.TYPE_COMMIT); |
| |
| } |
| } else { |
| ref = rw.peel(ref); |
| if (ref instanceof RevCommit) { |
| RevCommit commit = ((RevCommit) ref); |
| if (commit.getParentCount() == 0) |
| ref = null; |
| else |
| ref = commit.getParent(0); |
| } else |
| throw new IncorrectObjectTypeException(ref, |
| Constants.TYPE_COMMIT); |
| } |
| break; |
| case '~': |
| if (ref == null) { |
| ref = parseSimple(rw, new String(rev, 0, i)); |
| if (ref == null) |
| return null; |
| } |
| ref = rw.peel(ref); |
| if (!(ref instanceof RevCommit)) |
| throw new IncorrectObjectTypeException(ref, |
| Constants.TYPE_COMMIT); |
| int l; |
| for (l = i + 1; l < rev.length; ++l) { |
| if (!Character.isDigit(rev[l])) |
| break; |
| } |
| String distnum = new String(rev, i + 1, l - i - 1); |
| int dist; |
| try { |
| dist = Integer.parseInt(distnum); |
| } catch (NumberFormatException e) { |
| throw new RevisionSyntaxException( |
| JGitText.get().invalidAncestryLength, revstr); |
| } |
| while (dist > 0) { |
| RevCommit commit = (RevCommit) ref; |
| if (commit.getParentCount() == 0) { |
| ref = null; |
| break; |
| } |
| commit = commit.getParent(0); |
| rw.parseHeaders(commit); |
| ref = commit; |
| --dist; |
| } |
| i = l - 1; |
| break; |
| case '@': |
| int m; |
| String time = null; |
| for (m = i + 2; m < rev.length; ++m) { |
| if (rev[m] == '}') { |
| time = new String(rev, i + 2, m - i - 2); |
| break; |
| } |
| } |
| if (time != null) |
| throw new RevisionSyntaxException( |
| JGitText.get().reflogsNotYetSupportedByRevisionParser, |
| revstr); |
| i = m - 1; |
| break; |
| default: |
| if (ref != null) |
| throw new RevisionSyntaxException(revstr); |
| } |
| } |
| return ref != null ? ref.copy() : resolveSimple(revstr); |
| } |
| |
| private RevObject parseSimple(RevWalk rw, String revstr) throws IOException { |
| ObjectId id = resolveSimple(revstr); |
| return id != null ? rw.parseAny(id) : null; |
| } |
| |
| private ObjectId resolveSimple(final String revstr) throws IOException { |
| if (ObjectId.isId(revstr)) |
| return ObjectId.fromString(revstr); |
| final Ref r = getRefDatabase().getRef(revstr); |
| return r != null ? r.getObjectId() : null; |
| } |
| |
| /** Increment the use counter by one, requiring a matched {@link #close()}. */ |
| public void incrementOpen() { |
| useCnt.incrementAndGet(); |
| } |
| |
| /** Decrement the use count, and maybe close resources. */ |
| public void close() { |
| if (useCnt.decrementAndGet() == 0) { |
| doClose(); |
| } |
| } |
| |
| /** |
| * Invoked when the use count drops to zero during {@link #close()}. |
| * <p> |
| * The default implementation closes the object and ref databases. |
| */ |
| protected void doClose() { |
| getObjectDatabase().close(); |
| getRefDatabase().close(); |
| } |
| |
| /** |
| * Add a single existing pack to the list of available pack files. |
| * |
| * @param pack |
| * path of the pack file to open. |
| * @param idx |
| * path of the corresponding index file. |
| * @throws IOException |
| * index file could not be opened, read, or is not recognized as |
| * a Git pack file index. |
| */ |
| public abstract void openPack(File pack, File idx) throws IOException; |
| |
| public String toString() { |
| return "Repository[" + getDirectory() + "]"; |
| } |
| |
| /** |
| * Get the name of the reference that {@code HEAD} points to. |
| * <p> |
| * This is essentially the same as doing: |
| * |
| * <pre> |
| * return getRef(Constants.HEAD).getTarget().getName() |
| * </pre> |
| * |
| * Except when HEAD is detached, in which case this method returns the |
| * current ObjectId in hexadecimal string format. |
| * |
| * @return name of current branch (for example {@code refs/heads/master}) or |
| * an ObjectId in hex format if the current branch is detached. |
| * @throws IOException |
| */ |
| public String getFullBranch() throws IOException { |
| Ref head = getRef(Constants.HEAD); |
| if (head == null) |
| return null; |
| if (head.isSymbolic()) |
| return head.getTarget().getName(); |
| if (head.getObjectId() != null) |
| return head.getObjectId().name(); |
| return null; |
| } |
| |
| /** |
| * Get the short name of the current branch that {@code HEAD} points to. |
| * <p> |
| * This is essentially the same as {@link #getFullBranch()}, except the |
| * leading prefix {@code refs/heads/} is removed from the reference before |
| * it is returned to the caller. |
| * |
| * @return name of current branch (for example {@code master}), or an |
| * ObjectId in hex format if the current branch is detached. |
| * @throws IOException |
| */ |
| public String getBranch() throws IOException { |
| String name = getFullBranch(); |
| if (name != null) |
| return shortenRefName(name); |
| return name; |
| } |
| |
| /** |
| * Objects known to exist but not expressed by {@link #getAllRefs()}. |
| * <p> |
| * When a repository borrows objects from another repository, it can |
| * advertise that it safely has that other repository's references, without |
| * exposing any other details about the other repository. This may help |
| * a client trying to push changes avoid pushing more than it needs to. |
| * |
| * @return unmodifiable collection of other known objects. |
| */ |
| public Set<ObjectId> getAdditionalHaves() { |
| return Collections.emptySet(); |
| } |
| |
| /** |
| * Get a ref by name. |
| * |
| * @param name |
| * the name of the ref to lookup. May be a short-hand form, e.g. |
| * "master" which is is automatically expanded to |
| * "refs/heads/master" if "refs/heads/master" already exists. |
| * @return the Ref with the given name, or null if it does not exist |
| * @throws IOException |
| */ |
| public Ref getRef(final String name) throws IOException { |
| return getRefDatabase().getRef(name); |
| } |
| |
| /** |
| * @return mutable map of all known refs (heads, tags, remotes). |
| */ |
| public Map<String, Ref> getAllRefs() { |
| try { |
| return getRefDatabase().getRefs(RefDatabase.ALL); |
| } catch (IOException e) { |
| return new HashMap<String, Ref>(); |
| } |
| } |
| |
| /** |
| * @return mutable map of all tags; key is short tag name ("v1.0") and value |
| * of the entry contains the ref with the full tag name |
| * ("refs/tags/v1.0"). |
| */ |
| public Map<String, Ref> getTags() { |
| try { |
| return getRefDatabase().getRefs(Constants.R_TAGS); |
| } catch (IOException e) { |
| return new HashMap<String, Ref>(); |
| } |
| } |
| |
| /** |
| * Peel a possibly unpeeled reference to an annotated tag. |
| * <p> |
| * If the ref cannot be peeled (as it does not refer to an annotated tag) |
| * the peeled id stays null, but {@link Ref#isPeeled()} will be true. |
| * |
| * @param ref |
| * The ref to peel |
| * @return <code>ref</code> if <code>ref.isPeeled()</code> is true; else a |
| * new Ref object representing the same data as Ref, but isPeeled() |
| * will be true and getPeeledObjectId will contain the peeled object |
| * (or null). |
| */ |
| public Ref peel(final Ref ref) { |
| try { |
| return getRefDatabase().peel(ref); |
| } catch (IOException e) { |
| // Historical accident; if the reference cannot be peeled due |
| // to some sort of repository access problem we claim that the |
| // same as if the reference was not an annotated tag. |
| return ref; |
| } |
| } |
| |
| /** |
| * @return a map with all objects referenced by a peeled ref. |
| */ |
| public Map<AnyObjectId, Set<Ref>> getAllRefsByPeeledObjectId() { |
| Map<String, Ref> allRefs = getAllRefs(); |
| Map<AnyObjectId, Set<Ref>> ret = new HashMap<AnyObjectId, Set<Ref>>(allRefs.size()); |
| for (Ref ref : allRefs.values()) { |
| ref = peel(ref); |
| AnyObjectId target = ref.getPeeledObjectId(); |
| if (target == null) |
| target = ref.getObjectId(); |
| // We assume most Sets here are singletons |
| Set<Ref> oset = ret.put(target, Collections.singleton(ref)); |
| if (oset != null) { |
| // that was not the case (rare) |
| if (oset.size() == 1) { |
| // Was a read-only singleton, we must copy to a new Set |
| oset = new HashSet<Ref>(oset); |
| } |
| ret.put(target, oset); |
| oset.add(ref); |
| } |
| } |
| return ret; |
| } |
| |
| /** |
| * @return a representation of the index associated with this |
| * {@link Repository} |
| * @throws IOException |
| * if the index can not be read |
| * @throws IllegalStateException |
| * if this is bare (see {@link #isBare()}) |
| */ |
| public GitIndex getIndex() throws IOException, IllegalStateException { |
| if (isBare()) |
| throw new IllegalStateException( |
| JGitText.get().bareRepositoryNoWorkdirAndIndex); |
| if (index == null) { |
| index = new GitIndex(this); |
| index.read(); |
| } else { |
| index.rereadIfNecessary(); |
| } |
| return index; |
| } |
| |
| /** |
| * @return the index file location |
| * @throws IllegalStateException |
| * if this is bare (see {@link #isBare()}) |
| */ |
| public File getIndexFile() throws IllegalStateException { |
| if (isBare()) |
| throw new IllegalStateException( |
| JGitText.get().bareRepositoryNoWorkdirAndIndex); |
| return indexFile; |
| } |
| |
| static byte[] gitInternalSlash(byte[] bytes) { |
| if (File.separatorChar == '/') |
| return bytes; |
| for (int i=0; i<bytes.length; ++i) |
| if (bytes[i] == File.separatorChar) |
| bytes[i] = '/'; |
| return bytes; |
| } |
| |
| /** |
| * @return an important state |
| */ |
| public RepositoryState getRepositoryState() { |
| if (isBare()) |
| return RepositoryState.BARE; |
| |
| // Pre Git-1.6 logic |
| if (new File(getWorkDir(), ".dotest").exists()) |
| return RepositoryState.REBASING; |
| if (new File(getDirectory(), ".dotest-merge").exists()) |
| return RepositoryState.REBASING_INTERACTIVE; |
| |
| // From 1.6 onwards |
| if (new File(getDirectory(),"rebase-apply/rebasing").exists()) |
| return RepositoryState.REBASING_REBASING; |
| if (new File(getDirectory(),"rebase-apply/applying").exists()) |
| return RepositoryState.APPLY; |
| if (new File(getDirectory(),"rebase-apply").exists()) |
| return RepositoryState.REBASING; |
| |
| if (new File(getDirectory(),"rebase-merge/interactive").exists()) |
| return RepositoryState.REBASING_INTERACTIVE; |
| if (new File(getDirectory(),"rebase-merge").exists()) |
| return RepositoryState.REBASING_MERGE; |
| |
| // Both versions |
| if (new File(getDirectory(), "MERGE_HEAD").exists()) { |
| // we are merging - now check whether we have unmerged paths |
| try { |
| if (!DirCache.read(this).hasUnmergedPaths()) { |
| // no unmerged paths -> return the MERGING_RESOLVED state |
| return RepositoryState.MERGING_RESOLVED; |
| } |
| } catch (IOException e) { |
| // Can't decide whether unmerged paths exists. Return |
| // MERGING state to be on the safe side (in state MERGING |
| // you are not allow to do anything) |
| e.printStackTrace(); |
| } |
| return RepositoryState.MERGING; |
| } |
| |
| if (new File(getDirectory(), "BISECT_LOG").exists()) |
| return RepositoryState.BISECTING; |
| |
| return RepositoryState.SAFE; |
| } |
| |
| /** |
| * Check validity of a ref name. It must not contain character that has |
| * a special meaning in a Git object reference expression. Some other |
| * dangerous characters are also excluded. |
| * |
| * For portability reasons '\' is excluded |
| * |
| * @param refName |
| * |
| * @return true if refName is a valid ref name |
| */ |
| public static boolean isValidRefName(final String refName) { |
| final int len = refName.length(); |
| if (len == 0) |
| return false; |
| if (refName.endsWith(".lock")) |
| return false; |
| |
| int components = 1; |
| char p = '\0'; |
| for (int i = 0; i < len; i++) { |
| final char c = refName.charAt(i); |
| if (c <= ' ') |
| return false; |
| switch (c) { |
| case '.': |
| switch (p) { |
| case '\0': case '/': case '.': |
| return false; |
| } |
| if (i == len -1) |
| return false; |
| break; |
| case '/': |
| if (i == 0 || i == len - 1) |
| return false; |
| components++; |
| break; |
| case '{': |
| if (p == '@') |
| return false; |
| break; |
| case '~': case '^': case ':': |
| case '?': case '[': case '*': |
| case '\\': |
| return false; |
| } |
| p = c; |
| } |
| return components > 1; |
| } |
| |
| /** |
| * Strip work dir and return normalized repository path. |
| * |
| * @param workDir Work dir |
| * @param file File whose path shall be stripped of its workdir |
| * @return normalized repository relative path or the empty |
| * string if the file is not relative to the work directory. |
| */ |
| public static String stripWorkDir(File workDir, File file) { |
| final String filePath = file.getPath(); |
| final String workDirPath = workDir.getPath(); |
| |
| if (filePath.length() <= workDirPath.length() || |
| filePath.charAt(workDirPath.length()) != File.separatorChar || |
| !filePath.startsWith(workDirPath)) { |
| File absWd = workDir.isAbsolute() ? workDir : workDir.getAbsoluteFile(); |
| File absFile = file.isAbsolute() ? file : file.getAbsoluteFile(); |
| if (absWd == workDir && absFile == file) |
| return ""; |
| return stripWorkDir(absWd, absFile); |
| } |
| |
| String relName = filePath.substring(workDirPath.length() + 1); |
| if (File.separatorChar != '/') |
| relName = relName.replace(File.separatorChar, '/'); |
| return relName; |
| } |
| |
| /** |
| * @return the "bare"-ness of this Repository |
| */ |
| public boolean isBare() { |
| return workDir == null; |
| } |
| |
| /** |
| * @return the workdir file, i.e. where the files are checked out |
| * @throws IllegalStateException |
| * if the repository is "bare" |
| */ |
| public File getWorkDir() throws IllegalStateException { |
| if (isBare()) |
| throw new IllegalStateException( |
| JGitText.get().bareRepositoryNoWorkdirAndIndex); |
| return workDir; |
| } |
| |
| /** |
| * Override default workdir |
| * |
| * @param workTree |
| * the work tree directory |
| */ |
| public void setWorkDir(File workTree) { |
| this.workDir = workTree; |
| } |
| |
| /** |
| * Register a {@link RepositoryListener} which will be notified |
| * when ref changes are detected. |
| * |
| * @param l |
| */ |
| public void addRepositoryChangedListener(final RepositoryListener l) { |
| listeners.add(l); |
| } |
| |
| /** |
| * Remove a registered {@link RepositoryListener} |
| * @param l |
| */ |
| public void removeRepositoryChangedListener(final RepositoryListener l) { |
| listeners.remove(l); |
| } |
| |
| /** |
| * Register a global {@link RepositoryListener} which will be notified |
| * when a ref changes in any repository are detected. |
| * |
| * @param l |
| */ |
| public static void addAnyRepositoryChangedListener(final RepositoryListener l) { |
| allListeners.add(l); |
| } |
| |
| /** |
| * Remove a globally registered {@link RepositoryListener} |
| * @param l |
| */ |
| public static void removeAnyRepositoryChangedListener(final RepositoryListener l) { |
| allListeners.remove(l); |
| } |
| |
| void fireRefsChanged() { |
| final RefsChangedEvent event = new RefsChangedEvent(this); |
| List<RepositoryListener> all; |
| synchronized (listeners) { |
| all = new ArrayList<RepositoryListener>(listeners); |
| } |
| synchronized (allListeners) { |
| all.addAll(allListeners); |
| } |
| for (final RepositoryListener l : all) { |
| l.refsChanged(event); |
| } |
| } |
| |
| void fireIndexChanged() { |
| final IndexChangedEvent event = new IndexChangedEvent(this); |
| List<RepositoryListener> all; |
| synchronized (listeners) { |
| all = new ArrayList<RepositoryListener>(listeners); |
| } |
| synchronized (allListeners) { |
| all.addAll(allListeners); |
| } |
| for (final RepositoryListener l : all) { |
| l.indexChanged(event); |
| } |
| } |
| |
| /** |
| * Force a scan for changed refs. |
| * |
| * @throws IOException |
| */ |
| public abstract void scanForRepoChanges() throws IOException; |
| |
| /** |
| * @param refName |
| * |
| * @return a more user friendly ref name |
| */ |
| public String shortenRefName(String refName) { |
| if (refName.startsWith(Constants.R_HEADS)) |
| return refName.substring(Constants.R_HEADS.length()); |
| if (refName.startsWith(Constants.R_TAGS)) |
| return refName.substring(Constants.R_TAGS.length()); |
| if (refName.startsWith(Constants.R_REMOTES)) |
| return refName.substring(Constants.R_REMOTES.length()); |
| return refName; |
| } |
| |
| /** |
| * @param refName |
| * @return a {@link ReflogReader} for the supplied refname, or null if the |
| * named ref does not exist. |
| * @throws IOException the ref could not be accessed. |
| */ |
| public abstract ReflogReader getReflogReader(String refName) |
| throws IOException; |
| |
| /** |
| * Return the information stored in the file $GIT_DIR/MERGE_MSG. In this |
| * file operations triggering a merge will store a template for the commit |
| * message of the merge commit. |
| * |
| * @return a String containing the content of the MERGE_MSG file or |
| * {@code null} if this file doesn't exist |
| * @throws IOException |
| * @throws IllegalStateException |
| * if the repository is "bare" |
| */ |
| public String readMergeCommitMsg() throws IOException { |
| if (isBare()) |
| throw new IllegalStateException( |
| JGitText.get().bareRepositoryNoWorkdirAndIndex); |
| |
| File mergeMsgFile = new File(getDirectory(), Constants.MERGE_MSG); |
| try { |
| return new String(IO.readFully(mergeMsgFile)); |
| } catch (FileNotFoundException e) { |
| // MERGE_MSG file has disappeared in the meantime |
| // ignore it |
| return null; |
| } |
| } |
| |
| /** |
| * Return the information stored in the file $GIT_DIR/MERGE_HEAD. In this |
| * file operations triggering a merge will store the IDs of all heads which |
| * should be merged together with HEAD. |
| * |
| * @return a list of {@link Commit}s which IDs are listed in the MERGE_HEAD |
| * file or {@code null} if this file doesn't exist. Also if the file |
| * exists but is empty {@code null} will be returned |
| * @throws IOException |
| * @throws IllegalStateException |
| * if the repository is "bare" |
| */ |
| public List<ObjectId> readMergeHeads() throws IOException { |
| if (isBare()) |
| throw new IllegalStateException( |
| JGitText.get().bareRepositoryNoWorkdirAndIndex); |
| |
| File mergeHeadFile = new File(getDirectory(), Constants.MERGE_HEAD); |
| byte[] raw; |
| try { |
| raw = IO.readFully(mergeHeadFile); |
| } catch (FileNotFoundException notFound) { |
| return new LinkedList<ObjectId>(); |
| } |
| |
| if (raw.length == 0) |
| throw new IOException("MERGE_HEAD file empty: " + mergeHeadFile); |
| |
| LinkedList<ObjectId> heads = new LinkedList<ObjectId>(); |
| for (int p = 0; p < raw.length;) { |
| heads.add(ObjectId.fromString(raw, p)); |
| p = RawParseUtils |
| .nextLF(raw, p + Constants.OBJECT_ID_STRING_LENGTH); |
| } |
| return heads; |
| } |
| } |