Remove getId from ObjectLoader API as its unnecessary overhead
[egit/qmx.git] / org.spearce.jgit / src / org / spearce / jgit / lib / Repository.java
blob02f8103df0b0b1205d51d26e1cd4b4d1bba1d598
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.Arrays;
50 import java.util.Collection;
51 import java.util.Collections;
52 import java.util.HashMap;
53 import java.util.HashSet;
54 import java.util.LinkedList;
55 import java.util.List;
56 import java.util.Map;
57 import java.util.Set;
58 import java.util.Vector;
60 import org.spearce.jgit.errors.IncorrectObjectTypeException;
61 import org.spearce.jgit.errors.RevisionSyntaxException;
62 import org.spearce.jgit.util.FS;
64 /**
65 * Represents a Git repository. A repository holds all objects and refs used for
66 * managing source code (could by any type of file, but source code is what
67 * SCM's are typically used for).
69 * In Git terms all data is stored in GIT_DIR, typically a directory called
70 * .git. A work tree is maintained unless the repository is a bare repository.
71 * Typically the .git directory is located at the root of the work dir.
73 * <ul>
74 * <li>GIT_DIR
75 * <ul>
76 * <li>objects/ - objects</li>
77 * <li>refs/ - tags and heads</li>
78 * <li>config - configuration</li>
79 * <li>info/ - more configurations</li>
80 * </ul>
81 * </li>
82 * </ul>
83 * <p>
84 * This class is thread-safe.
85 * <p>
86 * This implementation only handles a subtly undocumented subset of git features.
89 public class Repository {
90 private final File gitDir;
92 private final RepositoryConfig config;
94 private final RefDatabase refs;
96 private File[] objectDirectoryList;
98 private PackFile[] packFileList;
100 private GitIndex index;
102 private List<RepositoryListener> listeners = new Vector<RepositoryListener>(); // thread safe
103 static private List<RepositoryListener> allListeners = new Vector<RepositoryListener>(); // thread safe
106 * Construct a representation of a Git repository.
108 * @param d
109 * GIT_DIR (the location of the repository metadata).
110 * @throws IOException
111 * the repository appears to already exist but cannot be
112 * accessed.
114 public Repository(final File d) throws IOException {
115 gitDir = d.getAbsoluteFile();
116 try {
117 objectDirectoryList = readObjectsDirs(
118 FS.resolve(gitDir, "objects"), new ArrayList<File>())
119 .toArray(new File[0]);
120 } catch (IOException e) {
121 IOException ex = new IOException("Cannot find all object dirs for " + gitDir);
122 ex.initCause(e);
123 throw ex;
125 refs = new RefDatabase(this);
126 packFileList = new PackFile[0];
127 config = new RepositoryConfig(this);
129 final boolean isExisting = objectDirectoryList[0].exists();
130 if (isExisting) {
131 getConfig().load();
132 final String repositoryFormatVersion = getConfig().getString(
133 "core", null, "repositoryFormatVersion");
134 if (!"0".equals(repositoryFormatVersion)) {
135 throw new IOException("Unknown repository format \""
136 + repositoryFormatVersion + "\"; expected \"0\".");
138 } else {
139 getConfig().create();
141 if (isExisting)
142 scanForPacks();
145 private static Collection<File> readObjectsDirs(File objectsDir,
146 Collection<File> ret) throws IOException {
147 ret.add(objectsDir);
148 final File altFile = FS.resolve(objectsDir, "info/alternates");
149 if (altFile.exists()) {
150 BufferedReader ar = new BufferedReader(new FileReader(altFile));
151 try {
152 for (String alt=ar.readLine(); alt!=null; alt=ar.readLine()) {
153 readObjectsDirs(FS.resolve(objectsDir, alt), ret);
155 } catch (Exception e) {
156 ar.close();
159 return ret;
163 * Create a new Git repository initializing the necessary files and
164 * directories.
166 * @throws IOException
168 public synchronized void create() throws IOException {
169 if (gitDir.exists()) {
170 throw new IllegalStateException("Repository already exists: "
171 + gitDir);
174 gitDir.mkdirs();
175 refs.create();
177 objectDirectoryList[0].mkdirs();
178 new File(objectDirectoryList[0], "pack").mkdir();
179 new File(objectDirectoryList[0], "info").mkdir();
181 new File(gitDir, "branches").mkdir();
182 new File(gitDir, "remotes").mkdir();
183 final String master = Constants.R_HEADS + Constants.MASTER;
184 refs.link(Constants.HEAD, master);
186 getConfig().create();
187 getConfig().save();
190 private synchronized File[] objectsDirs(){
191 return objectDirectoryList;
194 private synchronized PackFile[] packs(){
195 return packFileList;
199 * @return GIT_DIR
201 public File getDirectory() {
202 return gitDir;
206 * @return the directory containing the objects owned by this repository.
208 public File getObjectsDirectory() {
209 return objectsDirs()[0];
213 * @return the configuration of this repository
215 public RepositoryConfig getConfig() {
216 return config;
220 * Construct a filename where the loose object having a specified SHA-1
221 * should be stored. If the object is stored in a shared repository the path
222 * to the alternative repo will be returned. If the object is not yet store
223 * a usable path in this repo will be returned. It is assumed that callers
224 * will look for objects in a pack first.
226 * @param objectId
227 * @return suggested file name
229 public File toFile(final AnyObjectId objectId) {
230 final String n = objectId.name();
231 String d=n.substring(0, 2);
232 String f=n.substring(2);
233 final File[] objectsDirs = objectsDirs();
234 for (int i=0; i<objectsDirs.length; ++i) {
235 File ret = new File(new File(objectsDirs[i], d), f);
236 if (ret.exists())
237 return ret;
239 return new File(new File(objectsDirs[0], d), f);
243 * @param objectId
244 * @return true if the specified object is stored in this repo or any of the
245 * known shared repositories.
247 public boolean hasObject(final AnyObjectId objectId) {
248 final PackFile[] packs = packs();
249 int k = packs.length;
250 if (k > 0) {
251 do {
252 if (packs[--k].hasObject(objectId))
253 return true;
254 } while (k > 0);
256 return toFile(objectId).isFile();
260 * @param id
261 * SHA-1 of an object.
263 * @return a {@link ObjectLoader} for accessing the data of the named
264 * object, or null if the object does not exist.
265 * @throws IOException
267 public ObjectLoader openObject(final AnyObjectId id)
268 throws IOException {
269 final WindowCursor wc = new WindowCursor();
270 try {
271 return openObject(wc, id);
272 } finally {
273 wc.release();
278 * @param curs
279 * temporary working space associated with the calling thread.
280 * @param id
281 * SHA-1 of an object.
283 * @return a {@link ObjectLoader} for accessing the data of the named
284 * object, or null if the object does not exist.
285 * @throws IOException
287 public ObjectLoader openObject(final WindowCursor curs, final AnyObjectId id)
288 throws IOException {
289 final PackFile[] packs = packs();
290 int k = packs.length;
291 if (k > 0) {
292 do {
293 try {
294 final ObjectLoader ol = packs[--k].get(curs, id);
295 if (ol != null)
296 return ol;
297 } catch (IOException ioe) {
298 // This shouldn't happen unless the pack was corrupted
299 // after we opened it or the VM runs out of memory. This is
300 // a know problem with memory mapped I/O in java and have
301 // been noticed with JDK < 1.6. Tell the gc that now is a good
302 // time to collect and try once more.
303 try {
304 curs.release();
305 System.gc();
306 final ObjectLoader ol = packs[k].get(curs, id);
307 if (ol != null)
308 return ol;
309 } catch (IOException ioe2) {
310 ioe2.printStackTrace();
311 ioe.printStackTrace();
312 // Still fails.. that's BAD, maybe the pack has
313 // been corrupted after all, or the gc didn't manage
314 // to release enough previously mmaped areas.
317 } while (k > 0);
319 try {
320 return new UnpackedObjectLoader(this, id);
321 } catch (FileNotFoundException fnfe) {
322 return null;
327 * Open object in all packs containing specified object.
329 * @param objectId
330 * id of object to search for
331 * @param curs
332 * temporary working space associated with the calling thread.
333 * @return collection of loaders for this object, from all packs containing
334 * this object
335 * @throws IOException
337 public Collection<PackedObjectLoader> openObjectInAllPacks(
338 final AnyObjectId objectId, final WindowCursor curs)
339 throws IOException {
340 Collection<PackedObjectLoader> result = new LinkedList<PackedObjectLoader>();
341 openObjectInAllPacks(objectId, result, curs);
342 return result;
346 * Open object in all packs containing specified object.
348 * @param objectId
349 * id of object to search for
350 * @param resultLoaders
351 * result collection of loaders for this object, filled with
352 * loaders from all packs containing specified object
353 * @param curs
354 * temporary working space associated with the calling thread.
355 * @throws IOException
357 void openObjectInAllPacks(final AnyObjectId objectId,
358 final Collection<PackedObjectLoader> resultLoaders,
359 final WindowCursor curs) throws IOException {
360 for (PackFile pack : packs()) {
361 final PackedObjectLoader loader = pack.get(curs, objectId);
362 if (loader != null)
363 resultLoaders.add(loader);
368 * @param id
369 * SHA'1 of a blob
370 * @return an {@link ObjectLoader} for accessing the data of a named blob
371 * @throws IOException
373 public ObjectLoader openBlob(final ObjectId id) throws IOException {
374 return openObject(id);
378 * @param id
379 * SHA'1 of a tree
380 * @return an {@link ObjectLoader} for accessing the data of a named tree
381 * @throws IOException
383 public ObjectLoader openTree(final ObjectId id) throws IOException {
384 return openObject(id);
388 * Access a Commit object using a symbolic reference. This reference may
389 * be a SHA-1 or ref in combination with a number of symbols translating
390 * from one ref or SHA1-1 to another, such as HEAD^ etc.
392 * @param revstr a reference to a git commit object
393 * @return a Commit named by the specified string
394 * @throws IOException for I/O error or unexpected object type.
396 * @see #resolve(String)
398 public Commit mapCommit(final String revstr) throws IOException {
399 final ObjectId id = resolve(revstr);
400 return id != null ? mapCommit(id) : null;
404 * Access any type of Git object by id and
406 * @param id
407 * SHA-1 of object to read
408 * @param refName optional, only relevant for simple tags
409 * @return The Git object if found or null
410 * @throws IOException
412 public Object mapObject(final ObjectId id, final String refName) throws IOException {
413 final ObjectLoader or = openObject(id);
414 if (or == null)
415 return null;
416 final byte[] raw = or.getBytes();
417 if (or.getType() == Constants.OBJ_TREE)
418 return makeTree(id, raw);
419 if (or.getType() == Constants.OBJ_COMMIT)
420 return makeCommit(id, raw);
421 if (or.getType() == Constants.OBJ_TAG)
422 return makeTag(id, refName, raw);
423 if (or.getType() == Constants.OBJ_BLOB)
424 return raw;
425 throw new IncorrectObjectTypeException(id,
426 "COMMIT nor TREE nor BLOB nor TAG");
430 * Access a Commit by SHA'1 id.
431 * @param id
432 * @return Commit or null
433 * @throws IOException for I/O error or unexpected object type.
435 public Commit mapCommit(final ObjectId id) throws IOException {
436 final ObjectLoader or = openObject(id);
437 if (or == null)
438 return null;
439 final byte[] raw = or.getBytes();
440 if (Constants.OBJ_COMMIT == or.getType())
441 return new Commit(this, id, raw);
442 throw new IncorrectObjectTypeException(id, Constants.TYPE_COMMIT);
445 private Commit makeCommit(final ObjectId id, final byte[] raw) {
446 Commit ret = new Commit(this, id, raw);
447 return ret;
451 * Access a Tree object using a symbolic reference. This reference may
452 * be a SHA-1 or ref in combination with a number of symbols translating
453 * from one ref or SHA1-1 to another, such as HEAD^{tree} etc.
455 * @param revstr a reference to a git commit object
456 * @return a Tree named by the specified string
457 * @throws IOException
459 * @see #resolve(String)
461 public Tree mapTree(final String revstr) throws IOException {
462 final ObjectId id = resolve(revstr);
463 return id != null ? mapTree(id) : null;
467 * Access a Tree by SHA'1 id.
468 * @param id
469 * @return Tree or null
470 * @throws IOException for I/O error or unexpected object type.
472 public Tree mapTree(final ObjectId id) throws IOException {
473 final ObjectLoader or = openObject(id);
474 if (or == null)
475 return null;
476 final byte[] raw = or.getBytes();
477 if (Constants.OBJ_TREE == or.getType()) {
478 return new Tree(this, id, raw);
480 if (Constants.OBJ_COMMIT == or.getType())
481 return mapTree(ObjectId.fromString(raw, 5));
482 throw new IncorrectObjectTypeException(id, Constants.TYPE_TREE);
485 private Tree makeTree(final ObjectId id, final byte[] raw) throws IOException {
486 Tree ret = new Tree(this, id, raw);
487 return ret;
490 private Tag makeTag(final ObjectId id, final String refName, final byte[] raw) {
491 Tag ret = new Tag(this, id, refName, raw);
492 return ret;
496 * Access a tag by symbolic name.
498 * @param revstr
499 * @return a Tag or null
500 * @throws IOException on I/O error or unexpected type
502 public Tag mapTag(String revstr) throws IOException {
503 final ObjectId id = resolve(revstr);
504 return id != null ? mapTag(revstr, id) : null;
508 * Access a Tag by SHA'1 id
509 * @param refName
510 * @param id
511 * @return Commit or null
512 * @throws IOException for I/O error or unexpected object type.
514 public Tag mapTag(final String refName, final ObjectId id) throws IOException {
515 final ObjectLoader or = openObject(id);
516 if (or == null)
517 return null;
518 final byte[] raw = or.getBytes();
519 if (Constants.OBJ_TAG == or.getType())
520 return new Tag(this, id, refName, raw);
521 return new Tag(this, id, refName, null);
525 * Create a command to update, create or delete a ref in this repository.
527 * @param ref
528 * name of the ref the caller wants to modify.
529 * @return an update command. The caller must finish populating this command
530 * and then invoke one of the update methods to actually make a
531 * change.
532 * @throws IOException
533 * a symbolic ref was passed in and could not be resolved back
534 * to the base ref, as the symbolic ref could not be read.
536 public RefUpdate updateRef(final String ref) throws IOException {
537 return refs.newUpdate(ref);
541 * Parse a git revision string and return an object id.
543 * Currently supported is combinations of these.
544 * <ul>
545 * <li>SHA-1 - a SHA-1</li>
546 * <li>refs/... - a ref name</li>
547 * <li>ref^n - nth parent reference</li>
548 * <li>ref~n - distance via parent reference</li>
549 * <li>ref@{n} - nth version of ref</li>
550 * <li>ref^{tree} - tree references by ref</li>
551 * <li>ref^{commit} - commit references by ref</li>
552 * </ul>
554 * Not supported is
555 * <ul>
556 * <li>timestamps in reflogs, ref@{full or relative timestamp}</li>
557 * <li>abbreviated SHA-1's</li>
558 * </ul>
560 * @param revstr A git object references expression
561 * @return an ObjectId or null if revstr can't be resolved to any ObjectId
562 * @throws IOException on serious errors
564 public ObjectId resolve(final String revstr) throws IOException {
565 char[] rev = revstr.toCharArray();
566 Object ref = null;
567 ObjectId refId = null;
568 for (int i = 0; i < rev.length; ++i) {
569 switch (rev[i]) {
570 case '^':
571 if (refId == null) {
572 String refstr = new String(rev,0,i);
573 refId = resolveSimple(refstr);
574 if (refId == null)
575 return null;
577 if (i + 1 < rev.length) {
578 switch (rev[i + 1]) {
579 case '0':
580 case '1':
581 case '2':
582 case '3':
583 case '4':
584 case '5':
585 case '6':
586 case '7':
587 case '8':
588 case '9':
589 int j;
590 ref = mapObject(refId, null);
591 while (ref instanceof Tag) {
592 Tag tag = (Tag)ref;
593 refId = tag.getObjId();
594 ref = mapObject(refId, null);
596 if (!(ref instanceof Commit))
597 throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
598 for (j=i+1; j<rev.length; ++j) {
599 if (!Character.isDigit(rev[j]))
600 break;
602 String parentnum = new String(rev, i+1, j-i-1);
603 int pnum;
604 try {
605 pnum = Integer.parseInt(parentnum);
606 } catch (NumberFormatException e) {
607 throw new RevisionSyntaxException(
608 "Invalid commit parent number",
609 revstr);
611 if (pnum != 0) {
612 final ObjectId parents[] = ((Commit) ref)
613 .getParentIds();
614 if (pnum > parents.length)
615 refId = null;
616 else
617 refId = parents[pnum - 1];
619 i = j - 1;
620 break;
621 case '{':
622 int k;
623 String item = null;
624 for (k=i+2; k<rev.length; ++k) {
625 if (rev[k] == '}') {
626 item = new String(rev, i+2, k-i-2);
627 break;
630 i = k;
631 if (item != null)
632 if (item.equals("tree")) {
633 ref = mapObject(refId, null);
634 while (ref instanceof Tag) {
635 Tag t = (Tag)ref;
636 refId = t.getObjId();
637 ref = mapObject(refId, null);
639 if (ref instanceof Treeish)
640 refId = ((Treeish)ref).getTreeId();
641 else
642 throw new IncorrectObjectTypeException(refId, Constants.TYPE_TREE);
644 else if (item.equals("commit")) {
645 ref = mapObject(refId, null);
646 while (ref instanceof Tag) {
647 Tag t = (Tag)ref;
648 refId = t.getObjId();
649 ref = mapObject(refId, null);
651 if (!(ref instanceof Commit))
652 throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
654 else if (item.equals("blob")) {
655 ref = mapObject(refId, null);
656 while (ref instanceof Tag) {
657 Tag t = (Tag)ref;
658 refId = t.getObjId();
659 ref = mapObject(refId, null);
661 if (!(ref instanceof byte[]))
662 throw new IncorrectObjectTypeException(refId, Constants.TYPE_BLOB);
664 else if (item.equals("")) {
665 ref = mapObject(refId, null);
666 while (ref instanceof Tag) {
667 Tag t = (Tag)ref;
668 refId = t.getObjId();
669 ref = mapObject(refId, null);
672 else
673 throw new RevisionSyntaxException(revstr);
674 else
675 throw new RevisionSyntaxException(revstr);
676 break;
677 default:
678 ref = mapObject(refId, null);
679 if (ref instanceof Commit) {
680 final ObjectId parents[] = ((Commit) ref)
681 .getParentIds();
682 if (parents.length == 0)
683 refId = null;
684 else
685 refId = parents[0];
686 } else
687 throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
690 } else {
691 ref = mapObject(refId, null);
692 while (ref instanceof Tag) {
693 Tag tag = (Tag)ref;
694 refId = tag.getObjId();
695 ref = mapObject(refId, null);
697 if (ref instanceof Commit) {
698 final ObjectId parents[] = ((Commit) ref)
699 .getParentIds();
700 if (parents.length == 0)
701 refId = null;
702 else
703 refId = parents[0];
704 } else
705 throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
707 break;
708 case '~':
709 if (ref == null) {
710 String refstr = new String(rev,0,i);
711 refId = resolveSimple(refstr);
712 if (refId == null)
713 return null;
714 ref = mapObject(refId, null);
716 while (ref instanceof Tag) {
717 Tag tag = (Tag)ref;
718 refId = tag.getObjId();
719 ref = mapObject(refId, null);
721 if (!(ref instanceof Commit))
722 throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
723 int l;
724 for (l = i + 1; l < rev.length; ++l) {
725 if (!Character.isDigit(rev[l]))
726 break;
728 String distnum = new String(rev, i+1, l-i-1);
729 int dist;
730 try {
731 dist = Integer.parseInt(distnum);
732 } catch (NumberFormatException e) {
733 throw new RevisionSyntaxException(
734 "Invalid ancestry length", revstr);
736 while (dist > 0) {
737 final ObjectId[] parents = ((Commit) ref).getParentIds();
738 if (parents.length == 0) {
739 refId = null;
740 break;
742 refId = parents[0];
743 ref = mapCommit(refId);
744 --dist;
746 i = l - 1;
747 break;
748 case '@':
749 int m;
750 String time = null;
751 for (m=i+2; m<rev.length; ++m) {
752 if (rev[m] == '}') {
753 time = new String(rev, i+2, m-i-2);
754 break;
757 if (time != null)
758 throw new RevisionSyntaxException("reflogs not yet supported by revision parser yet", revstr);
759 i = m - 1;
760 break;
761 default:
762 if (refId != null)
763 throw new RevisionSyntaxException(revstr);
766 if (refId == null)
767 refId = resolveSimple(revstr);
768 return refId;
771 private ObjectId resolveSimple(final String revstr) throws IOException {
772 if (ObjectId.isId(revstr))
773 return ObjectId.fromString(revstr);
774 final Ref r = refs.readRef(revstr);
775 return r != null ? r.getObjectId() : null;
779 * Close all resources used by this repository
781 public void close() {
782 closePacks();
785 synchronized void closePacks() {
786 for (int k = packFileList.length - 1; k >= 0; k--)
787 packFileList[k].close();
788 packFileList = new PackFile[0];
792 * Add a single existing pack to the list of available pack files.
794 * @param pack
795 * path of the pack file to open.
796 * @param idx
797 * path of the corresponding index file.
798 * @throws IOException
799 * index file could not be opened, read, or is not recognized as
800 * a Git pack file index.
802 public void openPack(final File pack, final File idx) throws IOException {
803 final String p = pack.getName();
804 final String i = idx.getName();
805 if (p.length() != 50 || !p.startsWith("pack-") || !p.endsWith(".pack"))
806 throw new IllegalArgumentException("Not a valid pack " + pack);
807 if (i.length() != 49 || !i.startsWith("pack-") || !i.endsWith(".idx"))
808 throw new IllegalArgumentException("Not a valid pack " + idx);
809 if (!p.substring(0,45).equals(i.substring(0,45)))
810 throw new IllegalArgumentException("Pack " + pack
811 + "does not match index " + idx);
813 synchronized (this) {
814 final PackFile[] cur = packFileList;
815 final PackFile[] arr = new PackFile[cur.length + 1];
816 System.arraycopy(cur, 0, arr, 1, cur.length);
817 arr[0] = new PackFile(this, idx, pack);
818 packFileList = arr;
823 * Scan the object dirs, including alternates for packs
824 * to use.
826 public void scanForPacks() {
827 final ArrayList<PackFile> p = new ArrayList<PackFile>();
828 p.addAll(Arrays.asList(packs()));
829 for (final File d : objectsDirs())
830 scanForPacks(new File(d, "pack"), p);
831 final PackFile[] arr = new PackFile[p.size()];
832 p.toArray(arr);
833 synchronized (this) {
834 packFileList = arr;
838 private void scanForPacks(final File packDir, Collection<PackFile> packList) {
839 final String[] idxList = packDir.list(new FilenameFilter() {
840 public boolean accept(final File baseDir, final String n) {
841 // Must match "pack-[0-9a-f]{40}.idx" to be an index.
842 return n.length() == 49 && n.endsWith(".idx")
843 && n.startsWith("pack-");
846 if (idxList != null) {
847 SCAN: for (final String indexName : idxList) {
848 final String n = indexName.substring(0, indexName.length() - 4);
849 final File idxFile = new File(packDir, n + ".idx");
850 final File packFile = new File(packDir, n + ".pack");
852 if (!packFile.isFile()) {
853 // Sometimes C Git's http fetch transport leaves a
854 // .idx file behind and does not download the .pack.
855 // We have to skip over such useless indexes.
857 continue;
860 for (final PackFile p : packList) {
861 if (packFile.equals(p.getPackFile()))
862 continue SCAN;
865 try {
866 packList.add(new PackFile(this, idxFile, packFile));
867 } catch (IOException ioe) {
868 // Whoops. That's not a pack!
870 ioe.printStackTrace();
877 * Writes a symref (e.g. HEAD) to disk
879 * @param name symref name
880 * @param target pointed to ref
881 * @throws IOException
883 public void writeSymref(final String name, final String target)
884 throws IOException {
885 refs.link(name, target);
888 public String toString() {
889 return "Repository[" + getDirectory() + "]";
893 * @return name of topmost Stacked Git patch.
894 * @throws IOException
896 public String getPatch() throws IOException {
897 final File ptr = new File(getDirectory(),"patches/"+getBranch()+"/applied");
898 final BufferedReader br = new BufferedReader(new FileReader(ptr));
899 String last=null;
900 try {
901 String line;
902 while ((line=br.readLine())!=null) {
903 last = line;
905 } finally {
906 br.close();
908 return last;
912 * @return name of current branch
913 * @throws IOException
915 public String getFullBranch() throws IOException {
916 final File ptr = new File(getDirectory(),Constants.HEAD);
917 final BufferedReader br = new BufferedReader(new FileReader(ptr));
918 String ref;
919 try {
920 ref = br.readLine();
921 } finally {
922 br.close();
924 if (ref.startsWith("ref: "))
925 ref = ref.substring(5);
926 return ref;
930 * @return name of current branch.
931 * @throws IOException
933 public String getBranch() throws IOException {
934 try {
935 final File ptr = new File(getDirectory(), Constants.HEAD);
936 final BufferedReader br = new BufferedReader(new FileReader(ptr));
937 String ref;
938 try {
939 ref = br.readLine();
940 } finally {
941 br.close();
943 if (ref.startsWith("ref: "))
944 ref = ref.substring(5);
945 if (ref.startsWith("refs/heads/"))
946 ref = ref.substring(11);
947 return ref;
948 } catch (FileNotFoundException e) {
949 final File ptr = new File(getDirectory(),"head-name");
950 final BufferedReader br = new BufferedReader(new FileReader(ptr));
951 String ref;
952 try {
953 ref = br.readLine();
954 } finally {
955 br.close();
957 return ref;
962 * @return all known refs (heads, tags, remotes).
964 public Map<String, Ref> getAllRefs() {
965 return refs.getAllRefs();
969 * @return all tags; key is short tag name ("v1.0") and value of the entry
970 * contains the ref with the full tag name ("refs/tags/v1.0").
972 public Map<String, Ref> getTags() {
973 return refs.getTags();
977 * Peel a possibly unpeeled ref and updates it.
978 * <p>
979 * If the ref cannot be peeled (as it does not refer to an annotated tag)
980 * the peeled id stays null, but {@link Ref#isPeeled()} will be true.
982 * @param ref
983 * The ref to peel
984 * @return <code>ref</code> if <code>ref.isPeeled()</code> is true; else a
985 * new Ref object representing the same data as Ref, but isPeeled()
986 * will be true and getPeeledObjectId will contain the peeled object
987 * (or null).
989 public Ref peel(final Ref ref) {
990 return refs.peel(ref);
994 * @return a map with all objects referenced by a peeled ref.
996 public Map<AnyObjectId, Set<Ref>> getAllRefsByPeeledObjectId() {
997 Map<String, Ref> allRefs = getAllRefs();
998 Map<AnyObjectId, Set<Ref>> ret = new HashMap<AnyObjectId, Set<Ref>>(allRefs.size());
999 for (Ref ref : allRefs.values()) {
1000 if (!ref.isPeeled())
1001 ref = peel(ref);
1002 AnyObjectId target = ref.getPeeledObjectId();
1003 if (target == null)
1004 target = ref.getObjectId();
1005 // We assume most Sets here are singletons
1006 Set<Ref> oset = ret.put(target, Collections.singleton(ref));
1007 if (oset != null) {
1008 // that was not the case (rare)
1009 if (oset.size() == 1) {
1010 // Was a read-only singleton, we must copy to a new Set
1011 oset = new HashSet<Ref>(oset);
1013 ret.put(target, oset);
1014 oset.add(ref);
1017 return ret;
1020 /** Clean up stale caches */
1021 public void refreshFromDisk() {
1022 refs.clearCache();
1026 * @return a representation of the index associated with this repo
1027 * @throws IOException
1029 public GitIndex getIndex() throws IOException {
1030 if (index == null) {
1031 index = new GitIndex(this);
1032 index.read();
1033 } else {
1034 index.rereadIfNecessary();
1036 return index;
1039 static byte[] gitInternalSlash(byte[] bytes) {
1040 if (File.separatorChar == '/')
1041 return bytes;
1042 for (int i=0; i<bytes.length; ++i)
1043 if (bytes[i] == File.separatorChar)
1044 bytes[i] = '/';
1045 return bytes;
1049 * @return an important state
1051 public RepositoryState getRepositoryState() {
1052 // Pre Git-1.6 logic
1053 if (new File(getWorkDir(), ".dotest").exists())
1054 return RepositoryState.REBASING;
1055 if (new File(gitDir,".dotest-merge").exists())
1056 return RepositoryState.REBASING_INTERACTIVE;
1058 // From 1.6 onwards
1059 if (new File(getDirectory(),"rebase-apply/rebasing").exists())
1060 return RepositoryState.REBASING_REBASING;
1061 if (new File(getDirectory(),"rebase-apply/applying").exists())
1062 return RepositoryState.APPLY;
1063 if (new File(getDirectory(),"rebase-apply").exists())
1064 return RepositoryState.REBASING;
1066 if (new File(getDirectory(),"rebase-merge/interactive").exists())
1067 return RepositoryState.REBASING_INTERACTIVE;
1068 if (new File(getDirectory(),"rebase-merge").exists())
1069 return RepositoryState.REBASING_MERGE;
1071 // Both versions
1072 if (new File(gitDir,"MERGE_HEAD").exists())
1073 return RepositoryState.MERGING;
1074 if (new File(gitDir,"BISECT_LOG").exists())
1075 return RepositoryState.BISECTING;
1077 return RepositoryState.SAFE;
1081 * Check validity of a ref name. It must not contain character that has
1082 * a special meaning in a Git object reference expression. Some other
1083 * dangerous characters are also excluded.
1085 * @param refName
1087 * @return true if refName is a valid ref name
1089 public static boolean isValidRefName(final String refName) {
1090 final int len = refName.length();
1091 if (len == 0)
1092 return false;
1094 char p = '\0';
1095 for (int i=0; i<len; ++i) {
1096 char c = refName.charAt(i);
1097 if (c <= ' ')
1098 return false;
1099 switch(c) {
1100 case '.':
1101 if (i == 0)
1102 return false;
1103 if (p == '/')
1104 return false;
1105 if (p == '.')
1106 return false;
1107 break;
1108 case '/':
1109 if (i == 0)
1110 return false;
1111 if (i == len -1)
1112 return false;
1113 break;
1114 case '~': case '^': case ':':
1115 case '?': case '[':
1116 return false;
1117 case '*':
1118 return false;
1120 p = c;
1122 return true;
1126 * Strip work dir and return normalized repository path
1128 * @param wd Work dir
1129 * @param f File whose path shall be stripped of its workdir
1130 * @return normalized repository relative path
1132 public static String stripWorkDir(File wd, File f) {
1133 String relName = f.getPath().substring(wd.getPath().length() + 1);
1134 relName = relName.replace(File.separatorChar, '/');
1135 return relName;
1139 * @return the workdir file, i.e. where the files are checked out
1141 public File getWorkDir() {
1142 return getDirectory().getParentFile();
1146 * Register a {@link RepositoryListener} which will be notified
1147 * when ref changes are detected.
1149 * @param l
1151 public void addRepositoryChangedListener(final RepositoryListener l) {
1152 listeners.add(l);
1156 * Remove a registered {@link RepositoryListener}
1157 * @param l
1159 public void removeRepositoryChangedListener(final RepositoryListener l) {
1160 listeners.remove(l);
1164 * Register a global {@link RepositoryListener} which will be notified
1165 * when a ref changes in any repository are detected.
1167 * @param l
1169 public static void addAnyRepositoryChangedListener(final RepositoryListener l) {
1170 allListeners.add(l);
1174 * Remove a globally registered {@link RepositoryListener}
1175 * @param l
1177 public static void removeAnyRepositoryChangedListener(final RepositoryListener l) {
1178 allListeners.remove(l);
1181 void fireRefsMaybeChanged() {
1182 if (refs.lastRefModification != refs.lastNotifiedRefModification) {
1183 refs.lastNotifiedRefModification = refs.lastRefModification;
1184 final RefsChangedEvent event = new RefsChangedEvent(this);
1185 List<RepositoryListener> all;
1186 synchronized (listeners) {
1187 all = new ArrayList<RepositoryListener>(listeners);
1189 synchronized (allListeners) {
1190 all.addAll(allListeners);
1192 for (final RepositoryListener l : all) {
1193 l.refsChanged(event);
1198 void fireIndexChanged() {
1199 final IndexChangedEvent event = new IndexChangedEvent(this);
1200 List<RepositoryListener> all;
1201 synchronized (listeners) {
1202 all = new ArrayList<RepositoryListener>(listeners);
1204 synchronized (allListeners) {
1205 all.addAll(allListeners);
1207 for (final RepositoryListener l : all) {
1208 l.indexChanged(event);
1213 * Force a scan for changed refs.
1215 * @throws IOException
1217 public void scanForRepoChanges() throws IOException {
1218 getAllRefs(); // This will look for changes to refs
1219 getIndex(); // This will detect changes in the index