Don't throw Error wrapping IOException from packed-refs read
[egit/zawir.git] / org.spearce.jgit / src / org / spearce / jgit / lib / Repository.java
blob25bcc7ac5514a0ac74a268c32a0167bd5c5e10c2
1 /*
2 * Copyright (C) 2006 Shawn Pearce <spearce@spearce.org>
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public
6 * License, version 2, as published by the Free Software Foundation.
8 * This library is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 * General Public License for more details.
13 * You should have received a copy of the GNU General Public
14 * License along with this library; if not, write to the Free Software
15 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
17 package org.spearce.jgit.lib;
19 import java.io.BufferedReader;
20 import java.io.File;
21 import java.io.FileNotFoundException;
22 import java.io.FileReader;
23 import java.io.FilenameFilter;
24 import java.io.IOException;
25 import java.util.ArrayList;
26 import java.util.Collection;
27 import java.util.HashMap;
28 import java.util.Map;
29 import java.util.Set;
31 import org.spearce.jgit.errors.IncorrectObjectTypeException;
32 import org.spearce.jgit.errors.ObjectWritingException;
33 import org.spearce.jgit.errors.RevisionSyntaxException;
34 import org.spearce.jgit.stgit.StGitPatch;
35 import org.spearce.jgit.util.FS;
37 /**
38 * Represents a Git repository. A repository holds all objects and refs used for
39 * managing source code (could by any type of file, but source code is what
40 * SCM's are typically used for).
42 * In Git terms all data is stored in GIT_DIR, typically a directory called
43 * .git. A work tree is maintained unless the repository is a bare repository.
44 * Typically the .git directory is located at the root of the work dir.
46 * <ul>
47 * <li>GIT_DIR
48 * <ul>
49 * <li>objects/ - objects</li>
50 * <li>refs/ - tags and heads</li>
51 * <li>config - configuration</li>
52 * <li>info/ - more configurations</li>
53 * </ul>
54 * </li>
55 * </ul>
57 * This implementation only handles a subtly undocumented subset of git features.
60 public class Repository {
61 private static final String[] refSearchPaths = { "", "refs/",
62 Constants.TAGS_PREFIX + "/", Constants.HEADS_PREFIX + "/",
63 Constants.REMOTES_PREFIX + "/" };
65 private final File gitDir;
67 private final File[] objectsDirs;
69 private final File refsDir;
71 private final File packedRefsFile;
73 private final RepositoryConfig config;
75 private PackFile[] packs;
77 private final WindowCache windows;
79 private GitIndex index;
81 /**
82 * Construct a representation of this git repo managing a Git repository.
84 * @param d
85 * GIT_DIR
86 * @throws IOException
88 public Repository(final File d) throws IOException {
89 this(null, d);
92 /**
93 * Construct a representation of a Git repository.
95 * @param wc
96 * cache this repository's data will be cached through during
97 * access. May be shared with another repository, or null to
98 * indicate this repository should allocate its own private
99 * cache.
100 * @param d
101 * GIT_DIR (the location of the repository metadata).
102 * @throws IOException
103 * the repository appears to already exist but cannot be
104 * accessed.
106 public Repository(final WindowCache wc, final File d) throws IOException {
107 gitDir = d.getAbsoluteFile();
108 try {
109 objectsDirs = readObjectsDirs(FS.resolve(gitDir, "objects"),
110 new ArrayList<File>()).toArray(new File[0]);
111 } catch (IOException e) {
112 IOException ex = new IOException("Cannot find all object dirs for " + gitDir);
113 ex.initCause(e);
114 throw ex;
116 refsDir = FS.resolve(gitDir, "refs");
117 packedRefsFile = FS.resolve(gitDir, "packed-refs");
118 packs = new PackFile[0];
119 config = new RepositoryConfig(this);
121 final boolean isExisting = objectsDirs[0].exists();
122 if (isExisting) {
123 getConfig().load();
124 final String repositoryFormatVersion = getConfig().getString(
125 "core", null, "repositoryFormatVersion");
126 if (!"0".equals(repositoryFormatVersion)) {
127 throw new IOException("Unknown repository format \""
128 + repositoryFormatVersion + "\"; expected \"0\".");
130 } else {
131 getConfig().create();
133 windows = wc != null ? wc : new WindowCache(getConfig());
134 if (isExisting)
135 scanForPacks();
138 private Collection<File> readObjectsDirs(File objectsDir, Collection<File> ret) throws IOException {
139 ret.add(objectsDir);
140 final File altFile = FS.resolve(objectsDir, "info/alternates");
141 if (altFile.exists()) {
142 BufferedReader ar = new BufferedReader(new FileReader(altFile));
143 for (String alt=ar.readLine(); alt!=null; alt=ar.readLine()) {
144 readObjectsDirs(FS.resolve(objectsDir, alt), ret);
146 ar.close();
148 return ret;
152 * Create a new Git repository initializing the necessary files and
153 * directories.
155 * @throws IOException
157 public void create() throws IOException {
158 if (gitDir.exists()) {
159 throw new IllegalStateException("Repository already exists: "
160 + gitDir);
163 gitDir.mkdirs();
165 objectsDirs[0].mkdirs();
166 new File(objectsDirs[0], "pack").mkdir();
167 new File(objectsDirs[0], "info").mkdir();
169 refsDir.mkdir();
170 new File(refsDir, "heads").mkdir();
171 new File(refsDir, "tags").mkdir();
173 new File(gitDir, "branches").mkdir();
174 new File(gitDir, "remotes").mkdir();
175 final String master = Constants.HEADS_PREFIX + "/" + Constants.MASTER;
176 writeSymref(Constants.HEAD, master);
178 getConfig().create();
179 getConfig().save();
183 * @return GIT_DIR
185 public File getDirectory() {
186 return gitDir;
190 * @return the directory containg the objects owned by this repository.
192 public File getObjectsDirectory() {
193 return objectsDirs[0];
197 * @return the configuration of this repository
199 public RepositoryConfig getConfig() {
200 return config;
204 * @return the cache needed for accessing packed objects in this repository.
206 public WindowCache getWindowCache() {
207 return windows;
211 * Construct a filename where the loose object having a specified SHA-1
212 * should be stored. If the object is stored in a shared repository the path
213 * to the alternative repo will be returned. If the object is not yet store
214 * a usable path in this repo will be returned. It is assumed that callers
215 * will look for objects in a pack first.
217 * @param objectId
218 * @return suggested file name
220 public File toFile(final AnyObjectId objectId) {
221 final String n = objectId.toString();
222 String d=n.substring(0, 2);
223 String f=n.substring(2);
224 for (int i=0; i<objectsDirs.length; ++i) {
225 File ret = new File(new File(objectsDirs[i], d), f);
226 if (ret.exists())
227 return ret;
229 return new File(new File(objectsDirs[0], d), f);
233 * @param objectId
234 * @return true if the specified object is stored in this repo or any of the
235 * known shared repositories.
237 public boolean hasObject(final AnyObjectId objectId) {
238 int k = packs.length;
239 if (k > 0) {
240 do {
241 if (packs[--k].hasObject(objectId))
242 return true;
243 } while (k > 0);
245 return toFile(objectId).isFile();
249 * @param id
250 * SHA-1 of an object.
252 * @return a {@link ObjectLoader} for accessing the data of the named
253 * object, or null if the object does not exist.
254 * @throws IOException
256 public ObjectLoader openObject(final AnyObjectId id)
257 throws IOException {
258 return openObject(new WindowCursor(),id);
262 * @param curs
263 * temporary working space associated with the calling thread.
264 * @param id
265 * SHA-1 of an object.
267 * @return a {@link ObjectLoader} for accessing the data of the named
268 * object, or null if the object does not exist.
269 * @throws IOException
271 public ObjectLoader openObject(final WindowCursor curs, final AnyObjectId id)
272 throws IOException {
273 int k = packs.length;
274 if (k > 0) {
275 do {
276 try {
277 final ObjectLoader ol = packs[--k].get(curs, id);
278 if (ol != null)
279 return ol;
280 } catch (IOException ioe) {
281 // This shouldn't happen unless the pack was corrupted
282 // after we opened it or the VM runs out of memory. This is
283 // a know problem with memory mapped I/O in java and have
284 // been noticed with JDK < 1.6. Tell the gc that now is a good
285 // time to collect and try once more.
286 try {
287 curs.release();
288 System.gc();
289 final ObjectLoader ol = packs[k].get(curs, id);
290 if (ol != null)
291 return ol;
292 } catch (IOException ioe2) {
293 ioe2.printStackTrace();
294 ioe.printStackTrace();
295 // Still fails.. that's BAD, maybe the pack has
296 // been corrupted after all, or the gc didn't manage
297 // to release enough previously mmaped areas.
300 } while (k > 0);
302 try {
303 return new UnpackedObjectLoader(this, id.toObjectId());
304 } catch (FileNotFoundException fnfe) {
305 return null;
310 * @param id
311 * SHA'1 of a blob
312 * @return an {@link ObjectLoader} for accessing the data of a named blob
313 * @throws IOException
315 public ObjectLoader openBlob(final ObjectId id) throws IOException {
316 return openObject(id);
320 * @param id
321 * SHA'1 of a tree
322 * @return an {@link ObjectLoader} for accessing the data of a named tree
323 * @throws IOException
325 public ObjectLoader openTree(final ObjectId id) throws IOException {
326 return openObject(id);
330 * Access a Commit object using a symbolic reference. This reference may
331 * be a SHA-1 or ref in combination with a number of symbols translating
332 * from one ref or SHA1-1 to another, such as HEAD^ etc.
334 * @param revstr a reference to a git commit object
335 * @return a Commit named by the specified string
336 * @throws IOException for I/O error or unexpected object type.
338 * @see #resolve(String)
340 public Commit mapCommit(final String revstr) throws IOException {
341 final ObjectId id = resolve(revstr);
342 return id != null ? mapCommit(id) : null;
346 * Access any type of Git object by id and
348 * @param id
349 * SHA-1 of object to read
350 * @param refName optional, only relevant for simple tags
351 * @return The Git object if found or null
352 * @throws IOException
354 public Object mapObject(final ObjectId id, final String refName) throws IOException {
355 final ObjectLoader or = openObject(id);
356 final byte[] raw = or.getBytes();
357 if (or.getType() == Constants.OBJ_TREE)
358 return makeTree(id, raw);
359 if (or.getType() == Constants.OBJ_COMMIT)
360 return makeCommit(id, raw);
361 if (or.getType() == Constants.OBJ_TAG)
362 return makeTag(id, refName, raw);
363 if (or.getType() == Constants.OBJ_BLOB)
364 return raw;
365 return null;
369 * Access a Commit by SHA'1 id.
370 * @param id
371 * @return Commit or null
372 * @throws IOException for I/O error or unexpected object type.
374 public Commit mapCommit(final ObjectId id) throws IOException {
375 final ObjectLoader or = openObject(id);
376 if (or == null)
377 return null;
378 final byte[] raw = or.getBytes();
379 if (Constants.OBJ_COMMIT == or.getType())
380 return new Commit(this, id, raw);
381 throw new IncorrectObjectTypeException(id, Constants.TYPE_COMMIT);
384 private Commit makeCommit(final ObjectId id, final byte[] raw) {
385 Commit ret = new Commit(this, id, raw);
386 return ret;
390 * Access a Tree object using a symbolic reference. This reference may
391 * be a SHA-1 or ref in combination with a number of symbols translating
392 * from one ref or SHA1-1 to another, such as HEAD^{tree} etc.
394 * @param revstr a reference to a git commit object
395 * @return a Tree named by the specified string
396 * @throws IOException
398 * @see #resolve(String)
400 public Tree mapTree(final String revstr) throws IOException {
401 final ObjectId id = resolve(revstr);
402 return id != null ? mapTree(id) : null;
406 * Access a Tree by SHA'1 id.
407 * @param id
408 * @return Tree or null
409 * @throws IOException for I/O error or unexpected object type.
411 public Tree mapTree(final ObjectId id) throws IOException {
412 final ObjectLoader or = openObject(id);
413 if (or == null)
414 return null;
415 final byte[] raw = or.getBytes();
416 if (Constants.OBJ_TREE == or.getType()) {
417 return new Tree(this, id, raw);
419 if (Constants.OBJ_COMMIT == or.getType())
420 return mapTree(ObjectId.fromString(raw, 5));
421 throw new IncorrectObjectTypeException(id, Constants.TYPE_TREE);
424 private Tree makeTree(final ObjectId id, final byte[] raw) throws IOException {
425 Tree ret = new Tree(this, id, raw);
426 return ret;
429 private Tag makeTag(final ObjectId id, final String refName, final byte[] raw) {
430 Tag ret = new Tag(this, id, refName, raw);
431 return ret;
435 * Access a tag by symbolic name.
437 * @param revstr
438 * @return a Tag or null
439 * @throws IOException on I/O error or unexpected type
441 public Tag mapTag(String revstr) throws IOException {
442 final ObjectId id = resolve(revstr);
443 return id != null ? mapTag(revstr, id) : null;
447 * Access a Tag by SHA'1 id
448 * @param refName
449 * @param id
450 * @return Commit or null
451 * @throws IOException for I/O error or unexpected object type.
453 public Tag mapTag(final String refName, final ObjectId id) throws IOException {
454 final ObjectLoader or = openObject(id);
455 if (or == null)
456 return null;
457 final byte[] raw = or.getBytes();
458 if (Constants.OBJ_TAG == or.getType())
459 return new Tag(this, id, refName, raw);
460 return new Tag(this, id, refName, null);
464 * Get a locked handle to a ref suitable for updating or creating.
466 * @param ref name to lock
467 * @return a locked ref
468 * @throws IOException
470 public LockFile lockRef(final String ref) throws IOException {
471 final Ref r = readRef(ref, true);
472 final LockFile l = new LockFile(fileForRef(r.getName()));
473 return l.lock() ? l : null;
477 * Create a command to update (or create) a ref in this repository.
479 * @param ref
480 * name of the ref the caller wants to modify.
481 * @return an update command. The caller must finish populating this command
482 * and then invoke one of the update methods to actually make a
483 * change.
484 * @throws IOException
485 * a symbolic ref was passed in and could not be resolved back
486 * to the base ref, as the symbolic ref could not be read.
488 public RefUpdate updateRef(final String ref) throws IOException {
489 final Ref r = readRef(ref, true);
490 return new RefUpdate(this, r, fileForRef(r.getName()));
494 * Parse a git revision string and return an object id.
496 * Currently supported is combinations of these.
497 * <ul>
498 * <li>SHA-1 - a SHA-1</li>
499 * <li>refs/... - a ref name</li>
500 * <li>ref^n - nth parent reference</li>
501 * <li>ref~n - distance via parent reference</li>
502 * <li>ref@{n} - nth version of ref</li>
503 * <li>ref^{tree} - tree references by ref</li>
504 * <li>ref^{commit} - commit references by ref</li>
505 * </ul>
507 * Not supported is
508 * <ul>
509 * <li>timestamps in reflogs, ref@{full or relative timestamp}</li>
510 * <li>abbreviated SHA-1's</li>
511 * </ul>
513 * @param revstr A git object references expression
514 * @return an ObjectId
515 * @throws IOException on serious errors
517 public ObjectId resolve(final String revstr) throws IOException {
518 char[] rev = revstr.toCharArray();
519 Object ref = null;
520 ObjectId refId = null;
521 for (int i = 0; i < rev.length; ++i) {
522 switch (rev[i]) {
523 case '^':
524 if (refId == null) {
525 String refstr = new String(rev,0,i);
526 refId = resolveSimple(refstr);
527 if (refId == null)
528 return null;
530 if (i + 1 < rev.length) {
531 switch (rev[i + 1]) {
532 case '0':
533 case '1':
534 case '2':
535 case '3':
536 case '4':
537 case '5':
538 case '6':
539 case '7':
540 case '8':
541 case '9':
542 int j;
543 ref = mapObject(refId, null);
544 if (!(ref instanceof Commit))
545 throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
546 for (j=i+1; j<rev.length; ++j) {
547 if (!Character.isDigit(rev[j]))
548 break;
550 String parentnum = new String(rev, i+1, j-i-1);
551 int pnum = Integer.parseInt(parentnum);
552 if (pnum != 0)
553 refId = ((Commit)ref).getParentIds()[pnum - 1];
554 i = j - 1;
555 break;
556 case '{':
557 int k;
558 String item = null;
559 for (k=i+2; k<rev.length; ++k) {
560 if (rev[k] == '}') {
561 item = new String(rev, i+2, k-i-2);
562 break;
565 i = k;
566 if (item != null)
567 if (item.equals("tree")) {
568 ref = mapObject(refId, null);
569 while (ref instanceof Tag) {
570 Tag t = (Tag)ref;
571 refId = t.getObjId();
572 ref = mapObject(refId, null);
574 if (ref instanceof Treeish)
575 refId = ((Treeish)ref).getTreeId();
576 else
577 throw new IncorrectObjectTypeException(refId, Constants.TYPE_TREE);
579 else if (item.equals("commit")) {
580 ref = mapObject(refId, null);
581 while (ref instanceof Tag) {
582 Tag t = (Tag)ref;
583 refId = t.getObjId();
584 ref = mapObject(refId, null);
586 if (!(ref instanceof Commit))
587 throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
589 else if (item.equals("blob")) {
590 ref = mapObject(refId, null);
591 while (ref instanceof Tag) {
592 Tag t = (Tag)ref;
593 refId = t.getObjId();
594 ref = mapObject(refId, null);
596 if (!(ref instanceof byte[]))
597 throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
599 else if (item.equals("")) {
600 ref = mapObject(refId, null);
601 if (ref instanceof Tag)
602 refId = ((Tag)ref).getObjId();
603 else {
604 // self
607 else
608 throw new RevisionSyntaxException(revstr);
609 else
610 throw new RevisionSyntaxException(revstr);
611 break;
612 default:
613 ref = mapObject(refId, null);
614 if (ref instanceof Commit)
615 refId = ((Commit)ref).getParentIds()[0];
616 else
617 throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
620 } else {
621 ref = mapObject(refId, null);
622 if (ref instanceof Commit)
623 refId = ((Commit)ref).getParentIds()[0];
624 else
625 throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT);
627 break;
628 case '~':
629 if (ref == null) {
630 String refstr = new String(rev,0,i);
631 refId = resolveSimple(refstr);
632 ref = mapCommit(refId);
634 int l;
635 for (l = i + 1; l < rev.length; ++l) {
636 if (!Character.isDigit(rev[l]))
637 break;
639 String distnum = new String(rev, i+1, l-i-1);
640 int dist = Integer.parseInt(distnum);
641 while (dist >= 0) {
642 refId = ((Commit)ref).getParentIds()[0];
643 ref = mapCommit(refId);
644 --dist;
646 i = l - 1;
647 break;
648 case '@':
649 int m;
650 String time = null;
651 for (m=i+2; m<rev.length; ++m) {
652 if (rev[m] == '}') {
653 time = new String(rev, i+2, m-i-2);
654 break;
657 if (time != null)
658 throw new RevisionSyntaxException("reflogs not yet supported by revision parser yet", revstr);
659 i = m - 1;
660 break;
661 default:
662 if (refId != null)
663 throw new RevisionSyntaxException(revstr);
666 if (refId == null)
667 refId = resolveSimple(revstr);
668 return refId;
671 private ObjectId resolveSimple(final String revstr) throws IOException {
672 if (ObjectId.isId(revstr))
673 return ObjectId.fromString(revstr);
674 final Ref r = readRef(revstr, false);
675 if (r != null) {
676 return r.getObjectId();
678 return null;
682 * Close all resources used by this repository
684 public void close() {
685 closePacks();
688 void closePacks() {
689 for (int k = packs.length - 1; k >= 0; k--) {
690 packs[k].close();
692 packs = new PackFile[0];
696 * Add a single existing pack to the list of available pack files.
698 * @param pack
699 * path of the pack file to open.
700 * @param idx
701 * path of the corresponding index file.
702 * @throws IOException
703 * index file could not be opened, read, or is not recognized as
704 * a Git pack file index.
706 public void openPack(final File pack, final File idx) throws IOException {
707 final String p = pack.getName();
708 final String i = idx.getName();
709 if (p.length() != 50 || !p.startsWith("pack-") || !p.endsWith(".pack"))
710 throw new IllegalArgumentException("Not a valid pack " + pack);
711 if (i.length() != 49 || !i.startsWith("pack-") || !i.endsWith(".idx"))
712 throw new IllegalArgumentException("Not a valid pack " + idx);
713 if (!p.substring(0,45).equals(i.substring(0,45)))
714 throw new IllegalArgumentException("Pack " + pack
715 + "does not match index " + idx);
717 final PackFile[] cur = packs;
718 final PackFile[] arr = new PackFile[cur.length + 1];
719 System.arraycopy(cur, 0, arr, 1, cur.length);
720 arr[0] = new PackFile(this, idx, pack);
721 packs = arr;
725 * Scan the object dirs, including alternates for packs
726 * to use.
728 public void scanForPacks() {
729 final ArrayList<PackFile> p = new ArrayList<PackFile>();
730 for (int i=0; i<objectsDirs.length; ++i)
731 scanForPacks(new File(objectsDirs[i], "pack"), p);
732 final PackFile[] arr = new PackFile[p.size()];
733 p.toArray(arr);
734 packs = arr;
737 private void scanForPacks(final File packDir, Collection<PackFile> packList) {
738 final String[] idxList = packDir.list(new FilenameFilter() {
739 public boolean accept(final File baseDir, final String n) {
740 // Must match "pack-[0-9a-f]{40}.idx" to be an index.
741 return n.length() == 49 && n.endsWith(".idx")
742 && n.startsWith("pack-");
745 if (idxList != null) {
746 for (final String indexName : idxList) {
747 final String n = indexName.substring(0, indexName.length() - 4);
748 final File idxFile = new File(packDir, n + ".idx");
749 final File packFile = new File(packDir, n + ".pack");
750 try {
751 packList.add(new PackFile(this, idxFile, packFile));
752 } catch (IOException ioe) {
753 // Whoops. That's not a pack!
755 ioe.printStackTrace();
762 * Writes a symref (e.g. HEAD) to disk
764 * @param name symref name
765 * @param target pointed to ref
766 * @throws IOException
768 public void writeSymref(final String name, final String target)
769 throws IOException {
770 final byte[] content = ("ref: " + target + "\n").getBytes("UTF-8");
771 final LockFile lck = new LockFile(fileForRef(name));
772 if (!lck.lock())
773 throw new ObjectWritingException("Unable to lock " + name);
774 try {
775 lck.write(content);
776 } catch (IOException ioe) {
777 throw new ObjectWritingException("Unable to write " + name, ioe);
779 if (!lck.commit())
780 throw new ObjectWritingException("Unable to write " + name);
783 private Ref readRef(final String revstr, final boolean missingOk)
784 throws IOException {
785 refreshPackedRefsCache();
786 for (int k = 0; k < refSearchPaths.length; k++) {
787 final Ref r = readRefBasic(refSearchPaths[k] + revstr);
788 if (missingOk || r.getObjectId() != null) {
789 return r;
792 return null;
795 private Ref readRefBasic(String name) throws IOException {
796 int depth = 0;
797 REF_READING: do {
798 // prefer unpacked ref to packed ref
799 final File f = fileForRef(name);
800 if (!f.isFile()) {
801 // look for packed ref, since this one doesn't exist
802 ObjectId id = packedRefs.get(name);
803 if (id != null)
804 return new Ref(name, id);
806 // no packed ref found, return blank one
807 return new Ref(name, null);
810 final BufferedReader br = new BufferedReader(new FileReader(f));
811 try {
812 final String line = br.readLine();
813 if (line == null || line.length() == 0)
814 return new Ref(name, null);
815 else if (line.startsWith("ref: ")) {
816 name = line.substring("ref: ".length());
817 continue REF_READING;
818 } else if (ObjectId.isId(line))
819 return new Ref(name, ObjectId.fromString(line));
820 throw new IOException("Not a ref: " + name + ": " + line);
821 } finally {
822 br.close();
824 } while (depth++ < 5);
825 throw new IOException("Exceed maximum ref depth. Circular reference?");
828 private File fileForRef(final String name) {
829 if (name.startsWith("refs/"))
830 return new File(refsDir, name.substring("refs/".length()));
831 return new File(gitDir, name);
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 the names of all refs (local and remotes branches, tags)
910 public Collection<String> getAllRefs() {
911 return listRefs("");
914 private Collection<String> listRefs(String refSubDir) {
915 // add / to end, unless empty
916 if (refSubDir.length() > 0 && refSubDir.charAt(refSubDir.length() -1 ) != '/')
917 refSubDir += "/";
919 Collection<String> branchesRaw = listFilesRecursively(new File(refsDir, refSubDir), null);
920 ArrayList<String> branches = new ArrayList<String>();
921 for (String b : branchesRaw) {
922 branches.add("refs/" + refSubDir + b);
925 refreshPackedRefsCache();
926 Set<String> keySet = packedRefs.keySet();
927 for (String s : keySet)
928 if (s.startsWith("refs/" + refSubDir) && !branches.contains(s))
929 branches.add(s);
930 return branches;
934 * @return all git tags
936 public Collection<String> getTags() {
937 return listRefs("tags");
940 private Map<String,ObjectId> packedRefs = new HashMap<String,ObjectId>();
941 private long packedrefstime = 0;
943 private void refreshPackedRefsCache() {
944 if (!packedRefsFile.exists()) {
945 if (packedRefs.size() > 0)
946 packedRefs = new HashMap<String,ObjectId>();
947 return;
949 if (packedRefsFile.lastModified() == packedrefstime)
950 return;
951 Map<String,ObjectId> newPackedRefs = new HashMap<String,ObjectId>();
952 FileReader fileReader = null;
953 try {
954 fileReader = new FileReader(packedRefsFile);
955 BufferedReader b=new BufferedReader(fileReader);
956 String p;
957 while ((p = b.readLine()) != null) {
958 if (p.charAt(0) == '#')
959 continue;
960 if (p.charAt(0) == '^') {
961 continue;
963 int spos = p.indexOf(' ');
964 ObjectId id = ObjectId.fromString(p.substring(0,spos));
965 String name = p.substring(spos+1);
966 newPackedRefs.put(name, id);
968 } catch (IOException e) {
969 throw new RuntimeException("Cannot read packed refs",e);
970 } finally {
971 if (fileReader != null) {
972 try {
973 fileReader.close();
974 } catch (IOException e) {
975 // Cannot do anything more here
976 e.printStackTrace();
980 packedRefs = newPackedRefs;
984 * @return true if HEAD points to a StGit patch.
986 public boolean isStGitMode() {
987 try {
988 File file = new File(getDirectory(), "HEAD");
989 BufferedReader reader = new BufferedReader(new FileReader(file));
990 String string = reader.readLine();
991 if (!string.startsWith("ref: refs/heads/"))
992 return false;
993 String branch = string.substring("ref: refs/heads/".length());
994 File currentPatches = new File(new File(new File(getDirectory(),
995 "patches"), branch), "applied");
996 if (!currentPatches.exists())
997 return false;
998 if (currentPatches.length() == 0)
999 return false;
1000 return true;
1002 } catch (IOException e) {
1003 e.printStackTrace();
1004 return false;
1009 * @return applied patches in a map indexed on current commit id
1010 * @throws IOException
1012 public Map<ObjectId,StGitPatch> getAppliedPatches() throws IOException {
1013 Map<ObjectId,StGitPatch> ret = new HashMap<ObjectId,StGitPatch>();
1014 if (isStGitMode()) {
1015 File patchDir = new File(new File(getDirectory(),"patches"),getBranch());
1016 BufferedReader apr = new BufferedReader(new FileReader(new File(patchDir,"applied")));
1017 for (String patchName=apr.readLine(); patchName!=null; patchName=apr.readLine()) {
1018 File topFile = new File(new File(new File(patchDir,"patches"), patchName), "top");
1019 BufferedReader tfr = new BufferedReader(new FileReader(topFile));
1020 String objectId = tfr.readLine();
1021 ObjectId id = ObjectId.fromString(objectId);
1022 ret.put(id, new StGitPatch(patchName, id));
1023 tfr.close();
1025 apr.close();
1027 return ret;
1030 private Collection<String> listFilesRecursively(File root, File start) {
1031 if (start == null)
1032 start = root;
1033 Collection<String> ret = new ArrayList<String>();
1034 File[] files = start.listFiles();
1035 for (int i = 0; i < files.length; ++i) {
1036 if (files[i].isDirectory())
1037 ret.addAll(listFilesRecursively(root, files[i]));
1038 else if (files[i].length() == 41) {
1039 String name = files[i].toString().substring(
1040 root.toString().length() + 1);
1041 if (File.separatorChar != '/')
1042 name = name.replace(File.separatorChar, '/');
1043 ret.add(name);
1046 return ret;
1049 /** Clean up stale caches */
1050 public void refreshFromDisk() {
1051 packedRefs = null;
1055 * @return a representation of the index associated with this repo
1056 * @throws IOException
1058 public GitIndex getIndex() throws IOException {
1059 if (index == null) {
1060 index = new GitIndex(this);
1061 index.read();
1062 } else {
1063 index.rereadIfNecessary();
1065 return index;
1068 static byte[] gitInternalSlash(byte[] bytes) {
1069 if (File.separatorChar == '/')
1070 return bytes;
1071 for (int i=0; i<bytes.length; ++i)
1072 if (bytes[i] == File.separatorChar)
1073 bytes[i] = '/';
1074 return bytes;
1078 * @return an important state
1080 public RepositoryState getRepositoryState() {
1081 if (new File(getWorkDir(), ".dotest").exists())
1082 return RepositoryState.REBASING;
1083 if (new File(gitDir,".dotest-merge").exists())
1084 return RepositoryState.REBASING_INTERACTIVE;
1085 if (new File(gitDir,"MERGE_HEAD").exists())
1086 return RepositoryState.MERGING;
1087 if (new File(gitDir,"BISECT_LOG").exists())
1088 return RepositoryState.BISECTING;
1089 return RepositoryState.SAFE;
1093 * Check validty of a ref name. It must not contain character that has
1094 * a special meaning in a Git object reference expression. Some other
1095 * dangerous characters are also excluded.
1097 * @param refName
1099 * @return true if refName is a valid ref name
1101 public static boolean isValidRefName(final String refName) {
1102 final int len = refName.length();
1103 char p = '\0';
1104 for (int i=0; i<len; ++i) {
1105 char c = refName.charAt(i);
1106 if (c <= ' ')
1107 return false;
1108 switch(c) {
1109 case '.':
1110 if (i == 0)
1111 return false;
1112 if (p == '/')
1113 return false;
1114 if (p == '.')
1115 return false;
1116 break;
1117 case '/':
1118 if (i == 0)
1119 return false;
1120 if (i == len -1)
1121 return false;
1122 break;
1123 case '~': case '^': case ':':
1124 case '?': case '[':
1125 return false;
1126 case '*':
1127 return false;
1129 p = c;
1131 return true;
1135 * String work dir and return normalized repository path
1137 * @param wd Work dir
1138 * @param f File whose path shall be stripp off it's workdir
1139 * @return normalized repository relative path
1141 public static String stripWorkDir(File wd, File f) {
1142 String relName = f.getPath().substring(wd.getPath().length() + 1);
1143 relName = relName.replace(File.separatorChar, '/');
1144 return relName;
1148 * @return the workdir file, i.e. where the files are checked out
1150 public File getWorkDir() {
1151 return getDirectory().getParentFile();