Fix Repository.mapObject() for missing objects
[egit/zawir.git] / org.spearce.jgit / src / org / spearce / jgit / lib / Repository.java
bloba8591cc7e1e3df369e349354976cb9d5815f87bd
1 /*
2 * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
3 * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
4 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
6 * All rights reserved.
8 * Redistribution and use in source and binary forms, with or
9 * without modification, are permitted provided that the following
10 * conditions are met:
12 * - Redistributions of source code must retain the above copyright
13 * notice, this list of conditions and the following disclaimer.
15 * - Redistributions in binary form must reproduce the above
16 * copyright notice, this list of conditions and the following
17 * disclaimer in the documentation and/or other materials provided
18 * with the distribution.
20 * - Neither the name of the Git Development Community nor the
21 * names of its contributors may be used to endorse or promote
22 * products derived from this software without specific prior
23 * written permission.
25 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
26 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
27 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
28 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
29 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
30 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
31 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
32 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
33 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
34 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
35 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
36 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
37 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
40 package org.spearce.jgit.lib;
42 import java.io.BufferedReader;
43 import java.io.File;
44 import java.io.FileNotFoundException;
45 import java.io.FileReader;
46 import java.io.FilenameFilter;
47 import java.io.IOException;
48 import java.util.ArrayList;
49 import java.util.Collection;
50 import java.util.HashMap;
51 import java.util.LinkedList;
52 import java.util.List;
53 import java.util.Map;
54 import java.util.Vector;
56 import org.spearce.jgit.errors.IncorrectObjectTypeException;
57 import org.spearce.jgit.errors.RevisionSyntaxException;
58 import org.spearce.jgit.stgit.StGitPatch;
59 import org.spearce.jgit.util.FS;
61 /**
62 * Represents a Git repository. A repository holds all objects and refs used for
63 * managing source code (could by any type of file, but source code is what
64 * SCM's are typically used for).
66 * In Git terms all data is stored in GIT_DIR, typically a directory called
67 * .git. A work tree is maintained unless the repository is a bare repository.
68 * Typically the .git directory is located at the root of the work dir.
70 * <ul>
71 * <li>GIT_DIR
72 * <ul>
73 * <li>objects/ - objects</li>
74 * <li>refs/ - tags and heads</li>
75 * <li>config - configuration</li>
76 * <li>info/ - more configurations</li>
77 * </ul>
78 * </li>
79 * </ul>
81 * This implementation only handles a subtly undocumented subset of git features.
84 public class Repository {
85 private final File gitDir;
87 private final File[] objectsDirs;
89 private final RepositoryConfig config;
91 private final RefDatabase refs;
93 private PackFile[] packs;
95 private GitIndex index;
97 private List<RepositoryListener> listeners = new Vector<RepositoryListener>(); // thread safe
98 static private List<RepositoryListener> allListeners = new Vector<RepositoryListener>(); // thread safe
101 * Construct a representation of a Git repository.
103 * @param d
104 * GIT_DIR (the location of the repository metadata).
105 * @throws IOException
106 * the repository appears to already exist but cannot be
107 * accessed.
109 public Repository(final File d) throws IOException {
110 gitDir = d.getAbsoluteFile();
111 try {
112 objectsDirs = readObjectsDirs(FS.resolve(gitDir, "objects"),
113 new ArrayList<File>()).toArray(new File[0]);
114 } catch (IOException e) {
115 IOException ex = new IOException("Cannot find all object dirs for " + gitDir);
116 ex.initCause(e);
117 throw ex;
119 refs = new RefDatabase(this);
120 packs = new PackFile[0];
121 config = new RepositoryConfig(this);
123 final boolean isExisting = objectsDirs[0].exists();
124 if (isExisting) {
125 getConfig().load();
126 final String repositoryFormatVersion = getConfig().getString(
127 "core", null, "repositoryFormatVersion");
128 if (!"0".equals(repositoryFormatVersion)) {
129 throw new IOException("Unknown repository format \""
130 + repositoryFormatVersion + "\"; expected \"0\".");
132 } else {
133 getConfig().create();
135 if (isExisting)
136 scanForPacks();
139 private Collection<File> readObjectsDirs(File objectsDir, Collection<File> ret) throws IOException {
140 ret.add(objectsDir);
141 final File altFile = FS.resolve(objectsDir, "info/alternates");
142 if (altFile.exists()) {
143 BufferedReader ar = new BufferedReader(new FileReader(altFile));
144 for (String alt=ar.readLine(); alt!=null; alt=ar.readLine()) {
145 readObjectsDirs(FS.resolve(objectsDir, alt), ret);
147 ar.close();
149 return ret;
153 * Create a new Git repository initializing the necessary files and
154 * directories.
156 * @throws IOException
158 public void create() throws IOException {
159 if (gitDir.exists()) {
160 throw new IllegalStateException("Repository already exists: "
161 + gitDir);
164 gitDir.mkdirs();
165 refs.create();
167 objectsDirs[0].mkdirs();
168 new File(objectsDirs[0], "pack").mkdir();
169 new File(objectsDirs[0], "info").mkdir();
171 new File(gitDir, "branches").mkdir();
172 new File(gitDir, "remotes").mkdir();
173 final String master = Constants.HEADS_PREFIX + "/" + Constants.MASTER;
174 refs.link(Constants.HEAD, master);
176 getConfig().create();
177 getConfig().save();
181 * @return GIT_DIR
183 public File getDirectory() {
184 return gitDir;
188 * @return the directory containg the objects owned by this repository.
190 public File getObjectsDirectory() {
191 return objectsDirs[0];
195 * @return the configuration of this repository
197 public RepositoryConfig getConfig() {
198 return config;
202 * Construct a filename where the loose object having a specified SHA-1
203 * should be stored. If the object is stored in a shared repository the path
204 * to the alternative repo will be returned. If the object is not yet store
205 * a usable path in this repo will be returned. It is assumed that callers
206 * will look for objects in a pack first.
208 * @param objectId
209 * @return suggested file name
211 public File toFile(final AnyObjectId objectId) {
212 final String n = objectId.toString();
213 String d=n.substring(0, 2);
214 String f=n.substring(2);
215 for (int i=0; i<objectsDirs.length; ++i) {
216 File ret = new File(new File(objectsDirs[i], d), f);
217 if (ret.exists())
218 return ret;
220 return new File(new File(objectsDirs[0], d), f);
224 * @param objectId
225 * @return true if the specified object is stored in this repo or any of the
226 * known shared repositories.
228 public boolean hasObject(final AnyObjectId objectId) {
229 int k = packs.length;
230 if (k > 0) {
231 do {
232 if (packs[--k].hasObject(objectId))
233 return true;
234 } while (k > 0);
236 return toFile(objectId).isFile();
240 * @param id
241 * SHA-1 of an object.
243 * @return a {@link ObjectLoader} for accessing the data of the named
244 * object, or null if the object does not exist.
245 * @throws IOException
247 public ObjectLoader openObject(final AnyObjectId id)
248 throws IOException {
249 return openObject(new WindowCursor(),id);
253 * @param curs
254 * temporary working space associated with the calling thread.
255 * @param id
256 * SHA-1 of an object.
258 * @return a {@link ObjectLoader} for accessing the data of the named
259 * object, or null if the object does not exist.
260 * @throws IOException
262 public ObjectLoader openObject(final WindowCursor curs, final AnyObjectId id)
263 throws IOException {
264 int k = packs.length;
265 if (k > 0) {
266 do {
267 try {
268 final ObjectLoader ol = packs[--k].get(curs, id);
269 if (ol != null)
270 return ol;
271 } catch (IOException ioe) {
272 // This shouldn't happen unless the pack was corrupted
273 // after we opened it or the VM runs out of memory. This is
274 // a know problem with memory mapped I/O in java and have
275 // been noticed with JDK < 1.6. Tell the gc that now is a good
276 // time to collect and try once more.
277 try {
278 curs.release();
279 System.gc();
280 final ObjectLoader ol = packs[k].get(curs, id);
281 if (ol != null)
282 return ol;
283 } catch (IOException ioe2) {
284 ioe2.printStackTrace();
285 ioe.printStackTrace();
286 // Still fails.. that's BAD, maybe the pack has
287 // been corrupted after all, or the gc didn't manage
288 // to release enough previously mmaped areas.
291 } while (k > 0);
293 try {
294 return new UnpackedObjectLoader(this, id.toObjectId());
295 } catch (FileNotFoundException fnfe) {
296 return null;
301 * Open object in all packs containing specified object.
303 * @param objectId
304 * id of object to search for
305 * @param curs
306 * temporary working space associated with the calling thread.
307 * @return collection of loaders for this object, from all packs containing
308 * this object
309 * @throws IOException
311 public Collection<PackedObjectLoader> openObjectInAllPacks(
312 final AnyObjectId objectId, final WindowCursor curs)
313 throws IOException {
314 Collection<PackedObjectLoader> result = new LinkedList<PackedObjectLoader>();
315 openObjectInAllPacks(objectId, result, curs);
316 return result;
320 * Open object in all packs containing specified object.
322 * @param objectId
323 * id of object to search for
324 * @param resultLoaders
325 * result collection of loaders for this object, filled with
326 * loaders from all packs containing specified object
327 * @param curs
328 * temporary working space associated with the calling thread.
329 * @throws IOException
331 void openObjectInAllPacks(final AnyObjectId objectId,
332 final Collection<PackedObjectLoader> resultLoaders,
333 final WindowCursor curs) throws IOException {
334 for (PackFile pack : packs) {
335 final PackedObjectLoader loader = pack.get(curs, objectId);
336 if (loader != null)
337 resultLoaders.add(loader);
342 * @param id
343 * SHA'1 of a blob
344 * @return an {@link ObjectLoader} for accessing the data of a named blob
345 * @throws IOException
347 public ObjectLoader openBlob(final ObjectId id) throws IOException {
348 return openObject(id);
352 * @param id
353 * SHA'1 of a tree
354 * @return an {@link ObjectLoader} for accessing the data of a named tree
355 * @throws IOException
357 public ObjectLoader openTree(final ObjectId id) throws IOException {
358 return openObject(id);
362 * Access a Commit object using a symbolic reference. This reference may
363 * be a SHA-1 or ref in combination with a number of symbols translating
364 * from one ref or SHA1-1 to another, such as HEAD^ etc.
366 * @param revstr a reference to a git commit object
367 * @return a Commit named by the specified string
368 * @throws IOException for I/O error or unexpected object type.
370 * @see #resolve(String)
372 public Commit mapCommit(final String revstr) throws IOException {
373 final ObjectId id = resolve(revstr);
374 return id != null ? mapCommit(id) : null;
378 * Access any type of Git object by id and
380 * @param id
381 * SHA-1 of object to read
382 * @param refName optional, only relevant for simple tags
383 * @return The Git object if found or null
384 * @throws IOException
386 public Object mapObject(final ObjectId id, final String refName) throws IOException {
387 final ObjectLoader or = openObject(id);
388 if (or == null)
389 return null;
390 final byte[] raw = or.getBytes();
391 if (or.getType() == Constants.OBJ_TREE)
392 return makeTree(id, raw);
393 if (or.getType() == Constants.OBJ_COMMIT)
394 return makeCommit(id, raw);
395 if (or.getType() == Constants.OBJ_TAG)
396 return makeTag(id, refName, raw);
397 if (or.getType() == Constants.OBJ_BLOB)
398 return raw;
399 throw new IncorrectObjectTypeException(id,
400 "COMMIT nor TREE nor BLOB nor TAG");
404 * Access a Commit by SHA'1 id.
405 * @param id
406 * @return Commit or null
407 * @throws IOException for I/O error or unexpected object type.
409 public Commit mapCommit(final ObjectId id) throws IOException {
410 final ObjectLoader or = openObject(id);
411 if (or == null)
412 return null;
413 final byte[] raw = or.getBytes();
414 if (Constants.OBJ_COMMIT == or.getType())
415 return new Commit(this, id, raw);
416 throw new IncorrectObjectTypeException(id, Constants.TYPE_COMMIT);
419 private Commit makeCommit(final ObjectId id, final byte[] raw) {
420 Commit ret = new Commit(this, id, raw);
421 return ret;
425 * Access a Tree object using a symbolic reference. This reference may
426 * be a SHA-1 or ref in combination with a number of symbols translating
427 * from one ref or SHA1-1 to another, such as HEAD^{tree} etc.
429 * @param revstr a reference to a git commit object
430 * @return a Tree named by the specified string
431 * @throws IOException
433 * @see #resolve(String)
435 public Tree mapTree(final String revstr) throws IOException {
436 final ObjectId id = resolve(revstr);
437 return id != null ? mapTree(id) : null;
441 * Access a Tree by SHA'1 id.
442 * @param id
443 * @return Tree or null
444 * @throws IOException for I/O error or unexpected object type.
446 public Tree mapTree(final ObjectId id) throws IOException {
447 final ObjectLoader or = openObject(id);
448 if (or == null)
449 return null;
450 final byte[] raw = or.getBytes();
451 if (Constants.OBJ_TREE == or.getType()) {
452 return new Tree(this, id, raw);
454 if (Constants.OBJ_COMMIT == or.getType())
455 return mapTree(ObjectId.fromString(raw, 5));
456 throw new IncorrectObjectTypeException(id, Constants.TYPE_TREE);
459 private Tree makeTree(final ObjectId id, final byte[] raw) throws IOException {
460 Tree ret = new Tree(this, id, raw);
461 return ret;
464 private Tag makeTag(final ObjectId id, final String refName, final byte[] raw) {
465 Tag ret = new Tag(this, id, refName, raw);
466 return ret;
470 * Access a tag by symbolic name.
472 * @param revstr
473 * @return a Tag or null
474 * @throws IOException on I/O error or unexpected type
476 public Tag mapTag(String revstr) throws IOException {
477 final ObjectId id = resolve(revstr);
478 return id != null ? mapTag(revstr, id) : null;
482 * Access a Tag by SHA'1 id
483 * @param refName
484 * @param id
485 * @return Commit or null
486 * @throws IOException for I/O error or unexpected object type.
488 public Tag mapTag(final String refName, final ObjectId id) throws IOException {
489 final ObjectLoader or = openObject(id);
490 if (or == null)
491 return null;
492 final byte[] raw = or.getBytes();
493 if (Constants.OBJ_TAG == or.getType())
494 return new Tag(this, id, refName, raw);
495 return new Tag(this, id, refName, null);
499 * Create a command to update, create or delete a ref in this repository.
501 * @param ref
502 * name of the ref the caller wants to modify.
503 * @return an update command. The caller must finish populating this command
504 * and then invoke one of the update methods to actually make a
505 * change.
506 * @throws IOException
507 * a symbolic ref was passed in and could not be resolved back
508 * to the base ref, as the symbolic ref could not be read.
510 public RefUpdate updateRef(final String ref) throws IOException {
511 return refs.newUpdate(ref);
515 * Parse a git revision string and return an object id.
517 * Currently supported is combinations of these.
518 * <ul>
519 * <li>SHA-1 - a SHA-1</li>
520 * <li>refs/... - a ref name</li>
521 * <li>ref^n - nth parent reference</li>
522 * <li>ref~n - distance via parent reference</li>
523 * <li>ref@{n} - nth version of ref</li>
524 * <li>ref^{tree} - tree references by ref</li>
525 * <li>ref^{commit} - commit references by ref</li>
526 * </ul>
528 * Not supported is
529 * <ul>
530 * <li>timestamps in reflogs, ref@{full or relative timestamp}</li>
531 * <li>abbreviated SHA-1's</li>
532 * </ul>
534 * @param revstr A git object references expression
535 * @return an ObjectId or null if revstr can't be resolved to any ObjectId
536 * @throws IOException on serious errors
538 public ObjectId resolve(final String revstr) throws IOException {
539 char[] rev = revstr.toCharArray();
540 Object ref = null;
541 ObjectId refId = null;
542 for (int i = 0; i < rev.length; ++i) {
543 switch (rev[i]) {
544 case '^':
545 if (refId == null) {
546 String refstr = new String(rev,0,i);
547 refId = resolveSimple(refstr);
548 if (refId == null)
549 return null;
551 if (i + 1 < rev.length) {
552 switch (rev[i + 1]) {
553 case '0':
554 case '1':
555 case '2':
556 case '3':
557 case '4':
558 case '5':
559 case '6':
560 case '7':
561 case '8':
562 case '9':
563 int j;
564 ref = mapObject(refId, null);
565 if (!(ref instanceof Commit))
566 throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
567 for (j=i+1; j<rev.length; ++j) {
568 if (!Character.isDigit(rev[j]))
569 break;
571 String parentnum = new String(rev, i+1, j-i-1);
572 int pnum = Integer.parseInt(parentnum);
573 if (pnum != 0)
574 refId = ((Commit)ref).getParentIds()[pnum - 1];
575 i = j - 1;
576 break;
577 case '{':
578 int k;
579 String item = null;
580 for (k=i+2; k<rev.length; ++k) {
581 if (rev[k] == '}') {
582 item = new String(rev, i+2, k-i-2);
583 break;
586 i = k;
587 if (item != null)
588 if (item.equals("tree")) {
589 ref = mapObject(refId, null);
590 while (ref instanceof Tag) {
591 Tag t = (Tag)ref;
592 refId = t.getObjId();
593 ref = mapObject(refId, null);
595 if (ref instanceof Treeish)
596 refId = ((Treeish)ref).getTreeId();
597 else
598 throw new IncorrectObjectTypeException(refId, Constants.TYPE_TREE);
600 else if (item.equals("commit")) {
601 ref = mapObject(refId, null);
602 while (ref instanceof Tag) {
603 Tag t = (Tag)ref;
604 refId = t.getObjId();
605 ref = mapObject(refId, null);
607 if (!(ref instanceof Commit))
608 throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
610 else if (item.equals("blob")) {
611 ref = mapObject(refId, null);
612 while (ref instanceof Tag) {
613 Tag t = (Tag)ref;
614 refId = t.getObjId();
615 ref = mapObject(refId, null);
617 if (!(ref instanceof byte[]))
618 throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
620 else if (item.equals("")) {
621 ref = mapObject(refId, null);
622 if (ref instanceof Tag)
623 refId = ((Tag)ref).getObjId();
624 else {
625 // self
628 else
629 throw new RevisionSyntaxException(revstr);
630 else
631 throw new RevisionSyntaxException(revstr);
632 break;
633 default:
634 ref = mapObject(refId, null);
635 if (ref instanceof Commit)
636 refId = ((Commit)ref).getParentIds()[0];
637 else
638 throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
641 } else {
642 ref = mapObject(refId, null);
643 if (ref instanceof Commit)
644 refId = ((Commit)ref).getParentIds()[0];
645 else
646 throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
648 break;
649 case '~':
650 if (ref == null) {
651 String refstr = new String(rev,0,i);
652 refId = resolveSimple(refstr);
653 ref = mapCommit(refId);
655 int l;
656 for (l = i + 1; l < rev.length; ++l) {
657 if (!Character.isDigit(rev[l]))
658 break;
660 String distnum = new String(rev, i+1, l-i-1);
661 int dist = Integer.parseInt(distnum);
662 while (dist >= 0) {
663 refId = ((Commit)ref).getParentIds()[0];
664 ref = mapCommit(refId);
665 --dist;
667 i = l - 1;
668 break;
669 case '@':
670 int m;
671 String time = null;
672 for (m=i+2; m<rev.length; ++m) {
673 if (rev[m] == '}') {
674 time = new String(rev, i+2, m-i-2);
675 break;
678 if (time != null)
679 throw new RevisionSyntaxException("reflogs not yet supported by revision parser yet", revstr);
680 i = m - 1;
681 break;
682 default:
683 if (refId != null)
684 throw new RevisionSyntaxException(revstr);
687 if (refId == null)
688 refId = resolveSimple(revstr);
689 return refId;
692 private ObjectId resolveSimple(final String revstr) throws IOException {
693 if (ObjectId.isId(revstr))
694 return ObjectId.fromString(revstr);
695 final Ref r = refs.readRef(revstr);
696 return r != null ? r.getObjectId() : null;
700 * Close all resources used by this repository
702 public void close() {
703 closePacks();
706 void closePacks() {
707 for (int k = packs.length - 1; k >= 0; k--) {
708 packs[k].close();
710 packs = new PackFile[0];
714 * Add a single existing pack to the list of available pack files.
716 * @param pack
717 * path of the pack file to open.
718 * @param idx
719 * path of the corresponding index file.
720 * @throws IOException
721 * index file could not be opened, read, or is not recognized as
722 * a Git pack file index.
724 public void openPack(final File pack, final File idx) throws IOException {
725 final String p = pack.getName();
726 final String i = idx.getName();
727 if (p.length() != 50 || !p.startsWith("pack-") || !p.endsWith(".pack"))
728 throw new IllegalArgumentException("Not a valid pack " + pack);
729 if (i.length() != 49 || !i.startsWith("pack-") || !i.endsWith(".idx"))
730 throw new IllegalArgumentException("Not a valid pack " + idx);
731 if (!p.substring(0,45).equals(i.substring(0,45)))
732 throw new IllegalArgumentException("Pack " + pack
733 + "does not match index " + idx);
735 final PackFile[] cur = packs;
736 final PackFile[] arr = new PackFile[cur.length + 1];
737 System.arraycopy(cur, 0, arr, 1, cur.length);
738 arr[0] = new PackFile(this, idx, pack);
739 packs = arr;
743 * Scan the object dirs, including alternates for packs
744 * to use.
746 public void scanForPacks() {
747 final ArrayList<PackFile> p = new ArrayList<PackFile>();
748 for (int i=0; i<objectsDirs.length; ++i)
749 scanForPacks(new File(objectsDirs[i], "pack"), p);
750 final PackFile[] arr = new PackFile[p.size()];
751 p.toArray(arr);
752 packs = arr;
755 private void scanForPacks(final File packDir, Collection<PackFile> packList) {
756 final String[] idxList = packDir.list(new FilenameFilter() {
757 public boolean accept(final File baseDir, final String n) {
758 // Must match "pack-[0-9a-f]{40}.idx" to be an index.
759 return n.length() == 49 && n.endsWith(".idx")
760 && n.startsWith("pack-");
763 if (idxList != null) {
764 for (final String indexName : idxList) {
765 final String n = indexName.substring(0, indexName.length() - 4);
766 final File idxFile = new File(packDir, n + ".idx");
767 final File packFile = new File(packDir, n + ".pack");
769 if (!packFile.isFile()) {
770 // Sometimes C Git's http fetch transport leaves a
771 // .idx file behind and does not download the .pack.
772 // We have to skip over such useless indexes.
774 continue;
777 try {
778 packList.add(new PackFile(this, idxFile, packFile));
779 } catch (IOException ioe) {
780 // Whoops. That's not a pack!
782 ioe.printStackTrace();
789 * Writes a symref (e.g. HEAD) to disk
791 * @param name symref name
792 * @param target pointed to ref
793 * @throws IOException
795 public void writeSymref(final String name, final String target)
796 throws IOException {
797 refs.link(name, target);
800 public String toString() {
801 return "Repository[" + getDirectory() + "]";
805 * @return name of topmost Stacked Git patch.
806 * @throws IOException
808 public String getPatch() throws IOException {
809 final File ptr = new File(getDirectory(),"patches/"+getBranch()+"/applied");
810 final BufferedReader br = new BufferedReader(new FileReader(ptr));
811 String last=null;
812 try {
813 String line;
814 while ((line=br.readLine())!=null) {
815 last = line;
817 } finally {
818 br.close();
820 return last;
824 * @return name of current branch
825 * @throws IOException
827 public String getFullBranch() throws IOException {
828 final File ptr = new File(getDirectory(),"HEAD");
829 final BufferedReader br = new BufferedReader(new FileReader(ptr));
830 String ref;
831 try {
832 ref = br.readLine();
833 } finally {
834 br.close();
836 if (ref.startsWith("ref: "))
837 ref = ref.substring(5);
838 return ref;
842 * @return name of current branch.
843 * @throws IOException
845 public String getBranch() throws IOException {
846 try {
847 final File ptr = new File(getDirectory(), Constants.HEAD);
848 final BufferedReader br = new BufferedReader(new FileReader(ptr));
849 String ref;
850 try {
851 ref = br.readLine();
852 } finally {
853 br.close();
855 if (ref.startsWith("ref: "))
856 ref = ref.substring(5);
857 if (ref.startsWith("refs/heads/"))
858 ref = ref.substring(11);
859 return ref;
860 } catch (FileNotFoundException e) {
861 final File ptr = new File(getDirectory(),"head-name");
862 final BufferedReader br = new BufferedReader(new FileReader(ptr));
863 String ref;
864 try {
865 ref = br.readLine();
866 } finally {
867 br.close();
869 return ref;
874 * @return all known refs (heads, tags, remotes).
876 public Map<String, Ref> getAllRefs() {
877 return refs.getAllRefs();
881 * @return all tags; key is short tag name ("v1.0") and value of the entry
882 * contains the ref with the full tag name ("refs/tags/v1.0").
884 public Map<String, Ref> getTags() {
885 return refs.getTags();
889 * @return true if HEAD points to a StGit patch.
891 public boolean isStGitMode() {
892 try {
893 File file = new File(getDirectory(), "HEAD");
894 BufferedReader reader = new BufferedReader(new FileReader(file));
895 String string = reader.readLine();
896 if (!string.startsWith("ref: refs/heads/"))
897 return false;
898 String branch = string.substring("ref: refs/heads/".length());
899 File currentPatches = new File(new File(new File(getDirectory(),
900 "patches"), branch), "applied");
901 if (!currentPatches.exists())
902 return false;
903 if (currentPatches.length() == 0)
904 return false;
905 return true;
907 } catch (IOException e) {
908 e.printStackTrace();
909 return false;
914 * @return applied patches in a map indexed on current commit id
915 * @throws IOException
917 public Map<ObjectId,StGitPatch> getAppliedPatches() throws IOException {
918 Map<ObjectId,StGitPatch> ret = new HashMap<ObjectId,StGitPatch>();
919 if (isStGitMode()) {
920 File patchDir = new File(new File(getDirectory(),"patches"),getBranch());
921 BufferedReader apr = new BufferedReader(new FileReader(new File(patchDir,"applied")));
922 for (String patchName=apr.readLine(); patchName!=null; patchName=apr.readLine()) {
923 File topFile = new File(new File(new File(patchDir,"patches"), patchName), "top");
924 BufferedReader tfr = new BufferedReader(new FileReader(topFile));
925 String objectId = tfr.readLine();
926 ObjectId id = ObjectId.fromString(objectId);
927 ret.put(id, new StGitPatch(patchName, id));
928 tfr.close();
930 apr.close();
932 return ret;
935 /** Clean up stale caches */
936 public void refreshFromDisk() {
937 refs.clearCache();
941 * @return a representation of the index associated with this repo
942 * @throws IOException
944 public GitIndex getIndex() throws IOException {
945 if (index == null) {
946 index = new GitIndex(this);
947 index.read();
948 } else {
949 index.rereadIfNecessary();
951 return index;
954 static byte[] gitInternalSlash(byte[] bytes) {
955 if (File.separatorChar == '/')
956 return bytes;
957 for (int i=0; i<bytes.length; ++i)
958 if (bytes[i] == File.separatorChar)
959 bytes[i] = '/';
960 return bytes;
964 * @return an important state
966 public RepositoryState getRepositoryState() {
967 if (new File(getWorkDir(), ".dotest").exists())
968 return RepositoryState.REBASING;
969 if (new File(gitDir,".dotest-merge").exists())
970 return RepositoryState.REBASING_INTERACTIVE;
971 if (new File(gitDir,"MERGE_HEAD").exists())
972 return RepositoryState.MERGING;
973 if (new File(gitDir,"BISECT_LOG").exists())
974 return RepositoryState.BISECTING;
975 return RepositoryState.SAFE;
979 * Check validty of a ref name. It must not contain character that has
980 * a special meaning in a Git object reference expression. Some other
981 * dangerous characters are also excluded.
983 * @param refName
985 * @return true if refName is a valid ref name
987 public static boolean isValidRefName(final String refName) {
988 final int len = refName.length();
989 char p = '\0';
990 for (int i=0; i<len; ++i) {
991 char c = refName.charAt(i);
992 if (c <= ' ')
993 return false;
994 switch(c) {
995 case '.':
996 if (i == 0)
997 return false;
998 if (p == '/')
999 return false;
1000 if (p == '.')
1001 return false;
1002 break;
1003 case '/':
1004 if (i == 0)
1005 return false;
1006 if (i == len -1)
1007 return false;
1008 break;
1009 case '~': case '^': case ':':
1010 case '?': case '[':
1011 return false;
1012 case '*':
1013 return false;
1015 p = c;
1017 return true;
1021 * String work dir and return normalized repository path
1023 * @param wd Work dir
1024 * @param f File whose path shall be stripp off it's workdir
1025 * @return normalized repository relative path
1027 public static String stripWorkDir(File wd, File f) {
1028 String relName = f.getPath().substring(wd.getPath().length() + 1);
1029 relName = relName.replace(File.separatorChar, '/');
1030 return relName;
1034 * @return the workdir file, i.e. where the files are checked out
1036 public File getWorkDir() {
1037 return getDirectory().getParentFile();
1041 * Register a {@link RepositoryListener} which will be notified
1042 * when ref changes are detected.
1044 * @param l
1046 public void addRepositoryChangedListener(final RepositoryListener l) {
1047 listeners.add(l);
1051 * Remove a registered {@link RepositoryListener}
1052 * @param l
1054 public void removeRepositoryChangedListener(final RepositoryListener l) {
1055 listeners.remove(l);
1059 * Register a global {@link RepositoryListener} which will be notified
1060 * when a ref changes in any repository are detected.
1062 * @param l
1064 public static void addAnyRepositoryChangedListener(final RepositoryListener l) {
1065 allListeners.add(l);
1069 * Remove a globally registered {@link RepositoryListener}
1070 * @param l
1072 public static void removeAnyRepositoryChangedListener(final RepositoryListener l) {
1073 allListeners.remove(l);
1076 void fireRefsMaybeChanged() {
1077 if (refs.lastRefModification != refs.lastNotifiedRefModification) {
1078 refs.lastNotifiedRefModification = refs.lastRefModification;
1079 final RefsChangedEvent event = new RefsChangedEvent(this);
1080 List<RepositoryListener> all;
1081 synchronized (listeners) {
1082 all = new ArrayList<RepositoryListener>(listeners);
1084 synchronized (allListeners) {
1085 all.addAll(allListeners);
1087 for (final RepositoryListener l : all) {
1088 l.refsChanged(event);
1093 void fireIndexChanged() {
1094 final IndexChangedEvent event = new IndexChangedEvent(this);
1095 List<RepositoryListener> all;
1096 synchronized (listeners) {
1097 all = new ArrayList<RepositoryListener>(listeners);
1099 synchronized (allListeners) {
1100 all.addAll(allListeners);
1102 for (final RepositoryListener l : all) {
1103 l.indexChanged(event);
1108 * Force a scan for changed refs.
1110 * @throws IOException
1112 public void scanForRepoChanges() throws IOException {
1113 getAllRefs(); // This will look for changes to refs
1114 getIndex(); // This will detect changes in the index