Fix IncorrectObjectTypeException thrown for incorrect ^{blob}
[egit/fonseca.git] / org.spearce.jgit / src / org / spearce / jgit / lib / Repository.java
blob730a26724c64671411be5f0498be0d4a8a748227
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.R_HEADS + 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 containing 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.name();
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;
573 try {
574 pnum = Integer.parseInt(parentnum);
575 } catch (NumberFormatException e) {
576 throw new RevisionSyntaxException(
577 "Invalid commit parent number",
578 revstr);
580 if (pnum != 0) {
581 final ObjectId parents[] = ((Commit) ref)
582 .getParentIds();
583 if (pnum > parents.length)
584 refId = null;
585 else
586 refId = parents[pnum - 1];
588 i = j - 1;
589 break;
590 case '{':
591 int k;
592 String item = null;
593 for (k=i+2; k<rev.length; ++k) {
594 if (rev[k] == '}') {
595 item = new String(rev, i+2, k-i-2);
596 break;
599 i = k;
600 if (item != null)
601 if (item.equals("tree")) {
602 ref = mapObject(refId, null);
603 while (ref instanceof Tag) {
604 Tag t = (Tag)ref;
605 refId = t.getObjId();
606 ref = mapObject(refId, null);
608 if (ref instanceof Treeish)
609 refId = ((Treeish)ref).getTreeId();
610 else
611 throw new IncorrectObjectTypeException(refId, Constants.TYPE_TREE);
613 else if (item.equals("commit")) {
614 ref = mapObject(refId, null);
615 while (ref instanceof Tag) {
616 Tag t = (Tag)ref;
617 refId = t.getObjId();
618 ref = mapObject(refId, null);
620 if (!(ref instanceof Commit))
621 throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
623 else if (item.equals("blob")) {
624 ref = mapObject(refId, null);
625 while (ref instanceof Tag) {
626 Tag t = (Tag)ref;
627 refId = t.getObjId();
628 ref = mapObject(refId, null);
630 if (!(ref instanceof byte[]))
631 throw new IncorrectObjectTypeException(refId, Constants.TYPE_BLOB);
633 else if (item.equals("")) {
634 ref = mapObject(refId, null);
635 if (ref instanceof Tag)
636 refId = ((Tag)ref).getObjId();
637 else {
638 // self
641 else
642 throw new RevisionSyntaxException(revstr);
643 else
644 throw new RevisionSyntaxException(revstr);
645 break;
646 default:
647 ref = mapObject(refId, null);
648 if (ref instanceof Commit) {
649 final ObjectId parents[] = ((Commit) ref)
650 .getParentIds();
651 if (parents.length == 0)
652 refId = null;
653 else
654 refId = parents[0];
655 } else
656 throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
659 } else {
660 ref = mapObject(refId, null);
661 if (ref instanceof Commit) {
662 final ObjectId parents[] = ((Commit) ref)
663 .getParentIds();
664 if (parents.length == 0)
665 refId = null;
666 else
667 refId = parents[0];
668 } else
669 throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
671 break;
672 case '~':
673 if (ref == null) {
674 String refstr = new String(rev,0,i);
675 refId = resolveSimple(refstr);
676 ref = mapCommit(refId);
678 int l;
679 for (l = i + 1; l < rev.length; ++l) {
680 if (!Character.isDigit(rev[l]))
681 break;
683 String distnum = new String(rev, i+1, l-i-1);
684 int dist;
685 try {
686 dist = Integer.parseInt(distnum);
687 } catch (NumberFormatException e) {
688 throw new RevisionSyntaxException(
689 "Invalid ancestry length", revstr);
691 while (dist >= 0) {
692 final ObjectId[] parents = ((Commit) ref).getParentIds();
693 if (parents.length == 0) {
694 refId = null;
695 break;
697 refId = parents[0];
698 ref = mapCommit(refId);
699 --dist;
701 i = l - 1;
702 break;
703 case '@':
704 int m;
705 String time = null;
706 for (m=i+2; m<rev.length; ++m) {
707 if (rev[m] == '}') {
708 time = new String(rev, i+2, m-i-2);
709 break;
712 if (time != null)
713 throw new RevisionSyntaxException("reflogs not yet supported by revision parser yet", revstr);
714 i = m - 1;
715 break;
716 default:
717 if (refId != null)
718 throw new RevisionSyntaxException(revstr);
721 if (refId == null)
722 refId = resolveSimple(revstr);
723 return refId;
726 private ObjectId resolveSimple(final String revstr) throws IOException {
727 if (ObjectId.isId(revstr))
728 return ObjectId.fromString(revstr);
729 final Ref r = refs.readRef(revstr);
730 return r != null ? r.getObjectId() : null;
734 * Close all resources used by this repository
736 public void close() {
737 closePacks();
740 void closePacks() {
741 for (int k = packs.length - 1; k >= 0; k--) {
742 packs[k].close();
744 packs = new PackFile[0];
748 * Add a single existing pack to the list of available pack files.
750 * @param pack
751 * path of the pack file to open.
752 * @param idx
753 * path of the corresponding index file.
754 * @throws IOException
755 * index file could not be opened, read, or is not recognized as
756 * a Git pack file index.
758 public void openPack(final File pack, final File idx) throws IOException {
759 final String p = pack.getName();
760 final String i = idx.getName();
761 if (p.length() != 50 || !p.startsWith("pack-") || !p.endsWith(".pack"))
762 throw new IllegalArgumentException("Not a valid pack " + pack);
763 if (i.length() != 49 || !i.startsWith("pack-") || !i.endsWith(".idx"))
764 throw new IllegalArgumentException("Not a valid pack " + idx);
765 if (!p.substring(0,45).equals(i.substring(0,45)))
766 throw new IllegalArgumentException("Pack " + pack
767 + "does not match index " + idx);
769 final PackFile[] cur = packs;
770 final PackFile[] arr = new PackFile[cur.length + 1];
771 System.arraycopy(cur, 0, arr, 1, cur.length);
772 arr[0] = new PackFile(this, idx, pack);
773 packs = arr;
777 * Scan the object dirs, including alternates for packs
778 * to use.
780 public void scanForPacks() {
781 final ArrayList<PackFile> p = new ArrayList<PackFile>();
782 for (int i=0; i<objectsDirs.length; ++i)
783 scanForPacks(new File(objectsDirs[i], "pack"), p);
784 final PackFile[] arr = new PackFile[p.size()];
785 p.toArray(arr);
786 packs = arr;
789 private void scanForPacks(final File packDir, Collection<PackFile> packList) {
790 final String[] idxList = packDir.list(new FilenameFilter() {
791 public boolean accept(final File baseDir, final String n) {
792 // Must match "pack-[0-9a-f]{40}.idx" to be an index.
793 return n.length() == 49 && n.endsWith(".idx")
794 && n.startsWith("pack-");
797 if (idxList != null) {
798 for (final String indexName : idxList) {
799 final String n = indexName.substring(0, indexName.length() - 4);
800 final File idxFile = new File(packDir, n + ".idx");
801 final File packFile = new File(packDir, n + ".pack");
803 if (!packFile.isFile()) {
804 // Sometimes C Git's http fetch transport leaves a
805 // .idx file behind and does not download the .pack.
806 // We have to skip over such useless indexes.
808 continue;
811 try {
812 packList.add(new PackFile(this, idxFile, packFile));
813 } catch (IOException ioe) {
814 // Whoops. That's not a pack!
816 ioe.printStackTrace();
823 * Writes a symref (e.g. HEAD) to disk
825 * @param name symref name
826 * @param target pointed to ref
827 * @throws IOException
829 public void writeSymref(final String name, final String target)
830 throws IOException {
831 refs.link(name, target);
834 public String toString() {
835 return "Repository[" + getDirectory() + "]";
839 * @return name of topmost Stacked Git patch.
840 * @throws IOException
842 public String getPatch() throws IOException {
843 final File ptr = new File(getDirectory(),"patches/"+getBranch()+"/applied");
844 final BufferedReader br = new BufferedReader(new FileReader(ptr));
845 String last=null;
846 try {
847 String line;
848 while ((line=br.readLine())!=null) {
849 last = line;
851 } finally {
852 br.close();
854 return last;
858 * @return name of current branch
859 * @throws IOException
861 public String getFullBranch() throws IOException {
862 final File ptr = new File(getDirectory(),"HEAD");
863 final BufferedReader br = new BufferedReader(new FileReader(ptr));
864 String ref;
865 try {
866 ref = br.readLine();
867 } finally {
868 br.close();
870 if (ref.startsWith("ref: "))
871 ref = ref.substring(5);
872 return ref;
876 * @return name of current branch.
877 * @throws IOException
879 public String getBranch() throws IOException {
880 try {
881 final File ptr = new File(getDirectory(), Constants.HEAD);
882 final BufferedReader br = new BufferedReader(new FileReader(ptr));
883 String ref;
884 try {
885 ref = br.readLine();
886 } finally {
887 br.close();
889 if (ref.startsWith("ref: "))
890 ref = ref.substring(5);
891 if (ref.startsWith("refs/heads/"))
892 ref = ref.substring(11);
893 return ref;
894 } catch (FileNotFoundException e) {
895 final File ptr = new File(getDirectory(),"head-name");
896 final BufferedReader br = new BufferedReader(new FileReader(ptr));
897 String ref;
898 try {
899 ref = br.readLine();
900 } finally {
901 br.close();
903 return ref;
908 * @return all known refs (heads, tags, remotes).
910 public Map<String, Ref> getAllRefs() {
911 return refs.getAllRefs();
915 * @return all tags; key is short tag name ("v1.0") and value of the entry
916 * contains the ref with the full tag name ("refs/tags/v1.0").
918 public Map<String, Ref> getTags() {
919 return refs.getTags();
923 * @return true if HEAD points to a StGit patch.
925 public boolean isStGitMode() {
926 try {
927 File file = new File(getDirectory(), "HEAD");
928 BufferedReader reader = new BufferedReader(new FileReader(file));
929 String string = reader.readLine();
930 if (!string.startsWith("ref: refs/heads/"))
931 return false;
932 String branch = string.substring("ref: refs/heads/".length());
933 File currentPatches = new File(new File(new File(getDirectory(),
934 "patches"), branch), "applied");
935 if (!currentPatches.exists())
936 return false;
937 if (currentPatches.length() == 0)
938 return false;
939 return true;
941 } catch (IOException e) {
942 e.printStackTrace();
943 return false;
948 * @return applied patches in a map indexed on current commit id
949 * @throws IOException
951 public Map<ObjectId,StGitPatch> getAppliedPatches() throws IOException {
952 Map<ObjectId,StGitPatch> ret = new HashMap<ObjectId,StGitPatch>();
953 if (isStGitMode()) {
954 File patchDir = new File(new File(getDirectory(),"patches"),getBranch());
955 BufferedReader apr = new BufferedReader(new FileReader(new File(patchDir,"applied")));
956 for (String patchName=apr.readLine(); patchName!=null; patchName=apr.readLine()) {
957 File topFile = new File(new File(new File(patchDir,"patches"), patchName), "top");
958 BufferedReader tfr = new BufferedReader(new FileReader(topFile));
959 String objectId = tfr.readLine();
960 ObjectId id = ObjectId.fromString(objectId);
961 ret.put(id, new StGitPatch(patchName, id));
962 tfr.close();
964 apr.close();
966 return ret;
969 /** Clean up stale caches */
970 public void refreshFromDisk() {
971 refs.clearCache();
975 * @return a representation of the index associated with this repo
976 * @throws IOException
978 public GitIndex getIndex() throws IOException {
979 if (index == null) {
980 index = new GitIndex(this);
981 index.read();
982 } else {
983 index.rereadIfNecessary();
985 return index;
988 static byte[] gitInternalSlash(byte[] bytes) {
989 if (File.separatorChar == '/')
990 return bytes;
991 for (int i=0; i<bytes.length; ++i)
992 if (bytes[i] == File.separatorChar)
993 bytes[i] = '/';
994 return bytes;
998 * @return an important state
1000 public RepositoryState getRepositoryState() {
1001 if (new File(getWorkDir(), ".dotest").exists())
1002 return RepositoryState.REBASING;
1003 if (new File(gitDir,".dotest-merge").exists())
1004 return RepositoryState.REBASING_INTERACTIVE;
1005 if (new File(gitDir,"MERGE_HEAD").exists())
1006 return RepositoryState.MERGING;
1007 if (new File(gitDir,"BISECT_LOG").exists())
1008 return RepositoryState.BISECTING;
1009 return RepositoryState.SAFE;
1013 * Check validity of a ref name. It must not contain character that has
1014 * a special meaning in a Git object reference expression. Some other
1015 * dangerous characters are also excluded.
1017 * @param refName
1019 * @return true if refName is a valid ref name
1021 public static boolean isValidRefName(final String refName) {
1022 final int len = refName.length();
1023 if (len == 0)
1024 return false;
1026 char p = '\0';
1027 for (int i=0; i<len; ++i) {
1028 char c = refName.charAt(i);
1029 if (c <= ' ')
1030 return false;
1031 switch(c) {
1032 case '.':
1033 if (i == 0)
1034 return false;
1035 if (p == '/')
1036 return false;
1037 if (p == '.')
1038 return false;
1039 break;
1040 case '/':
1041 if (i == 0)
1042 return false;
1043 if (i == len -1)
1044 return false;
1045 break;
1046 case '~': case '^': case ':':
1047 case '?': case '[':
1048 return false;
1049 case '*':
1050 return false;
1052 p = c;
1054 return true;
1058 * Strip work dir and return normalized repository path
1060 * @param wd Work dir
1061 * @param f File whose path shall be stripped of its workdir
1062 * @return normalized repository relative path
1064 public static String stripWorkDir(File wd, File f) {
1065 String relName = f.getPath().substring(wd.getPath().length() + 1);
1066 relName = relName.replace(File.separatorChar, '/');
1067 return relName;
1071 * @return the workdir file, i.e. where the files are checked out
1073 public File getWorkDir() {
1074 return getDirectory().getParentFile();
1078 * Register a {@link RepositoryListener} which will be notified
1079 * when ref changes are detected.
1081 * @param l
1083 public void addRepositoryChangedListener(final RepositoryListener l) {
1084 listeners.add(l);
1088 * Remove a registered {@link RepositoryListener}
1089 * @param l
1091 public void removeRepositoryChangedListener(final RepositoryListener l) {
1092 listeners.remove(l);
1096 * Register a global {@link RepositoryListener} which will be notified
1097 * when a ref changes in any repository are detected.
1099 * @param l
1101 public static void addAnyRepositoryChangedListener(final RepositoryListener l) {
1102 allListeners.add(l);
1106 * Remove a globally registered {@link RepositoryListener}
1107 * @param l
1109 public static void removeAnyRepositoryChangedListener(final RepositoryListener l) {
1110 allListeners.remove(l);
1113 void fireRefsMaybeChanged() {
1114 if (refs.lastRefModification != refs.lastNotifiedRefModification) {
1115 refs.lastNotifiedRefModification = refs.lastRefModification;
1116 final RefsChangedEvent event = new RefsChangedEvent(this);
1117 List<RepositoryListener> all;
1118 synchronized (listeners) {
1119 all = new ArrayList<RepositoryListener>(listeners);
1121 synchronized (allListeners) {
1122 all.addAll(allListeners);
1124 for (final RepositoryListener l : all) {
1125 l.refsChanged(event);
1130 void fireIndexChanged() {
1131 final IndexChangedEvent event = new IndexChangedEvent(this);
1132 List<RepositoryListener> all;
1133 synchronized (listeners) {
1134 all = new ArrayList<RepositoryListener>(listeners);
1136 synchronized (allListeners) {
1137 all.addAll(allListeners);
1139 for (final RepositoryListener l : all) {
1140 l.indexChanged(event);
1145 * Force a scan for changed refs.
1147 * @throws IOException
1149 public void scanForRepoChanges() throws IOException {
1150 getAllRefs(); // This will look for changes to refs
1151 getIndex(); // This will detect changes in the index