org.eclipse.jgit.lib.Repository Maven / Gradle / Ivy
/*
* Copyright (C) 2007, Dave Watson
* Copyright (C) 2008-2010, Google Inc.
* Copyright (C) 2006-2010, Robin Rosenberg
* Copyright (C) 2006-2012, Shawn O. Pearce
* Copyright (C) 2012, Daniel Megert
* 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.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.text.MessageFormat;
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.concurrent.atomic.AtomicInteger;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.errors.AmbiguousObjectException;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.NoWorkTreeException;
import org.eclipse.jgit.errors.RevisionSyntaxException;
import org.eclipse.jgit.events.IndexChangedEvent;
import org.eclipse.jgit.events.IndexChangedListener;
import org.eclipse.jgit.events.ListenerList;
import org.eclipse.jgit.events.RepositoryEvent;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.revwalk.RevBlob;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.storage.file.CheckoutEntry;
import org.eclipse.jgit.storage.file.ReflogEntry;
import org.eclipse.jgit.storage.file.ReflogReader;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.RemoteConfig;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.FileUtils;
import org.eclipse.jgit.util.IO;
import org.eclipse.jgit.util.RawParseUtils;
import org.eclipse.jgit.util.io.SafeBufferedOutputStream;
/**
* Represents a Git repository.
*
* 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).
*
* This class is thread-safe.
*/
public abstract class Repository {
private static final ListenerList globalListeners = new ListenerList();
/** @return the global listener list observing all events in this JVM. */
public static ListenerList getGlobalListenerList() {
return globalListeners;
}
private final AtomicInteger useCnt = new AtomicInteger(1);
/** Metadata directory holding the repository's critical files. */
private final File gitDir;
/** File abstraction used to resolve paths. */
private final FS fs;
private final ListenerList myListeners = new ListenerList();
/** If not bare, the top level directory of the working files. */
private final File workTree;
/** If not bare, the index file caching the working file states. */
private final File indexFile;
/**
* Initialize a new repository instance.
*
* @param options
* options to configure the repository.
*/
protected Repository(final BaseRepositoryBuilder options) {
gitDir = options.getGitDir();
fs = options.getFS();
workTree = options.getWorkTree();
indexFile = options.getIndexFile();
}
/** @return listeners observing only events on this repository. */
public ListenerList getListenerList() {
return myListeners;
}
/**
* Fire an event to all registered listeners.
*
* The source repository of the event is automatically set to this
* repository, before the event is delivered to any listeners.
*
* @param event
* the event to deliver.
*/
public void fireEvent(RepositoryEvent> event) {
event.setRepository(this);
myListeners.dispatch(event);
globalListeners.dispatch(event);
}
/**
* Create a new Git repository.
*
* 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 (a repository without a working
* directory) is created.
* @throws IOException
* in case of IO problem
*/
public abstract void create(boolean bare) throws IOException;
/** @return local metadata directory; null if repository isn't local. */
public File getDirectory() {
return gitDir;
}
/**
* @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 a new reader to read objects from {@link #getObjectDatabase()} */
public ObjectReader newObjectReader() {
return getObjectDatabase().newReader();
}
/** @return the reference database which stores the reference namespace. */
public abstract RefDatabase getRefDatabase();
/**
* @return the configuration of this repository
*/
public abstract StoredConfig getConfig();
/**
* @return the used file system abstraction
*/
public FS getFS() {
return fs;
}
/**
* @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) {
try {
return getObjectDatabase().has(objectId);
} catch (IOException e) {
// Legacy API, assume error means "no"
return false;
}
}
/**
* Open an object from this repository.
*
* This is a one-shot call interface which may be faster than allocating a
* {@link #newObjectReader()} to perform the lookup.
*
* @param objectId
* identity of the object to open.
* @return a {@link ObjectLoader} for accessing the object.
* @throws MissingObjectException
* the object does not exist.
* @throws IOException
* the object store cannot be accessed.
*/
public ObjectLoader open(final AnyObjectId objectId)
throws MissingObjectException, IOException {
return getObjectDatabase().open(objectId);
}
/**
* Open an object from this repository.
*
* This is a one-shot call interface which may be faster than allocating a
* {@link #newObjectReader()} to perform the lookup.
*
* @param objectId
* identity of the object to open.
* @param typeHint
* hint about the type of object being requested;
* {@link ObjectReader#OBJ_ANY} if the object type is not known,
* or does not matter to the caller.
* @return a {@link ObjectLoader} for accessing the object.
* @throws MissingObjectException
* the object does not exist.
* @throws IncorrectObjectTypeException
* typeHint was not OBJ_ANY, and the object's actual type does
* not match typeHint.
* @throws IOException
* the object store cannot be accessed.
*/
public ObjectLoader open(AnyObjectId objectId, int typeHint)
throws MissingObjectException, IncorrectObjectTypeException,
IOException {
return getObjectDatabase().open(objectId, typeHint);
}
/**
* 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.
*
* Combinations of these operators are supported:
*
* - HEAD, MERGE_HEAD, FETCH_HEAD
* - SHA-1: a complete or abbreviated SHA-1
* - refs/...: a complete reference name
* - short-name: a short reference name under {@code refs/heads},
* {@code refs/tags}, or {@code refs/remotes} namespace
* - tag-NN-gABBREV: output from describe, parsed by treating
* {@code ABBREV} as an abbreviated SHA-1.
* - id^: first parent of commit id, this is the same
* as {@code id^1}
* - id^0: ensure id is a commit
* - id^n: n-th parent of commit id
* - id~n: n-th historical ancestor of id, by first
* parent. {@code id~3} is equivalent to {@code id^1^1^1} or {@code id^^^}.
* - id:path: Lookup path under tree named by id
* - id^{commit}: ensure id is a commit
* - id^{tree}: ensure id is a tree
* - id^{tag}: ensure id is a tag
* - id^{blob}: ensure id is a blob
*
*
*
* The following operators are specified by Git conventions, but are not
* supported by this method:
*
* - ref@{n}: n-th version of ref as given by its reflog
* - ref@{time}: value of ref at the designated time
*
*
* @param revstr
* A git object references expression
* @return an ObjectId or null if revstr can't be resolved to any ObjectId
* @throws AmbiguousObjectException
* {@code revstr} contains an abbreviated ObjectId and this
* repository contains more than one object which match to the
* input abbreviation.
* @throws IncorrectObjectTypeException
* the id parsed does not meet the type required to finish
* applying the operators in the expression.
* @throws RevisionSyntaxException
* the expression is not supported by this implementation, or
* does not meet the standard syntax.
* @throws IOException
* on serious errors
*/
public ObjectId resolve(final String revstr)
throws AmbiguousObjectException, IncorrectObjectTypeException,
RevisionSyntaxException, IOException {
RevWalk rw = new RevWalk(this);
try {
Object resolved = resolve(rw, revstr);
if (resolved instanceof String) {
return getRef((String) resolved).getLeaf().getObjectId();
} else {
return (ObjectId) resolved;
}
} finally {
rw.release();
}
}
/**
* Simplify an expression, but unlike {@link #resolve(String)} it will not
* resolve a branch passed or resulting from the expression, such as @{-}.
* Thus this method can be used to process an expression to a method that
* expects a branch or revision id.
*
* @param revstr
* @return object id or ref name from resolved expression
* @throws AmbiguousObjectException
* @throws IOException
*/
public String simplify(final String revstr)
throws AmbiguousObjectException, IOException {
RevWalk rw = new RevWalk(this);
try {
Object resolved = resolve(rw, revstr);
if (resolved != null)
if (resolved instanceof String)
return (String) resolved;
else
return ((AnyObjectId) resolved).getName();
return null;
} finally {
rw.release();
}
}
private Object resolve(final RevWalk rw, final String revstr)
throws IOException {
char[] revChars = revstr.toCharArray();
RevObject rev = null;
String name = null;
int done = 0;
for (int i = 0; i < revChars.length; ++i) {
switch (revChars[i]) {
case '^':
if (rev == null) {
if (name == null)
if (done == 0)
name = new String(revChars, done, i);
else {
done = i + 1;
break;
}
rev = parseSimple(rw, name);
name = null;
if (rev == null)
return null;
}
if (i + 1 < revChars.length) {
switch (revChars[i + 1]) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
int j;
rev = rw.parseCommit(rev);
for (j = i + 1; j < revChars.length; ++j) {
if (!Character.isDigit(revChars[j]))
break;
}
String parentnum = new String(revChars, 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) rev;
if (pnum > commit.getParentCount())
rev = null;
else
rev = commit.getParent(pnum - 1);
}
i = j - 1;
done = j;
break;
case '{':
int k;
String item = null;
for (k = i + 2; k < revChars.length; ++k) {
if (revChars[k] == '}') {
item = new String(revChars, i + 2, k - i - 2);
break;
}
}
i = k;
if (item != null)
if (item.equals("tree")) { //$NON-NLS-1$
rev = rw.parseTree(rev);
} else if (item.equals("commit")) { //$NON-NLS-1$
rev = rw.parseCommit(rev);
} else if (item.equals("blob")) { //$NON-NLS-1$
rev = rw.peel(rev);
if (!(rev instanceof RevBlob))
throw new IncorrectObjectTypeException(rev,
Constants.TYPE_BLOB);
} else if (item.equals("")) { //$NON-NLS-1$
rev = rw.peel(rev);
} else
throw new RevisionSyntaxException(revstr);
else
throw new RevisionSyntaxException(revstr);
done = k;
break;
default:
rev = rw.peel(rev);
if (rev instanceof RevCommit) {
RevCommit commit = ((RevCommit) rev);
if (commit.getParentCount() == 0)
rev = null;
else
rev = commit.getParent(0);
} else
throw new IncorrectObjectTypeException(rev,
Constants.TYPE_COMMIT);
}
} else {
rev = rw.peel(rev);
if (rev instanceof RevCommit) {
RevCommit commit = ((RevCommit) rev);
if (commit.getParentCount() == 0)
rev = null;
else
rev = commit.getParent(0);
} else
throw new IncorrectObjectTypeException(rev,
Constants.TYPE_COMMIT);
}
done = i + 1;
break;
case '~':
if (rev == null) {
if (name == null)
if (done == 0)
name = new String(revChars, done, i);
else {
done = i + 1;
break;
}
rev = parseSimple(rw, name);
name = null;
if (rev == null)
return null;
}
rev = rw.peel(rev);
if (!(rev instanceof RevCommit))
throw new IncorrectObjectTypeException(rev,
Constants.TYPE_COMMIT);
int l;
for (l = i + 1; l < revChars.length; ++l) {
if (!Character.isDigit(revChars[l]))
break;
}
int dist;
if (l - i > 1) {
String distnum = new String(revChars, i + 1, l - i - 1);
try {
dist = Integer.parseInt(distnum);
} catch (NumberFormatException e) {
throw new RevisionSyntaxException(
JGitText.get().invalidAncestryLength, revstr);
}
} else
dist = 1;
while (dist > 0) {
RevCommit commit = (RevCommit) rev;
if (commit.getParentCount() == 0) {
rev = null;
break;
}
commit = commit.getParent(0);
rw.parseHeaders(commit);
rev = commit;
--dist;
}
i = l - 1;
done = l;
break;
case '@':
if (rev != null)
throw new RevisionSyntaxException(revstr);
if (i + 1 < revChars.length && revChars[i + 1] != '{')
continue;
int m;
String time = null;
for (m = i + 2; m < revChars.length; ++m) {
if (revChars[m] == '}') {
time = new String(revChars, i + 2, m - i - 2);
break;
}
}
if (time != null) {
if (time.equals("upstream")) { //$NON-NLS-1$
if (name == null)
name = new String(revChars, done, i);
if (name.equals("")) //$NON-NLS-1$
// Currently checked out branch, HEAD if
// detached
name = Constants.HEAD;
if (!Repository.isValidRefName("x/" + name)) //$NON-NLS-1$
throw new RevisionSyntaxException(revstr);
Ref ref = getRef(name);
name = null;
if (ref == null)
return null;
if (ref.isSymbolic())
ref = ref.getLeaf();
name = ref.getName();
RemoteConfig remoteConfig;
try {
remoteConfig = new RemoteConfig(getConfig(),
"origin"); //$NON-NLS-1$
} catch (URISyntaxException e) {
throw new RevisionSyntaxException(revstr);
}
String remoteBranchName = getConfig()
.getString(
ConfigConstants.CONFIG_BRANCH_SECTION,
Repository.shortenRefName(ref.getName()),
ConfigConstants.CONFIG_KEY_MERGE);
List fetchRefSpecs = remoteConfig
.getFetchRefSpecs();
for (RefSpec refSpec : fetchRefSpecs) {
if (refSpec.matchSource(remoteBranchName)) {
RefSpec expandFromSource = refSpec
.expandFromSource(remoteBranchName);
name = expandFromSource.getDestination();
break;
}
}
if (name == null)
throw new RevisionSyntaxException(revstr);
} else if (time.matches("^-\\d+$")) { //$NON-NLS-1$
if (name != null)
throw new RevisionSyntaxException(revstr);
else {
String previousCheckout = resolveReflogCheckout(-Integer
.parseInt(time));
if (ObjectId.isId(previousCheckout))
rev = parseSimple(rw, previousCheckout);
else
name = previousCheckout;
}
} else {
if (name == null)
name = new String(revChars, done, i);
if (name.equals("")) //$NON-NLS-1$
name = Constants.HEAD;
if (!Repository.isValidRefName("x/" + name)) //$NON-NLS-1$
throw new RevisionSyntaxException(revstr);
Ref ref = getRef(name);
name = null;
if (ref == null)
return null;
// @{n} means current branch, not HEAD@{1} unless
// detached
if (ref.isSymbolic())
ref = ref.getLeaf();
rev = resolveReflog(rw, ref, time);
}
i = m;
} else
throw new RevisionSyntaxException(revstr);
break;
case ':': {
RevTree tree;
if (rev == null) {
if (name == null)
name = new String(revChars, done, i);
if (name.equals("")) //$NON-NLS-1$
name = Constants.HEAD;
rev = parseSimple(rw, name);
name = null;
}
if (rev == null)
return null;
tree = rw.parseTree(rev);
if (i == revChars.length - 1)
return tree.copy();
TreeWalk tw = TreeWalk.forPath(rw.getObjectReader(),
new String(revChars, i + 1, revChars.length - i - 1),
tree);
return tw != null ? tw.getObjectId(0) : null;
}
default:
if (rev != null)
throw new RevisionSyntaxException(revstr);
}
}
if (rev != null)
return rev.copy();
if (name != null)
return name;
if (done == revstr.length())
return null;
name = revstr.substring(done);
if (!Repository.isValidRefName("x/" + name)) //$NON-NLS-1$
throw new RevisionSyntaxException(revstr);
if (getRef(name) != null)
return name;
return resolveSimple(name);
}
private static boolean isHex(char c) {
return ('0' <= c && c <= '9') //
|| ('a' <= c && c <= 'f') //
|| ('A' <= c && c <= 'F');
}
private static boolean isAllHex(String str, int ptr) {
while (ptr < str.length()) {
if (!isHex(str.charAt(ptr++)))
return false;
}
return true;
}
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);
if (Repository.isValidRefName("x/" + revstr)) { //$NON-NLS-1$
Ref r = getRefDatabase().getRef(revstr);
if (r != null)
return r.getObjectId();
}
if (AbbreviatedObjectId.isId(revstr))
return resolveAbbreviation(revstr);
int dashg = revstr.indexOf("-g"); //$NON-NLS-1$
if ((dashg + 5) < revstr.length() && 0 <= dashg
&& isHex(revstr.charAt(dashg + 2))
&& isHex(revstr.charAt(dashg + 3))
&& isAllHex(revstr, dashg + 4)) {
// Possibly output from git describe?
String s = revstr.substring(dashg + 2);
if (AbbreviatedObjectId.isId(s))
return resolveAbbreviation(s);
}
return null;
}
private String resolveReflogCheckout(int checkoutNo)
throws IOException {
List reflogEntries = new ReflogReader(this, Constants.HEAD)
.getReverseEntries();
for (ReflogEntry entry : reflogEntries) {
CheckoutEntry checkout = entry.parseCheckout();
if (checkout != null)
if (checkoutNo-- == 1)
return checkout.getFromBranch();
}
return null;
}
private RevCommit resolveReflog(RevWalk rw, Ref ref, String time)
throws IOException {
int number;
try {
number = Integer.parseInt(time);
} catch (NumberFormatException nfe) {
throw new RevisionSyntaxException(MessageFormat.format(
JGitText.get().invalidReflogRevision, time));
}
assert number >= 0;
ReflogReader reader = new ReflogReader(this, ref.getName());
ReflogEntry entry = reader.getReverseEntry(number);
if (entry == null)
throw new RevisionSyntaxException(MessageFormat.format(
JGitText.get().reflogEntryNotFound,
Integer.valueOf(number), ref.getName()));
return rw.parseCommit(entry.getNewId());
}
private ObjectId resolveAbbreviation(final String revstr) throws IOException,
AmbiguousObjectException {
AbbreviatedObjectId id = AbbreviatedObjectId.fromString(revstr);
ObjectReader reader = newObjectReader();
try {
Collection matches = reader.resolve(id);
if (matches.size() == 0)
return null;
else if (matches.size() == 1)
return matches.iterator().next();
else
throw new AmbiguousObjectException(id, matches);
} finally {
reader.release();
}
}
/** 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()}.
*
* The default implementation closes the object and ref databases.
*/
protected void doClose() {
getObjectDatabase().close();
getRefDatabase().close();
}
@SuppressWarnings("nls")
public String toString() {
String desc;
if (getDirectory() != null)
desc = getDirectory().getPath();
else
desc = getClass().getSimpleName() + "-" //$NON-NLS-1$
+ System.identityHashCode(this);
return "Repository[" + desc + "]"; //$NON-NLS-1$
}
/**
* Get the name of the reference that {@code HEAD} points to.
*
* This is essentially the same as doing:
*
*
* return getRef(Constants.HEAD).getTarget().getName()
*
*
* 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.
*
* 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()}.
*
* 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 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 getAllRefs() {
try {
return getRefDatabase().getRefs(RefDatabase.ALL);
} catch (IOException e) {
return new HashMap();
}
}
/**
* @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 getTags() {
try {
return getRefDatabase().getRefs(Constants.R_TAGS);
} catch (IOException e) {
return new HashMap();
}
}
/**
* Peel a possibly unpeeled reference to an annotated tag.
*
* 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 ref
if ref.isPeeled()
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> getAllRefsByPeeledObjectId() {
Map allRefs = getAllRefs();
Map> ret = new HashMap>(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 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(oset);
}
ret.put(target, oset);
oset.add(ref);
}
}
return ret;
}
/**
* @return the index file location
* @throws NoWorkTreeException
* if this is bare, which implies it has no working directory.
* See {@link #isBare()}.
*/
public File getIndexFile() throws NoWorkTreeException {
if (isBare())
throw new NoWorkTreeException();
return indexFile;
}
/**
* Create a new in-core index representation and read an index from disk.
*
* The new index will be read before it is returned to the caller. Read
* failures are reported as exceptions and therefore prevent the method from
* returning a partially populated index.
*
* @return a cache representing the contents of the specified index file (if
* it exists) or an empty cache if the file does not exist.
* @throws NoWorkTreeException
* if this is bare, which implies it has no working directory.
* See {@link #isBare()}.
* @throws IOException
* the index file is present but could not be read.
* @throws CorruptObjectException
* the index file is using a format or extension that this
* library does not support.
*/
public DirCache readDirCache() throws NoWorkTreeException,
CorruptObjectException, IOException {
return DirCache.read(this);
}
/**
* Create a new in-core index representation, lock it, and read from disk.
*
* The new index will be locked and then read before it is returned to the
* caller. Read failures are reported as exceptions and therefore prevent
* the method from returning a partially populated index.
*
* @return a cache representing the contents of the specified index file (if
* it exists) or an empty cache if the file does not exist.
* @throws NoWorkTreeException
* if this is bare, which implies it has no working directory.
* See {@link #isBare()}.
* @throws IOException
* the index file is present but could not be read, or the lock
* could not be obtained.
* @throws CorruptObjectException
* the index file is using a format or extension that this
* library does not support.
*/
public DirCache lockDirCache() throws NoWorkTreeException,
CorruptObjectException, IOException {
// we want DirCache to inform us so that we can inform registered
// listeners about index changes
IndexChangedListener l = new IndexChangedListener() {
public void onIndexChanged(IndexChangedEvent event) {
notifyIndexChanged();
}
};
return DirCache.lock(this, l);
}
static byte[] gitInternalSlash(byte[] bytes) {
if (File.separatorChar == '/')
return bytes;
for (int i=0; i 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)
}
return RepositoryState.MERGING;
}
if (new File(getDirectory(), "BISECT_LOG").exists()) //$NON-NLS-1$
return RepositoryState.BISECTING;
if (new File(getDirectory(), Constants.CHERRY_PICK_HEAD).exists()) {
try {
if (!readDirCache().hasUnmergedPaths()) {
// no unmerged paths
return RepositoryState.CHERRY_PICKING_RESOLVED;
}
} catch (IOException e) {
// fall through to CHERRY_PICKING
}
return RepositoryState.CHERRY_PICKING;
}
if (new File(getDirectory(), Constants.REVERT_HEAD).exists()) {
try {
if (!readDirCache().hasUnmergedPaths()) {
// no unmerged paths
return RepositoryState.REVERTING_RESOLVED;
}
} catch (IOException e) {
// fall through to REVERTING
}
return RepositoryState.REVERTING;
}
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")) //$NON-NLS-1$
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;
if (p == '/')
return false;
components++;
break;
case '{':
if (p == '@')
return false;
break;
case '~': case '^': case ':':
case '?': case '[': case '*':
case '\\':
case '\u007F':
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 ""; //$NON-NLS-1$
return stripWorkDir(absWd, absFile);
}
String relName = filePath.substring(workDirPath.length() + 1);
if (File.separatorChar != '/')
relName = relName.replace(File.separatorChar, '/');
return relName;
}
/**
* @return true if this is bare, which implies it has no working directory.
*/
public boolean isBare() {
return workTree == null;
}
/**
* @return the root directory of the working tree, where files are checked
* out for viewing and editing.
* @throws NoWorkTreeException
* if this is bare, which implies it has no working directory.
* See {@link #isBare()}.
*/
public File getWorkTree() throws NoWorkTreeException {
if (isBare())
throw new NoWorkTreeException();
return workTree;
}
/**
* Force a scan for changed refs.
*
* @throws IOException
*/
public abstract void scanForRepoChanges() throws IOException;
/**
* Notify that the index changed
*/
public abstract void notifyIndexChanged();
/**
* @param refName
*
* @return a more user friendly ref name
*/
public static 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 NoWorkTreeException
* if this is bare, which implies it has no working directory.
* See {@link #isBare()}.
*/
public String readMergeCommitMsg() throws IOException, NoWorkTreeException {
return readCommitMsgFile(Constants.MERGE_MSG);
}
/**
* Write new content to 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. If null
is specified as message the file will
* be deleted.
*
* @param msg
* the message which should be written or null
to
* delete the file
*
* @throws IOException
*/
public void writeMergeCommitMsg(String msg) throws IOException {
File mergeMsgFile = new File(gitDir, Constants.MERGE_MSG);
writeCommitMsg(mergeMsgFile, msg);
}
/**
* 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 commits 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 NoWorkTreeException
* if this is bare, which implies it has no working directory.
* See {@link #isBare()}.
*/
public List readMergeHeads() throws IOException, NoWorkTreeException {
if (isBare() || getDirectory() == null)
throw new NoWorkTreeException();
byte[] raw = readGitDirectoryFile(Constants.MERGE_HEAD);
if (raw == null)
return null;
LinkedList heads = new LinkedList();
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;
}
/**
* Write new merge-heads into $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. If null
is specified as list of commits
* the file will be deleted
*
* @param heads
* a list of commits which IDs should be written to
* $GIT_DIR/MERGE_HEAD or null
to delete the file
* @throws IOException
*/
public void writeMergeHeads(List heads) throws IOException {
writeHeadsFile(heads, Constants.MERGE_HEAD);
}
/**
* Return the information stored in the file $GIT_DIR/CHERRY_PICK_HEAD.
*
* @return object id from CHERRY_PICK_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 NoWorkTreeException
* if this is bare, which implies it has no working directory.
* See {@link #isBare()}.
*/
public ObjectId readCherryPickHead() throws IOException,
NoWorkTreeException {
if (isBare() || getDirectory() == null)
throw new NoWorkTreeException();
byte[] raw = readGitDirectoryFile(Constants.CHERRY_PICK_HEAD);
if (raw == null)
return null;
return ObjectId.fromString(raw, 0);
}
/**
* Return the information stored in the file $GIT_DIR/REVERT_HEAD.
*
* @return object id from REVERT_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 NoWorkTreeException
* if this is bare, which implies it has no working directory.
* See {@link #isBare()}.
*/
public ObjectId readRevertHead() throws IOException, NoWorkTreeException {
if (isBare() || getDirectory() == null)
throw new NoWorkTreeException();
byte[] raw = readGitDirectoryFile(Constants.REVERT_HEAD);
if (raw == null)
return null;
return ObjectId.fromString(raw, 0);
}
/**
* Write cherry pick commit into $GIT_DIR/CHERRY_PICK_HEAD. This is used in
* case of conflicts to store the cherry which was tried to be picked.
*
* @param head
* an object id of the cherry commit or null
to
* delete the file
* @throws IOException
*/
public void writeCherryPickHead(ObjectId head) throws IOException {
List heads = (head != null) ? Collections.singletonList(head)
: null;
writeHeadsFile(heads, Constants.CHERRY_PICK_HEAD);
}
/**
* Write revert commit into $GIT_DIR/REVERT_HEAD. This is used in case of
* conflicts to store the revert which was tried to be picked.
*
* @param head
* an object id of the revert commit or null
to
* delete the file
* @throws IOException
*/
public void writeRevertHead(ObjectId head) throws IOException {
List heads = (head != null) ? Collections.singletonList(head)
: null;
writeHeadsFile(heads, Constants.REVERT_HEAD);
}
/**
* Write original HEAD commit into $GIT_DIR/ORIG_HEAD.
*
* @param head
* an object id of the original HEAD commit or null
* to delete the file
* @throws IOException
*/
public void writeOrigHead(ObjectId head) throws IOException {
List heads = head != null ? Collections.singletonList(head)
: null;
writeHeadsFile(heads, Constants.ORIG_HEAD);
}
/**
* Return the information stored in the file $GIT_DIR/ORIG_HEAD.
*
* @return object id from ORIG_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 NoWorkTreeException
* if this is bare, which implies it has no working directory.
* See {@link #isBare()}.
*/
public ObjectId readOrigHead() throws IOException, NoWorkTreeException {
if (isBare() || getDirectory() == null)
throw new NoWorkTreeException();
byte[] raw = readGitDirectoryFile(Constants.ORIG_HEAD);
return raw != null ? ObjectId.fromString(raw, 0) : null;
}
/**
* Return the information stored in the file $GIT_DIR/SQUASH_MSG. In this
* file operations triggering a squashed merge will store a template for the
* commit message of the squash commit.
*
* @return a String containing the content of the SQUASH_MSG file or
* {@code null} if this file doesn't exist
* @throws IOException
* @throws NoWorkTreeException
* if this is bare, which implies it has no working directory.
* See {@link #isBare()}.
*/
public String readSquashCommitMsg() throws IOException {
return readCommitMsgFile(Constants.SQUASH_MSG);
}
/**
* Write new content to the file $GIT_DIR/SQUASH_MSG. In this file
* operations triggering a squashed merge will store a template for the
* commit message of the squash commit. If null
is specified as
* message the file will be deleted.
*
* @param msg
* the message which should be written or null
to
* delete the file
*
* @throws IOException
*/
public void writeSquashCommitMsg(String msg) throws IOException {
File squashMsgFile = new File(gitDir, Constants.SQUASH_MSG);
writeCommitMsg(squashMsgFile, msg);
}
private String readCommitMsgFile(String msgFilename) throws IOException {
if (isBare() || getDirectory() == null)
throw new NoWorkTreeException();
File mergeMsgFile = new File(getDirectory(), msgFilename);
try {
return RawParseUtils.decode(IO.readFully(mergeMsgFile));
} catch (FileNotFoundException e) {
// the file has disappeared in the meantime ignore it
return null;
}
}
private void writeCommitMsg(File msgFile, String msg) throws IOException {
if (msg != null) {
FileOutputStream fos = new FileOutputStream(msgFile);
try {
fos.write(msg.getBytes(Constants.CHARACTER_ENCODING));
} finally {
fos.close();
}
} else {
FileUtils.delete(msgFile, FileUtils.SKIP_MISSING);
}
}
/**
* Read a file from the git directory.
*
* @param filename
* @return the raw contents or null if the file doesn't exist or is empty
* @throws IOException
*/
private byte[] readGitDirectoryFile(String filename) throws IOException {
File file = new File(getDirectory(), filename);
try {
byte[] raw = IO.readFully(file);
return raw.length > 0 ? raw : null;
} catch (FileNotFoundException notFound) {
return null;
}
}
/**
* Write the given heads to a file in the git directory.
*
* @param heads
* a list of object ids to write or null if the file should be
* deleted.
* @param filename
* @throws FileNotFoundException
* @throws IOException
*/
private void writeHeadsFile(List heads, String filename)
throws FileNotFoundException, IOException {
File headsFile = new File(getDirectory(), filename);
if (heads != null) {
BufferedOutputStream bos = new SafeBufferedOutputStream(
new FileOutputStream(headsFile));
try {
for (ObjectId id : heads) {
id.copyTo(bos);
bos.write('\n');
}
} finally {
bos.close();
}
} else {
FileUtils.delete(headsFile, FileUtils.SKIP_MISSING);
}
}
}