/*
* Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
* Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
* Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
*
* 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 Git Development Community 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.spearce.jgit.lib;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
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.spearce.jgit.errors.ConfigInvalidException;
import org.spearce.jgit.errors.IncorrectObjectTypeException;
import org.spearce.jgit.errors.RevisionSyntaxException;
import org.spearce.jgit.util.FS;
import org.spearce.jgit.util.SystemReader;
/**
* Represents a Git repository. A repository holds all objects and refs used for
* managing source code (could by any type of file, but source code is what
* SCM's are typically used for).
*
* In Git terms all data is stored in GIT_DIR, typically a directory called
* .git. A work tree is maintained unless the repository is a bare repository.
* Typically the .git directory is located at the root of the work dir.
*
* <ul>
* <li>GIT_DIR
* <ul>
* <li>objects/ - objects</li>
* <li>refs/ - tags and heads</li>
* <li>config - configuration</li>
* <li>info/ - more configurations</li>
* </ul>
* </li>
* </ul>
* <p>
* This class is thread-safe.
* <p>
* This implementation only handles a subtly undocumented subset of git features.
*
*/
public class Repository {
private final AtomicInteger useCnt = new AtomicInteger(1);
private final File gitDir;
private final RepositoryConfig config;
private final RefDatabase refs;
private final ObjectDirectory objectDatabase;
private GitIndex index;
private final List<RepositoryListener> listeners = new Vector<RepositoryListener>(); // thread safe
static private final List<RepositoryListener> allListeners = new Vector<RepositoryListener>(); // thread safe
/**
* Construct a representation of a Git repository.
*
* @param d
* GIT_DIR (the location of the repository metadata).
* @throws IOException
* the repository appears to already exist but cannot be
* accessed.
*/
public Repository(final File d) throws IOException {
gitDir = d.getAbsoluteFile();
refs = new RefDatabase(this);
objectDatabase = new ObjectDirectory(FS.resolve(gitDir, "objects"));
final FileBasedConfig userConfig;
userConfig = SystemReader.getInstance().openUserConfig();
try {
userConfig.load();
} catch (ConfigInvalidException e1) {
IOException e2 = new IOException("User config file "
+ userConfig.getFile().getAbsolutePath() + " invalid: "
+ e1);
e2.initCause(e1);
throw e2;
}
config = new RepositoryConfig(userConfig, FS.resolve(gitDir, "config"));
if (objectDatabase.exists()) {
try {
getConfig().load();
} catch (ConfigInvalidException e1) {
IOException e2 = new IOException("Unknown repository format");
e2.initCause(e1);
throw e2;
}
final String repositoryFormatVersion = getConfig().getString(
"core", null, "repositoryFormatVersion");
if (!"0".equals(repositoryFormatVersion)) {
throw new IOException("Unknown repository format \""
+ repositoryFormatVersion + "\"; expected \"0\".");
}
}
}
/**
* Create a new Git repository initializing the necessary files and
* directories. Repository with working tree is created using this method.
*
* @throws IOException
* @see #create(boolean)
*/
public synchronized 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 void create(boolean bare) throws IOException {
final RepositoryConfig cfg = getConfig();
if (cfg.getFile().exists()) {
throw new IllegalStateException("Repository already exists: "
+ gitDir);
}
gitDir.mkdirs();
refs.create();
objectDatabase.create();
new File(gitDir, "branches").mkdir();
new File(gitDir, "remotes").mkdir();
final String master = Constants.R_HEADS + Constants.MASTER;
refs.link(Constants.HEAD, master);
cfg.setInt("core", null, "repositoryformatversion", 0);
cfg.setBoolean("core", null, "filemode", true);
if (bare)
cfg.setBoolean("core", null, "bare", true);
cfg.save();
}
/**
* @return GIT_DIR
*/
public File getDirectory() {
return gitDir;
}
/**
* @return the directory containing the objects owned by this repository.
*/
public File getObjectsDirectory() {
return objectDatabase.getDirectory();
}
/**
* @return the object database which stores this repository's data.
*/
public ObjectDatabase getObjectDatabase() {
return objectDatabase;
}
/**
* @return the configuration of this repository
*/
public RepositoryConfig getConfig() {
return config;
}
/**
* 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 File toFile(final AnyObjectId objectId) {
return objectDatabase.fileFor(objectId);
}
/**
* @param objectId
* @return true if the specified object is stored in this repo or any of the
* known shared repositories.
*/
public boolean hasObject(final AnyObjectId objectId) {
return objectDatabase.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(final WindowCursor curs, final AnyObjectId id)
throws IOException {
return objectDatabase.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 Collection<PackedObjectLoader> openObjectInAllPacks(
final AnyObjectId objectId, final WindowCursor curs)
throws IOException {
Collection<PackedObjectLoader> result = new LinkedList<PackedObjectLoader>();
openObjectInAllPacks(objectId, result, curs);
return result;
}
/**
* 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
*/
void openObjectInAllPacks(final AnyObjectId objectId,
final Collection<PackedObjectLoader> resultLoaders,
final WindowCursor curs) throws IOException {
objectDatabase.openObjectInAllPacks(resultLoaders, curs, objectId);
}
/**
* @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,
"COMMIT nor TREE nor BLOB nor TAG");
}
}
/**
* 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 refs.newUpdate(ref);
}
/**
* 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 refs.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();
Object ref = null;
ObjectId refId = null;
for (int i = 0; i < rev.length; ++i) {
switch (rev[i]) {
case '^':
if (refId == null) {
String refstr = new String(rev,0,i);
refId = resolveSimple(refstr);
if (refId == 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 = mapObject(refId, null);
while (ref instanceof Tag) {
Tag tag = (Tag)ref;
refId = tag.getObjId();
ref = mapObject(refId, null);
}
if (!(ref instanceof Commit))
throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
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(
"Invalid commit parent number",
revstr);
}
if (pnum != 0) {
final ObjectId parents[] = ((Commit) ref)
.getParentIds();
if (pnum > parents.length)
refId = null;
else
refId = parents[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 = mapObject(refId, null);
while (ref instanceof Tag) {
Tag t = (Tag)ref;
refId = t.getObjId();
ref = mapObject(refId, null);
}
if (ref instanceof Treeish)
refId = ((Treeish)ref).getTreeId();
else
throw new IncorrectObjectTypeException(refId, Constants.TYPE_TREE);
}
else if (item.equals("commit")) {
ref = mapObject(refId, null);
while (ref instanceof Tag) {
Tag t = (Tag)ref;
refId = t.getObjId();
ref = mapObject(refId, null);
}
if (!(ref instanceof Commit))
throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
}
else if (item.equals("blob")) {
ref = mapObject(refId, null);
while (ref instanceof Tag) {
Tag t = (Tag)ref;
refId = t.getObjId();
ref = mapObject(refId, null);
}
if (!(ref instanceof byte[]))
throw new IncorrectObjectTypeException(refId, Constants.TYPE_BLOB);
}
else if (item.equals("")) {
ref = mapObject(refId, null);
while (ref instanceof Tag) {
Tag t = (Tag)ref;
refId = t.getObjId();
ref = mapObject(refId, null);
}
}
else
throw new RevisionSyntaxException(revstr);
else
throw new RevisionSyntaxException(revstr);
break;
default:
ref = mapObject(refId, null);
if (ref instanceof Commit) {
final ObjectId parents[] = ((Commit) ref)
.getParentIds();
if (parents.length == 0)
refId = null;
else
refId = parents[0];
} else
throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
}
} else {
ref = mapObject(refId, null);
while (ref instanceof Tag) {
Tag tag = (Tag)ref;
refId = tag.getObjId();
ref = mapObject(refId, null);
}
if (ref instanceof Commit) {
final ObjectId parents[] = ((Commit) ref)
.getParentIds();
if (parents.length == 0)
refId = null;
else
refId = parents[0];
} else
throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
}
break;
case '~':
if (ref == null) {
String refstr = new String(rev,0,i);
refId = resolveSimple(refstr);
if (refId == null)
return null;
ref = mapObject(refId, null);
}
while (ref instanceof Tag) {
Tag tag = (Tag)ref;
refId = tag.getObjId();
ref = mapObject(refId, null);
}
if (!(ref instanceof Commit))
throw new IncorrectObjectTypeException(refId, 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(
"Invalid ancestry length", revstr);
}
while (dist > 0) {
final ObjectId[] parents = ((Commit) ref).getParentIds();
if (parents.length == 0) {
refId = null;
break;
}
refId = parents[0];
ref = mapCommit(refId);
--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("reflogs not yet supported by revision parser", revstr);
i = m - 1;
break;
default:
if (refId != null)
throw new RevisionSyntaxException(revstr);
}
}
if (refId == null)
refId = resolveSimple(revstr);
return refId;
}
private ObjectId resolveSimple(final String revstr) throws IOException {
if (ObjectId.isId(revstr))
return ObjectId.fromString(revstr);
final Ref r = refs.readRef(revstr);
return r != null ? r.getObjectId() : null;
}
/** Increment the use counter by one, requiring a matched {@link #close()}. */
public void incrementOpen() {
useCnt.incrementAndGet();
}
/**
* Close all resources used by this repository
*/
public void close() {
if (useCnt.decrementAndGet() == 0)
objectDatabase.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 void openPack(final File pack, final File idx) throws IOException {
objectDatabase.openPack(pack, idx);
}
/**
* Writes a symref (e.g. HEAD) to disk
*
* @param name symref name
* @param target pointed to ref
* @throws IOException
*/
public void writeSymref(final String name, final String target)
throws IOException {
refs.link(name, target);
}
public String toString() {
return "Repository[" + getDirectory() + "]";
}
/**
* @return name of current branch
* @throws IOException
*/
public String getFullBranch() throws IOException {
final File ptr = new File(getDirectory(),Constants.HEAD);
final BufferedReader br = new BufferedReader(new FileReader(ptr));
String ref;
try {
ref = br.readLine();
} finally {
br.close();
}
if (ref.startsWith("ref: "))
ref = ref.substring(5);
return ref;
}
/**
* @return name of current branch.
* @throws IOException
*/
public String getBranch() throws IOException {
try {
final File ptr = new File(getDirectory(), Constants.HEAD);
final BufferedReader br = new BufferedReader(new FileReader(ptr));
String ref;
try {
ref = br.readLine();
} finally {
br.close();
}
if (ref.startsWith("ref: "))
ref = ref.substring(5);
if (ref.startsWith("refs/heads/"))
ref = ref.substring(11);
return ref;
} catch (FileNotFoundException e) {
final File ptr = new File(getDirectory(),"head-name");
final BufferedReader br = new BufferedReader(new FileReader(ptr));
String ref;
try {
ref = br.readLine();
} finally {
br.close();
}
return ref;
}
}
/**
* 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 refs.readRef(name);
}
/**
* @return all known refs (heads, tags, remotes).
*/
public Map<String, Ref> getAllRefs() {
return refs.getAllRefs();
}
/**
* @return 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() {
return refs.getTags();
}
/**
* Peel a possibly unpeeled ref and updates it.
* <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) {
return refs.peel(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()) {
if (!ref.isPeeled())
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;
}
/** Clean up stale caches */
public void refreshFromDisk() {
refs.clearCache();
}
/**
* @return a representation of the index associated with this repo
* @throws IOException
*/
public GitIndex getIndex() throws IOException {
if (index == null) {
index = new GitIndex(this);
index.read();
} else {
index.rereadIfNecessary();
}
return index;
}
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() {
// Pre Git-1.6 logic
if (new File(getWorkDir(), ".dotest").exists())
return RepositoryState.REBASING;
if (new File(gitDir,".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(gitDir,"MERGE_HEAD").exists())
return RepositoryState.MERGING;
if (new File(gitDir,"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 workdir file, i.e. where the files are checked out
*/
public File getWorkDir() {
return getDirectory().getParentFile();
}
/**
* 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 fireRefsMaybeChanged() {
if (refs.lastRefModification != refs.lastNotifiedRefModification) {
refs.lastNotifiedRefModification = refs.lastRefModification;
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 void scanForRepoChanges() throws IOException {
getAllRefs(); // This will look for changes to refs
getIndex(); // This will detect changes in the index
}
/**
* @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 ReflogReader getReflogReader(String refName) throws IOException {
Ref ref = getRef(refName);
if (ref != null)
return new ReflogReader(this, ref.getOrigName());
return null;
}
}