Correct Javadoc for Repository.peel()
[egit.git] / org.spearce.jgit / src / org / spearce / jgit / lib / Repository.java
blobc9535312794e721ac8b9a540be09344d7d9b5696
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.Collections;
51 import java.util.HashMap;
52 import java.util.HashSet;
53 import java.util.LinkedList;
54 import java.util.List;
55 import java.util.Map;
56 import java.util.Set;
57 import java.util.Vector;
59 import org.spearce.jgit.errors.IncorrectObjectTypeException;
60 import org.spearce.jgit.errors.RevisionSyntaxException;
61 import org.spearce.jgit.stgit.StGitPatch;
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>
84 * This implementation only handles a subtly undocumented subset of git features.
87 public class Repository {
88 private final File gitDir;
90 private final File[] objectsDirs;
92 private final RepositoryConfig config;
94 private final RefDatabase refs;
96 private PackFile[] packs;
98 private GitIndex index;
100 private List<RepositoryListener> listeners = new Vector<RepositoryListener>(); // thread safe
101 static private List<RepositoryListener> allListeners = new Vector<RepositoryListener>(); // thread safe
104 * Construct a representation of a Git repository.
106 * @param d
107 * GIT_DIR (the location of the repository metadata).
108 * @throws IOException
109 * the repository appears to already exist but cannot be
110 * accessed.
112 public Repository(final File d) throws IOException {
113 gitDir = d.getAbsoluteFile();
114 try {
115 objectsDirs = readObjectsDirs(FS.resolve(gitDir, "objects"),
116 new ArrayList<File>()).toArray(new File[0]);
117 } catch (IOException e) {
118 IOException ex = new IOException("Cannot find all object dirs for " + gitDir);
119 ex.initCause(e);
120 throw ex;
122 refs = new RefDatabase(this);
123 packs = new PackFile[0];
124 config = new RepositoryConfig(this);
126 final boolean isExisting = objectsDirs[0].exists();
127 if (isExisting) {
128 getConfig().load();
129 final String repositoryFormatVersion = getConfig().getString(
130 "core", null, "repositoryFormatVersion");
131 if (!"0".equals(repositoryFormatVersion)) {
132 throw new IOException("Unknown repository format \""
133 + repositoryFormatVersion + "\"; expected \"0\".");
135 } else {
136 getConfig().create();
138 if (isExisting)
139 scanForPacks();
142 private Collection<File> readObjectsDirs(File objectsDir, Collection<File> ret) throws IOException {
143 ret.add(objectsDir);
144 final File altFile = FS.resolve(objectsDir, "info/alternates");
145 if (altFile.exists()) {
146 BufferedReader ar = new BufferedReader(new FileReader(altFile));
147 for (String alt=ar.readLine(); alt!=null; alt=ar.readLine()) {
148 readObjectsDirs(FS.resolve(objectsDir, alt), ret);
150 ar.close();
152 return ret;
156 * Create a new Git repository initializing the necessary files and
157 * directories.
159 * @throws IOException
161 public void create() throws IOException {
162 if (gitDir.exists()) {
163 throw new IllegalStateException("Repository already exists: "
164 + gitDir);
167 gitDir.mkdirs();
168 refs.create();
170 objectsDirs[0].mkdirs();
171 new File(objectsDirs[0], "pack").mkdir();
172 new File(objectsDirs[0], "info").mkdir();
174 new File(gitDir, "branches").mkdir();
175 new File(gitDir, "remotes").mkdir();
176 final String master = Constants.R_HEADS + Constants.MASTER;
177 refs.link(Constants.HEAD, master);
179 getConfig().create();
180 getConfig().save();
184 * @return GIT_DIR
186 public File getDirectory() {
187 return gitDir;
191 * @return the directory containing the objects owned by this repository.
193 public File getObjectsDirectory() {
194 return objectsDirs[0];
198 * @return the configuration of this repository
200 public RepositoryConfig getConfig() {
201 return config;
205 * Construct a filename where the loose object having a specified SHA-1
206 * should be stored. If the object is stored in a shared repository the path
207 * to the alternative repo will be returned. If the object is not yet store
208 * a usable path in this repo will be returned. It is assumed that callers
209 * will look for objects in a pack first.
211 * @param objectId
212 * @return suggested file name
214 public File toFile(final AnyObjectId objectId) {
215 final String n = objectId.name();
216 String d=n.substring(0, 2);
217 String f=n.substring(2);
218 for (int i=0; i<objectsDirs.length; ++i) {
219 File ret = new File(new File(objectsDirs[i], d), f);
220 if (ret.exists())
221 return ret;
223 return new File(new File(objectsDirs[0], d), f);
227 * @param objectId
228 * @return true if the specified object is stored in this repo or any of the
229 * known shared repositories.
231 public boolean hasObject(final AnyObjectId objectId) {
232 int k = packs.length;
233 if (k > 0) {
234 do {
235 if (packs[--k].hasObject(objectId))
236 return true;
237 } while (k > 0);
239 return toFile(objectId).isFile();
243 * @param id
244 * SHA-1 of an object.
246 * @return a {@link ObjectLoader} for accessing the data of the named
247 * object, or null if the object does not exist.
248 * @throws IOException
250 public ObjectLoader openObject(final AnyObjectId id)
251 throws IOException {
252 return openObject(new WindowCursor(),id);
256 * @param curs
257 * temporary working space associated with the calling thread.
258 * @param id
259 * SHA-1 of an object.
261 * @return a {@link ObjectLoader} for accessing the data of the named
262 * object, or null if the object does not exist.
263 * @throws IOException
265 public ObjectLoader openObject(final WindowCursor curs, final AnyObjectId id)
266 throws IOException {
267 int k = packs.length;
268 if (k > 0) {
269 do {
270 try {
271 final ObjectLoader ol = packs[--k].get(curs, id);
272 if (ol != null)
273 return ol;
274 } catch (IOException ioe) {
275 // This shouldn't happen unless the pack was corrupted
276 // after we opened it or the VM runs out of memory. This is
277 // a know problem with memory mapped I/O in java and have
278 // been noticed with JDK < 1.6. Tell the gc that now is a good
279 // time to collect and try once more.
280 try {
281 curs.release();
282 System.gc();
283 final ObjectLoader ol = packs[k].get(curs, id);
284 if (ol != null)
285 return ol;
286 } catch (IOException ioe2) {
287 ioe2.printStackTrace();
288 ioe.printStackTrace();
289 // Still fails.. that's BAD, maybe the pack has
290 // been corrupted after all, or the gc didn't manage
291 // to release enough previously mmaped areas.
294 } while (k > 0);
296 try {
297 return new UnpackedObjectLoader(this, id.toObjectId());
298 } catch (FileNotFoundException fnfe) {
299 return null;
304 * Open object in all packs containing specified object.
306 * @param objectId
307 * id of object to search for
308 * @param curs
309 * temporary working space associated with the calling thread.
310 * @return collection of loaders for this object, from all packs containing
311 * this object
312 * @throws IOException
314 public Collection<PackedObjectLoader> openObjectInAllPacks(
315 final AnyObjectId objectId, final WindowCursor curs)
316 throws IOException {
317 Collection<PackedObjectLoader> result = new LinkedList<PackedObjectLoader>();
318 openObjectInAllPacks(objectId, result, curs);
319 return result;
323 * Open object in all packs containing specified object.
325 * @param objectId
326 * id of object to search for
327 * @param resultLoaders
328 * result collection of loaders for this object, filled with
329 * loaders from all packs containing specified object
330 * @param curs
331 * temporary working space associated with the calling thread.
332 * @throws IOException
334 void openObjectInAllPacks(final AnyObjectId objectId,
335 final Collection<PackedObjectLoader> resultLoaders,
336 final WindowCursor curs) throws IOException {
337 for (PackFile pack : packs) {
338 final PackedObjectLoader loader = pack.get(curs, objectId);
339 if (loader != null)
340 resultLoaders.add(loader);
345 * @param id
346 * SHA'1 of a blob
347 * @return an {@link ObjectLoader} for accessing the data of a named blob
348 * @throws IOException
350 public ObjectLoader openBlob(final ObjectId id) throws IOException {
351 return openObject(id);
355 * @param id
356 * SHA'1 of a tree
357 * @return an {@link ObjectLoader} for accessing the data of a named tree
358 * @throws IOException
360 public ObjectLoader openTree(final ObjectId id) throws IOException {
361 return openObject(id);
365 * Access a Commit object using a symbolic reference. This reference may
366 * be a SHA-1 or ref in combination with a number of symbols translating
367 * from one ref or SHA1-1 to another, such as HEAD^ etc.
369 * @param revstr a reference to a git commit object
370 * @return a Commit named by the specified string
371 * @throws IOException for I/O error or unexpected object type.
373 * @see #resolve(String)
375 public Commit mapCommit(final String revstr) throws IOException {
376 final ObjectId id = resolve(revstr);
377 return id != null ? mapCommit(id) : null;
381 * Access any type of Git object by id and
383 * @param id
384 * SHA-1 of object to read
385 * @param refName optional, only relevant for simple tags
386 * @return The Git object if found or null
387 * @throws IOException
389 public Object mapObject(final ObjectId id, final String refName) throws IOException {
390 final ObjectLoader or = openObject(id);
391 if (or == null)
392 return null;
393 final byte[] raw = or.getBytes();
394 if (or.getType() == Constants.OBJ_TREE)
395 return makeTree(id, raw);
396 if (or.getType() == Constants.OBJ_COMMIT)
397 return makeCommit(id, raw);
398 if (or.getType() == Constants.OBJ_TAG)
399 return makeTag(id, refName, raw);
400 if (or.getType() == Constants.OBJ_BLOB)
401 return raw;
402 throw new IncorrectObjectTypeException(id,
403 "COMMIT nor TREE nor BLOB nor TAG");
407 * Access a Commit by SHA'1 id.
408 * @param id
409 * @return Commit or null
410 * @throws IOException for I/O error or unexpected object type.
412 public Commit mapCommit(final ObjectId id) throws IOException {
413 final ObjectLoader or = openObject(id);
414 if (or == null)
415 return null;
416 final byte[] raw = or.getBytes();
417 if (Constants.OBJ_COMMIT == or.getType())
418 return new Commit(this, id, raw);
419 throw new IncorrectObjectTypeException(id, Constants.TYPE_COMMIT);
422 private Commit makeCommit(final ObjectId id, final byte[] raw) {
423 Commit ret = new Commit(this, id, raw);
424 return ret;
428 * Access a Tree object using a symbolic reference. This reference may
429 * be a SHA-1 or ref in combination with a number of symbols translating
430 * from one ref or SHA1-1 to another, such as HEAD^{tree} etc.
432 * @param revstr a reference to a git commit object
433 * @return a Tree named by the specified string
434 * @throws IOException
436 * @see #resolve(String)
438 public Tree mapTree(final String revstr) throws IOException {
439 final ObjectId id = resolve(revstr);
440 return id != null ? mapTree(id) : null;
444 * Access a Tree by SHA'1 id.
445 * @param id
446 * @return Tree or null
447 * @throws IOException for I/O error or unexpected object type.
449 public Tree mapTree(final ObjectId id) throws IOException {
450 final ObjectLoader or = openObject(id);
451 if (or == null)
452 return null;
453 final byte[] raw = or.getBytes();
454 if (Constants.OBJ_TREE == or.getType()) {
455 return new Tree(this, id, raw);
457 if (Constants.OBJ_COMMIT == or.getType())
458 return mapTree(ObjectId.fromString(raw, 5));
459 throw new IncorrectObjectTypeException(id, Constants.TYPE_TREE);
462 private Tree makeTree(final ObjectId id, final byte[] raw) throws IOException {
463 Tree ret = new Tree(this, id, raw);
464 return ret;
467 private Tag makeTag(final ObjectId id, final String refName, final byte[] raw) {
468 Tag ret = new Tag(this, id, refName, raw);
469 return ret;
473 * Access a tag by symbolic name.
475 * @param revstr
476 * @return a Tag or null
477 * @throws IOException on I/O error or unexpected type
479 public Tag mapTag(String revstr) throws IOException {
480 final ObjectId id = resolve(revstr);
481 return id != null ? mapTag(revstr, id) : null;
485 * Access a Tag by SHA'1 id
486 * @param refName
487 * @param id
488 * @return Commit or null
489 * @throws IOException for I/O error or unexpected object type.
491 public Tag mapTag(final String refName, final ObjectId id) throws IOException {
492 final ObjectLoader or = openObject(id);
493 if (or == null)
494 return null;
495 final byte[] raw = or.getBytes();
496 if (Constants.OBJ_TAG == or.getType())
497 return new Tag(this, id, refName, raw);
498 return new Tag(this, id, refName, null);
502 * Create a command to update, create or delete a ref in this repository.
504 * @param ref
505 * name of the ref the caller wants to modify.
506 * @return an update command. The caller must finish populating this command
507 * and then invoke one of the update methods to actually make a
508 * change.
509 * @throws IOException
510 * a symbolic ref was passed in and could not be resolved back
511 * to the base ref, as the symbolic ref could not be read.
513 public RefUpdate updateRef(final String ref) throws IOException {
514 return refs.newUpdate(ref);
518 * Parse a git revision string and return an object id.
520 * Currently supported is combinations of these.
521 * <ul>
522 * <li>SHA-1 - a SHA-1</li>
523 * <li>refs/... - a ref name</li>
524 * <li>ref^n - nth parent reference</li>
525 * <li>ref~n - distance via parent reference</li>
526 * <li>ref@{n} - nth version of ref</li>
527 * <li>ref^{tree} - tree references by ref</li>
528 * <li>ref^{commit} - commit references by ref</li>
529 * </ul>
531 * Not supported is
532 * <ul>
533 * <li>timestamps in reflogs, ref@{full or relative timestamp}</li>
534 * <li>abbreviated SHA-1's</li>
535 * </ul>
537 * @param revstr A git object references expression
538 * @return an ObjectId or null if revstr can't be resolved to any ObjectId
539 * @throws IOException on serious errors
541 public ObjectId resolve(final String revstr) throws IOException {
542 char[] rev = revstr.toCharArray();
543 Object ref = null;
544 ObjectId refId = null;
545 for (int i = 0; i < rev.length; ++i) {
546 switch (rev[i]) {
547 case '^':
548 if (refId == null) {
549 String refstr = new String(rev,0,i);
550 refId = resolveSimple(refstr);
551 if (refId == null)
552 return null;
554 if (i + 1 < rev.length) {
555 switch (rev[i + 1]) {
556 case '0':
557 case '1':
558 case '2':
559 case '3':
560 case '4':
561 case '5':
562 case '6':
563 case '7':
564 case '8':
565 case '9':
566 int j;
567 ref = mapObject(refId, null);
568 while (ref instanceof Tag) {
569 Tag tag = (Tag)ref;
570 refId = tag.getObjId();
571 ref = mapObject(refId, null);
573 if (!(ref instanceof Commit))
574 throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
575 for (j=i+1; j<rev.length; ++j) {
576 if (!Character.isDigit(rev[j]))
577 break;
579 String parentnum = new String(rev, i+1, j-i-1);
580 int pnum;
581 try {
582 pnum = Integer.parseInt(parentnum);
583 } catch (NumberFormatException e) {
584 throw new RevisionSyntaxException(
585 "Invalid commit parent number",
586 revstr);
588 if (pnum != 0) {
589 final ObjectId parents[] = ((Commit) ref)
590 .getParentIds();
591 if (pnum > parents.length)
592 refId = null;
593 else
594 refId = parents[pnum - 1];
596 i = j - 1;
597 break;
598 case '{':
599 int k;
600 String item = null;
601 for (k=i+2; k<rev.length; ++k) {
602 if (rev[k] == '}') {
603 item = new String(rev, i+2, k-i-2);
604 break;
607 i = k;
608 if (item != null)
609 if (item.equals("tree")) {
610 ref = mapObject(refId, null);
611 while (ref instanceof Tag) {
612 Tag t = (Tag)ref;
613 refId = t.getObjId();
614 ref = mapObject(refId, null);
616 if (ref instanceof Treeish)
617 refId = ((Treeish)ref).getTreeId();
618 else
619 throw new IncorrectObjectTypeException(refId, Constants.TYPE_TREE);
621 else if (item.equals("commit")) {
622 ref = mapObject(refId, null);
623 while (ref instanceof Tag) {
624 Tag t = (Tag)ref;
625 refId = t.getObjId();
626 ref = mapObject(refId, null);
628 if (!(ref instanceof Commit))
629 throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
631 else if (item.equals("blob")) {
632 ref = mapObject(refId, null);
633 while (ref instanceof Tag) {
634 Tag t = (Tag)ref;
635 refId = t.getObjId();
636 ref = mapObject(refId, null);
638 if (!(ref instanceof byte[]))
639 throw new IncorrectObjectTypeException(refId, Constants.TYPE_BLOB);
641 else if (item.equals("")) {
642 ref = mapObject(refId, null);
643 while (ref instanceof Tag) {
644 Tag t = (Tag)ref;
645 refId = t.getObjId();
646 ref = mapObject(refId, null);
649 else
650 throw new RevisionSyntaxException(revstr);
651 else
652 throw new RevisionSyntaxException(revstr);
653 break;
654 default:
655 ref = mapObject(refId, null);
656 if (ref instanceof Commit) {
657 final ObjectId parents[] = ((Commit) ref)
658 .getParentIds();
659 if (parents.length == 0)
660 refId = null;
661 else
662 refId = parents[0];
663 } else
664 throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
667 } else {
668 ref = mapObject(refId, null);
669 while (ref instanceof Tag) {
670 Tag tag = (Tag)ref;
671 refId = tag.getObjId();
672 ref = mapObject(refId, null);
674 if (ref instanceof Commit) {
675 final ObjectId parents[] = ((Commit) ref)
676 .getParentIds();
677 if (parents.length == 0)
678 refId = null;
679 else
680 refId = parents[0];
681 } else
682 throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
684 break;
685 case '~':
686 if (ref == null) {
687 String refstr = new String(rev,0,i);
688 refId = resolveSimple(refstr);
689 if (refId == null)
690 return null;
691 ref = mapObject(refId, null);
693 while (ref instanceof Tag) {
694 Tag tag = (Tag)ref;
695 refId = tag.getObjId();
696 ref = mapObject(refId, null);
698 if (!(ref instanceof Commit))
699 throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
700 int l;
701 for (l = i + 1; l < rev.length; ++l) {
702 if (!Character.isDigit(rev[l]))
703 break;
705 String distnum = new String(rev, i+1, l-i-1);
706 int dist;
707 try {
708 dist = Integer.parseInt(distnum);
709 } catch (NumberFormatException e) {
710 throw new RevisionSyntaxException(
711 "Invalid ancestry length", revstr);
713 while (dist > 0) {
714 final ObjectId[] parents = ((Commit) ref).getParentIds();
715 if (parents.length == 0) {
716 refId = null;
717 break;
719 refId = parents[0];
720 ref = mapCommit(refId);
721 --dist;
723 i = l - 1;
724 break;
725 case '@':
726 int m;
727 String time = null;
728 for (m=i+2; m<rev.length; ++m) {
729 if (rev[m] == '}') {
730 time = new String(rev, i+2, m-i-2);
731 break;
734 if (time != null)
735 throw new RevisionSyntaxException("reflogs not yet supported by revision parser yet", revstr);
736 i = m - 1;
737 break;
738 default:
739 if (refId != null)
740 throw new RevisionSyntaxException(revstr);
743 if (refId == null)
744 refId = resolveSimple(revstr);
745 return refId;
748 private ObjectId resolveSimple(final String revstr) throws IOException {
749 if (ObjectId.isId(revstr))
750 return ObjectId.fromString(revstr);
751 final Ref r = refs.readRef(revstr);
752 return r != null ? r.getObjectId() : null;
756 * Close all resources used by this repository
758 public void close() {
759 closePacks();
762 void closePacks() {
763 for (int k = packs.length - 1; k >= 0; k--) {
764 packs[k].close();
766 packs = new PackFile[0];
770 * Add a single existing pack to the list of available pack files.
772 * @param pack
773 * path of the pack file to open.
774 * @param idx
775 * path of the corresponding index file.
776 * @throws IOException
777 * index file could not be opened, read, or is not recognized as
778 * a Git pack file index.
780 public void openPack(final File pack, final File idx) throws IOException {
781 final String p = pack.getName();
782 final String i = idx.getName();
783 if (p.length() != 50 || !p.startsWith("pack-") || !p.endsWith(".pack"))
784 throw new IllegalArgumentException("Not a valid pack " + pack);
785 if (i.length() != 49 || !i.startsWith("pack-") || !i.endsWith(".idx"))
786 throw new IllegalArgumentException("Not a valid pack " + idx);
787 if (!p.substring(0,45).equals(i.substring(0,45)))
788 throw new IllegalArgumentException("Pack " + pack
789 + "does not match index " + idx);
791 final PackFile[] cur = packs;
792 final PackFile[] arr = new PackFile[cur.length + 1];
793 System.arraycopy(cur, 0, arr, 1, cur.length);
794 arr[0] = new PackFile(this, idx, pack);
795 packs = arr;
799 * Scan the object dirs, including alternates for packs
800 * to use.
802 public void scanForPacks() {
803 final ArrayList<PackFile> p = new ArrayList<PackFile>();
804 for (int i=0; i<objectsDirs.length; ++i)
805 scanForPacks(new File(objectsDirs[i], "pack"), p);
806 final PackFile[] arr = new PackFile[p.size()];
807 p.toArray(arr);
808 packs = arr;
811 private void scanForPacks(final File packDir, Collection<PackFile> packList) {
812 final String[] idxList = packDir.list(new FilenameFilter() {
813 public boolean accept(final File baseDir, final String n) {
814 // Must match "pack-[0-9a-f]{40}.idx" to be an index.
815 return n.length() == 49 && n.endsWith(".idx")
816 && n.startsWith("pack-");
819 if (idxList != null) {
820 for (final String indexName : idxList) {
821 final String n = indexName.substring(0, indexName.length() - 4);
822 final File idxFile = new File(packDir, n + ".idx");
823 final File packFile = new File(packDir, n + ".pack");
825 if (!packFile.isFile()) {
826 // Sometimes C Git's http fetch transport leaves a
827 // .idx file behind and does not download the .pack.
828 // We have to skip over such useless indexes.
830 continue;
833 try {
834 packList.add(new PackFile(this, idxFile, packFile));
835 } catch (IOException ioe) {
836 // Whoops. That's not a pack!
838 ioe.printStackTrace();
845 * Writes a symref (e.g. HEAD) to disk
847 * @param name symref name
848 * @param target pointed to ref
849 * @throws IOException
851 public void writeSymref(final String name, final String target)
852 throws IOException {
853 refs.link(name, target);
856 public String toString() {
857 return "Repository[" + getDirectory() + "]";
861 * @return name of topmost Stacked Git patch.
862 * @throws IOException
864 public String getPatch() throws IOException {
865 final File ptr = new File(getDirectory(),"patches/"+getBranch()+"/applied");
866 final BufferedReader br = new BufferedReader(new FileReader(ptr));
867 String last=null;
868 try {
869 String line;
870 while ((line=br.readLine())!=null) {
871 last = line;
873 } finally {
874 br.close();
876 return last;
880 * @return name of current branch
881 * @throws IOException
883 public String getFullBranch() throws IOException {
884 final File ptr = new File(getDirectory(),"HEAD");
885 final BufferedReader br = new BufferedReader(new FileReader(ptr));
886 String ref;
887 try {
888 ref = br.readLine();
889 } finally {
890 br.close();
892 if (ref.startsWith("ref: "))
893 ref = ref.substring(5);
894 return ref;
898 * @return name of current branch.
899 * @throws IOException
901 public String getBranch() throws IOException {
902 try {
903 final File ptr = new File(getDirectory(), Constants.HEAD);
904 final BufferedReader br = new BufferedReader(new FileReader(ptr));
905 String ref;
906 try {
907 ref = br.readLine();
908 } finally {
909 br.close();
911 if (ref.startsWith("ref: "))
912 ref = ref.substring(5);
913 if (ref.startsWith("refs/heads/"))
914 ref = ref.substring(11);
915 return ref;
916 } catch (FileNotFoundException e) {
917 final File ptr = new File(getDirectory(),"head-name");
918 final BufferedReader br = new BufferedReader(new FileReader(ptr));
919 String ref;
920 try {
921 ref = br.readLine();
922 } finally {
923 br.close();
925 return ref;
930 * @return all known refs (heads, tags, remotes).
932 public Map<String, Ref> getAllRefs() {
933 return refs.getAllRefs();
937 * @return all tags; key is short tag name ("v1.0") and value of the entry
938 * contains the ref with the full tag name ("refs/tags/v1.0").
940 public Map<String, Ref> getTags() {
941 return refs.getTags();
945 * Peel a possibly unpeeled ref and updates it.
946 * <p>
947 * If the ref cannot be peeled (as it does not refer to an annotated tag)
948 * the peeled id stays null, but {@link Ref#isPeeled()} will be true.
950 * @param ref
951 * The ref to peel
952 * @return <code>ref</code> if <code>ref.isPeeled()</code> is true; else a
953 * new Ref object representing the same data as Ref, but isPeeled()
954 * will be true and getPeeledObjectId will contain the peeled object
955 * (or null).
957 public Ref peel(final Ref ref) {
958 return refs.peel(ref);
962 * @return a map with all objects referenced by a peeled ref.
964 public Map<AnyObjectId, Set<Ref>> getAllRefsByPeeledObjectId() {
965 Map<String, Ref> allRefs = getAllRefs();
966 Map<AnyObjectId, Set<Ref>> ret = new HashMap<AnyObjectId, Set<Ref>>(allRefs.size());
967 for (Ref ref : allRefs.values()) {
968 if (!ref.isPeeled())
969 ref = peel(ref);
970 AnyObjectId target = ref.getPeeledObjectId();
971 if (target == null)
972 target = ref.getObjectId();
973 // We assume most Sets here are singletons
974 Set<Ref> oset = ret.put(target, Collections.singleton(ref));
975 if (oset != null) {
976 // that was not the case (rare)
977 if (oset.size() == 1) {
978 // Was a read-only singleton, we must copy to a new Set
979 oset = new HashSet<Ref>(oset);
981 ret.put(target, oset);
982 oset.add(ref);
985 return ret;
989 * @return true if HEAD points to a StGit patch.
991 public boolean isStGitMode() {
992 try {
993 File file = new File(getDirectory(), "HEAD");
994 BufferedReader reader = new BufferedReader(new FileReader(file));
995 String string = reader.readLine();
996 if (!string.startsWith("ref: refs/heads/"))
997 return false;
998 String branch = string.substring("ref: refs/heads/".length());
999 File currentPatches = new File(new File(new File(getDirectory(),
1000 "patches"), branch), "applied");
1001 if (!currentPatches.exists())
1002 return false;
1003 if (currentPatches.length() == 0)
1004 return false;
1005 return true;
1007 } catch (IOException e) {
1008 e.printStackTrace();
1009 return false;
1014 * @return applied patches in a map indexed on current commit id
1015 * @throws IOException
1017 public Map<ObjectId,StGitPatch> getAppliedPatches() throws IOException {
1018 Map<ObjectId,StGitPatch> ret = new HashMap<ObjectId,StGitPatch>();
1019 if (isStGitMode()) {
1020 File patchDir = new File(new File(getDirectory(),"patches"),getBranch());
1021 BufferedReader apr = new BufferedReader(new FileReader(new File(patchDir,"applied")));
1022 for (String patchName=apr.readLine(); patchName!=null; patchName=apr.readLine()) {
1023 File topFile = new File(new File(new File(patchDir,"patches"), patchName), "top");
1024 BufferedReader tfr = new BufferedReader(new FileReader(topFile));
1025 String objectId = tfr.readLine();
1026 ObjectId id = ObjectId.fromString(objectId);
1027 ret.put(id, new StGitPatch(patchName, id));
1028 tfr.close();
1030 apr.close();
1032 return ret;
1035 /** Clean up stale caches */
1036 public void refreshFromDisk() {
1037 refs.clearCache();
1041 * @return a representation of the index associated with this repo
1042 * @throws IOException
1044 public GitIndex getIndex() throws IOException {
1045 if (index == null) {
1046 index = new GitIndex(this);
1047 index.read();
1048 } else {
1049 index.rereadIfNecessary();
1051 return index;
1054 static byte[] gitInternalSlash(byte[] bytes) {
1055 if (File.separatorChar == '/')
1056 return bytes;
1057 for (int i=0; i<bytes.length; ++i)
1058 if (bytes[i] == File.separatorChar)
1059 bytes[i] = '/';
1060 return bytes;
1064 * @return an important state
1066 public RepositoryState getRepositoryState() {
1067 // Pre Git-1.6 logic
1068 if (new File(getWorkDir(), ".dotest").exists())
1069 return RepositoryState.REBASING;
1070 if (new File(gitDir,".dotest-merge").exists())
1071 return RepositoryState.REBASING_INTERACTIVE;
1073 // From 1.6 onwards
1074 if (new File(getDirectory(),"rebase-apply/rebasing").exists())
1075 return RepositoryState.REBASING_REBASING;
1076 if (new File(getDirectory(),"rebase-apply/applying").exists())
1077 return RepositoryState.APPLY;
1078 if (new File(getDirectory(),"rebase-apply").exists())
1079 return RepositoryState.REBASING;
1081 if (new File(getDirectory(),"rebase-merge/interactive").exists())
1082 return RepositoryState.REBASING_INTERACTIVE;
1083 if (new File(getDirectory(),"rebase-merge").exists())
1084 return RepositoryState.REBASING_MERGE;
1086 // Both versions
1087 if (new File(gitDir,"MERGE_HEAD").exists())
1088 return RepositoryState.MERGING;
1089 if (new File(gitDir,"BISECT_LOG").exists())
1090 return RepositoryState.BISECTING;
1092 return RepositoryState.SAFE;
1096 * Check validity of a ref name. It must not contain character that has
1097 * a special meaning in a Git object reference expression. Some other
1098 * dangerous characters are also excluded.
1100 * @param refName
1102 * @return true if refName is a valid ref name
1104 public static boolean isValidRefName(final String refName) {
1105 final int len = refName.length();
1106 if (len == 0)
1107 return false;
1109 char p = '\0';
1110 for (int i=0; i<len; ++i) {
1111 char c = refName.charAt(i);
1112 if (c <= ' ')
1113 return false;
1114 switch(c) {
1115 case '.':
1116 if (i == 0)
1117 return false;
1118 if (p == '/')
1119 return false;
1120 if (p == '.')
1121 return false;
1122 break;
1123 case '/':
1124 if (i == 0)
1125 return false;
1126 if (i == len -1)
1127 return false;
1128 break;
1129 case '~': case '^': case ':':
1130 case '?': case '[':
1131 return false;
1132 case '*':
1133 return false;
1135 p = c;
1137 return true;
1141 * Strip work dir and return normalized repository path
1143 * @param wd Work dir
1144 * @param f File whose path shall be stripped of its workdir
1145 * @return normalized repository relative path
1147 public static String stripWorkDir(File wd, File f) {
1148 String relName = f.getPath().substring(wd.getPath().length() + 1);
1149 relName = relName.replace(File.separatorChar, '/');
1150 return relName;
1154 * @return the workdir file, i.e. where the files are checked out
1156 public File getWorkDir() {
1157 return getDirectory().getParentFile();
1161 * Register a {@link RepositoryListener} which will be notified
1162 * when ref changes are detected.
1164 * @param l
1166 public void addRepositoryChangedListener(final RepositoryListener l) {
1167 listeners.add(l);
1171 * Remove a registered {@link RepositoryListener}
1172 * @param l
1174 public void removeRepositoryChangedListener(final RepositoryListener l) {
1175 listeners.remove(l);
1179 * Register a global {@link RepositoryListener} which will be notified
1180 * when a ref changes in any repository are detected.
1182 * @param l
1184 public static void addAnyRepositoryChangedListener(final RepositoryListener l) {
1185 allListeners.add(l);
1189 * Remove a globally registered {@link RepositoryListener}
1190 * @param l
1192 public static void removeAnyRepositoryChangedListener(final RepositoryListener l) {
1193 allListeners.remove(l);
1196 void fireRefsMaybeChanged() {
1197 if (refs.lastRefModification != refs.lastNotifiedRefModification) {
1198 refs.lastNotifiedRefModification = refs.lastRefModification;
1199 final RefsChangedEvent event = new RefsChangedEvent(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.refsChanged(event);
1213 void fireIndexChanged() {
1214 final IndexChangedEvent event = new IndexChangedEvent(this);
1215 List<RepositoryListener> all;
1216 synchronized (listeners) {
1217 all = new ArrayList<RepositoryListener>(listeners);
1219 synchronized (allListeners) {
1220 all.addAll(allListeners);
1222 for (final RepositoryListener l : all) {
1223 l.indexChanged(event);
1228 * Force a scan for changed refs.
1230 * @throws IOException
1232 public void scanForRepoChanges() throws IOException {
1233 getAllRefs(); // This will look for changes to refs
1234 getIndex(); // This will detect changes in the index