/* * Copyright (C) 2007, Dave Watson * Copyright (C) 2008-2010, Google Inc. * Copyright (C) 2006-2010, Robin Rosenberg * Copyright (C) 2006-2008, Shawn O. Pearce * 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.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.ReflogEntry; import org.eclipse.jgit.storage.file.ReflogReader; 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, IOException { RevWalk rw = new RevWalk(this); try { return resolve(rw, revstr); } finally { rw.release(); } } private ObjectId resolve(final RevWalk rw, final String revstr) throws IOException { char[] rev = revstr.toCharArray(); RevObject ref = null; 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; } int dist; if (l - i > 1) { String distnum = new String(rev, 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) 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) { String refName = new String(rev, 0, i); Ref resolved = getRefDatabase().getRef(refName); if (resolved == null) return null; ref = resolveReflog(rw, resolved, time); i = m; } else i = m - 1; break; case ':': { RevTree tree; if (ref == null) { // We might not yet have parsed the left hand side. ObjectId id; try { if (i == 0) id = resolve(rw, Constants.HEAD); else id = resolve(rw, new String(rev, 0, i)); } catch (RevisionSyntaxException badSyntax) { throw new RevisionSyntaxException(revstr); } if (id == null) return null; tree = rw.parseTree(id); } else { tree = rw.parseTree(ref); } if (i == rev.length - 1) return tree.copy(); TreeWalk tw = TreeWalk.forPath(rw.getObjectReader(), new String(rev, i + 1, rev.length - i - 1), tree); return tw != null ? tw.getObjectId(0) : null; } default: if (ref != null) throw new RevisionSyntaxException(revstr); } } return ref != null ? ref.copy() : resolveSimple(revstr); } 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); Ref r = getRefDatabase().getRef(revstr); if (r != null) return r.getObjectId(); if (AbbreviatedObjectId.isId(revstr)) return resolveAbbreviation(revstr); int dashg = revstr.indexOf("-g"); 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 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)); } if (number < 0) throw new RevisionSyntaxException(MessageFormat.format( JGitText.get().invalidReflogRevision, time)); 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(); } public String toString() { String desc; if (getDirectory() != null) desc = getDirectory().getPath(); else desc = getClass().getSimpleName() + "-" + System.identityHashCode(this); return "Repository[" + desc + "]"; } /** * 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()) 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; } 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 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 { if (isBare() || getDirectory() == null) throw new NoWorkTreeException(); File mergeMsgFile = new File(getDirectory(), Constants.MERGE_MSG); try { return RawParseUtils.decode(IO.readFully(mergeMsgFile)); } catch (FileNotFoundException e) { // MERGE_MSG file has disappeared in the meantime // ignore it return null; } } /** * 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); if (msg != null) { FileOutputStream fos = new FileOutputStream(mergeMsgFile); try { fos.write(msg.getBytes(Constants.CHARACTER_ENCODING)); } finally { fos.close(); } } else { FileUtils.delete(mergeMsgFile, FileUtils.SKIP_MISSING); } } /** * 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); } /** * 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 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; } /** * 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); } } }