Make sure we get forward slashes for refs
[egit.git] / org.spearce.jgit / src / org / spearce / jgit / lib / Repository.java
blob3230ca13dd4aa84283f345e65af113119c6729af
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.FileFilter;
22 import java.io.FileNotFoundException;
23 import java.io.FileReader;
24 import java.io.IOException;
25 import java.lang.ref.Reference;
26 import java.lang.ref.SoftReference;
27 import java.util.ArrayList;
28 import java.util.Collection;
29 import java.util.HashMap;
30 import java.util.Map;
31 import java.util.Set;
32 import java.util.WeakHashMap;
34 import org.spearce.jgit.errors.IncorrectObjectTypeException;
35 import org.spearce.jgit.errors.ObjectWritingException;
37 public class Repository {
38 private static final String[] refSearchPaths = { "", "refs/", "refs/tags/",
39 "refs/heads/", };
41 private final File gitDir;
43 private final File[] objectsDirs;
45 private final File refsDir;
47 private final RepositoryConfig config;
49 private PackFile[] packs;
51 private WindowCache windows;
53 private Map<ObjectId,Reference<Tree>> treeCache = new WeakHashMap<ObjectId,Reference<Tree>>(30000);
54 private Map<ObjectId,Reference<Commit>> commitCache = new WeakHashMap<ObjectId,Reference<Commit>>(30000);
56 private GitIndex index;
58 public Repository(final File d) throws IOException {
59 gitDir = d.getAbsoluteFile();
60 try {
61 objectsDirs = readObjectsDirs(new File(gitDir, "objects"), new ArrayList<File>()).toArray(new File[0]);
62 } catch (IOException e) {
63 IOException ex = new IOException("Cannot find all object dirs for " + gitDir);
64 ex.initCause(e);
65 throw ex;
67 refsDir = new File(gitDir, "refs");
68 packs = new PackFile[0];
69 config = new RepositoryConfig(this);
70 if (objectsDirs[0].exists()) {
71 getConfig().load();
72 final String repositoryFormatVersion = getConfig().getString(
73 "core", "repositoryFormatVersion");
74 if (!"0".equals(repositoryFormatVersion)) {
75 throw new IOException("Unknown repository format \""
76 + repositoryFormatVersion + "\"; expected \"0\".");
78 initializeWindowCache();
79 scanForPacks();
83 private Collection<File> readObjectsDirs(File objectsDir, Collection<File> ret) throws IOException {
84 ret.add(objectsDir);
85 File alternatesFile = new File(objectsDir,"info/alternates");
86 if (alternatesFile.exists()) {
87 BufferedReader ar = new BufferedReader(new FileReader(alternatesFile));
88 for (String alt=ar.readLine(); alt!=null; alt=ar.readLine()) {
89 readObjectsDirs(new File(alt), ret);
91 ar.close();
93 return ret;
96 public void create() throws IOException {
97 if (gitDir.exists()) {
98 throw new IllegalStateException("Repository already exists: "
99 + gitDir);
102 gitDir.mkdirs();
104 objectsDirs[0].mkdirs();
105 new File(objectsDirs[0], "pack").mkdir();
106 new File(objectsDirs[0], "info").mkdir();
108 refsDir.mkdir();
109 new File(refsDir, "heads").mkdir();
110 new File(refsDir, "tags").mkdir();
112 new File(gitDir, "branches").mkdir();
113 new File(gitDir, "remotes").mkdir();
114 writeSymref("HEAD", "refs/heads/master");
116 getConfig().create();
117 getConfig().save();
118 initializeWindowCache();
121 private void initializeWindowCache() {
122 // FIXME these should be configurable...
123 windows = new WindowCache(256 * 1024 * 1024, 4);
126 public File getDirectory() {
127 return gitDir;
130 public File getObjectsDirectory() {
131 return objectsDirs[0];
134 public RepositoryConfig getConfig() {
135 return config;
138 public WindowCache getWindowCache() {
139 return windows;
142 public File toFile(final ObjectId objectId) {
143 final String n = objectId.toString();
144 String d=n.substring(0, 2);
145 String f=n.substring(2);
146 for (int i=0; i<objectsDirs.length; ++i) {
147 File ret = new File(new File(objectsDirs[i], d), f);
148 if (ret.exists())
149 return ret;
151 return new File(new File(objectsDirs[0], d), f);
154 public boolean hasObject(final ObjectId objectId) {
155 int k = packs.length;
156 if (k > 0) {
157 do {
158 if (packs[--k].hasObject(objectId))
159 return true;
160 } while (k > 0);
162 return toFile(objectId).isFile();
165 public ObjectLoader openObject(final ObjectId id) throws IOException {
166 int k = packs.length;
167 if (k > 0) {
168 do {
169 try {
170 final ObjectLoader ol = packs[--k].get(id);
171 if (ol != null)
172 return ol;
173 } catch (IOException ioe) {
174 // This shouldn't happen unless the pack was corrupted
175 // after we opened it or the VM runs out of memory. This is
176 // a know problem with memory mapped I/O in java and have
177 // been noticed with JDK < 1.6. Tell the gc that now is a good
178 // time to collect and try once more.
179 try {
180 System.gc();
181 final ObjectLoader ol = packs[k].get(id);
182 if (ol != null)
183 return ol;
184 } catch (IOException ioe2) {
185 ioe2.printStackTrace();
186 ioe.printStackTrace();
187 // Still fails.. that's BAD, maybe the pack has
188 // been corrupted after all, or the gc didn't manage
189 // to release enough previously mmaped areas.
192 } while (k > 0);
194 try {
195 return new UnpackedObjectLoader(this, id);
196 } catch (FileNotFoundException fnfe) {
197 return null;
201 public ObjectLoader openBlob(final ObjectId id) throws IOException {
202 return openObject(id);
205 public ObjectLoader openTree(final ObjectId id) throws IOException {
206 return openObject(id);
209 public Commit mapCommit(final String revstr) throws IOException {
210 final ObjectId id = resolve(revstr);
211 return id != null ? mapCommit(id) : null;
214 public Commit mapCommit(final ObjectId id) throws IOException {
215 Reference<Commit> retr = commitCache.get(id);
216 if (retr != null) {
217 Commit ret = retr.get();
218 if (ret != null)
219 return ret;
220 System.out.println("Found a null id, size was "+commitCache.size());
223 final ObjectLoader or = openObject(id);
224 if (or == null)
225 return null;
226 final byte[] raw = or.getBytes();
227 if (Constants.TYPE_COMMIT.equals(or.getType())) {
228 Commit ret = new Commit(this, id, raw);
229 // The key must not be the referenced strongly
230 // by the value in WeakHashMaps
231 commitCache.put(id, new SoftReference<Commit>(ret));
232 return ret;
234 throw new IncorrectObjectTypeException(id, Constants.TYPE_COMMIT);
237 public Tree mapTree(final String revstr) throws IOException {
238 final ObjectId id = resolve(revstr);
239 return id != null ? mapTree(id) : null;
242 public Tree mapTree(final ObjectId id) throws IOException {
243 Reference<Tree> wret = treeCache.get(id);
244 if (wret != null) {
245 Tree ret = wret.get();
246 if (ret != null)
247 return ret;
250 final ObjectLoader or = openObject(id);
251 if (or == null)
252 return null;
253 final byte[] raw = or.getBytes();
254 if (Constants.TYPE_TREE.equals(or.getType())) {
255 Tree ret = new Tree(this, id, raw);
256 treeCache.put(id, new SoftReference<Tree>(ret));
257 return ret;
259 if (Constants.TYPE_COMMIT.equals(or.getType()))
260 return mapTree(ObjectId.fromString(raw, 5));
261 throw new IncorrectObjectTypeException(id, Constants.TYPE_TREE);
264 public Tag mapTag(String revstr) throws IOException {
265 final ObjectId id = resolve(revstr);
266 return id != null ? mapTag(revstr, id) : null;
269 public Tag mapTag(final String refName, final ObjectId id) throws IOException {
270 final ObjectLoader or = openObject(id);
271 if (or == null)
272 return null;
273 final byte[] raw = or.getBytes();
274 if (Constants.TYPE_TAG.equals(or.getType()))
275 return new Tag(this, id, refName, raw);
276 return new Tag(this, id, refName, null);
279 public RefLock lockRef(final String ref) throws IOException {
280 final Ref r = readRef(ref, true);
281 final RefLock l = new RefLock(new File(gitDir, r.getName()));
282 return l.lock() ? l : null;
285 /** Parse a git revision string and return an object id.
287 * It is not fully implemented, so it only deals with
288 * commits and to some extent tags. Reflogs are not
289 * supported yet.
290 * The plan is to implement it fully.
291 * @param revstr A git object references expression
292 * @return an ObjectId
293 * @throws IOException on serious errors
295 public ObjectId parse(String revstr) throws IOException {
296 char[] rev = revstr.toCharArray();
297 ObjectId ret = null;
298 Commit ref = null;
299 for (int i = 0; i < rev.length; ++i) {
300 switch (rev[i]) {
301 case '^':
302 if (ref == null) {
303 String refstr = new String(rev,0,i);
304 ObjectId refId = resolveSimple(refstr);
305 ref = mapCommit(refId);
307 if (i + 1 < rev.length) {
308 switch (rev[i + 1]) {
309 case '0':
310 case '1':
311 case '2':
312 case '3':
313 case '4':
314 case '5':
315 case '6':
316 case '7':
317 case '8':
318 case '9':
319 int j;
320 for (j=i+1; j<rev.length; ++j) {
321 if (!Character.isDigit(rev[j]))
322 break;
324 String parentnum = new String(rev, i+1, j-i-1);
325 int pnum = Integer.parseInt(parentnum);
326 if (pnum != 0)
327 ref = mapCommit(ref.getParentIds()[pnum - 1]);
328 i = j - 1;
329 break;
330 case '{':
331 int k;
332 String item = null;
333 for (k=i+2; k<rev.length; ++k) {
334 if (rev[k] == '}') {
335 item = new String(rev, i+2, k-i-2);
336 break;
339 i = k;
340 if (item != null)
341 if (item.equals("tree"))
342 ret = ref.getTreeId();
343 else if (item.equals("commit"))
344 ; // just reference self
345 else
346 return null; // invalid
347 else
348 return null; // invalid
349 break;
350 default:
351 ref = mapCommit(ref.getParentIds()[0]);
353 } else {
354 ref = mapCommit(ref.getParentIds()[0]);
356 break;
357 case '~':
358 if (ref == null) {
359 String refstr = new String(rev,0,i);
360 ObjectId refId = resolveSimple(refstr);
361 ref = mapCommit(refId);
363 int l;
364 for (l = i + 1; l < rev.length; ++l) {
365 if (!Character.isDigit(rev[l]))
366 break;
368 String distnum = new String(rev, i+1, l-i-1);
369 int dist = Integer.parseInt(distnum);
370 while (dist >= 0) {
371 ref = mapCommit(ref.getParentIds()[0]);
372 --dist;
374 i = l - 1;
375 break;
376 case '@':
377 int m;
378 String time = null;
379 for (m=i+2; m<rev.length; ++m) {
380 if (rev[m] == '}') {
381 time = new String(rev, i+2, m-i-2);
382 break;
385 if (time != null)
386 throw new IllegalArgumentException("reflogs not yet supprted");
387 i = m - 1;
388 break;
389 default:
390 if (ref != null)
391 return null; // cannot parse, return null
394 if (ret == null)
395 if (ref != null)
396 ret = ref.getCommitId();
397 else
398 ret = resolveSimple(revstr);
399 return ret;
402 public ObjectId resolve(final String revstr) throws IOException {
403 return parse(revstr);
406 public ObjectId resolveSimple(final String revstr) throws IOException {
407 ObjectId id = null;
409 if (ObjectId.isId(revstr)) {
410 id = new ObjectId(revstr);
413 if (id == null) {
414 final Ref r = readRef(revstr, false);
415 if (r != null) {
416 id = r.getObjectId();
420 return id;
423 public void close() throws IOException {
424 closePacks();
427 public void closePacks() throws IOException {
428 for (int k = packs.length - 1; k >= 0; k--) {
429 packs[k].close();
431 packs = new PackFile[0];
434 public void scanForPacks() {
435 final ArrayList<PackFile> p = new ArrayList<PackFile>();
436 for (int i=0; i<objectsDirs.length; ++i)
437 scanForPacks(new File(objectsDirs[i], "pack"), p);
438 final PackFile[] arr = new PackFile[p.size()];
439 p.toArray(arr);
440 packs = arr;
443 public void scanForPacks(final File packDir, Collection<PackFile> packList) {
444 final File[] list = packDir.listFiles(new FileFilter() {
445 public boolean accept(final File f) {
446 final String n = f.getName();
447 if (!n.endsWith(".pack")) {
448 return false;
450 final String nBase = n.substring(0, n.lastIndexOf('.'));
451 final File idx = new File(packDir, nBase + ".idx");
452 return f.isFile() && f.canRead() && idx.isFile()
453 && idx.canRead();
456 if (list != null) {
457 for (int k = 0; k < list.length; k++) {
458 try {
459 packList.add(new PackFile(this, list[k]));
460 } catch (IOException ioe) {
461 // Whoops. That's not a pack!
468 public void writeSymref(final String name, final String target)
469 throws IOException {
470 final byte[] content = ("ref: " + target + "\n").getBytes("UTF-8");
471 final RefLock lck = new RefLock(new File(gitDir, name));
472 if (!lck.lock())
473 throw new ObjectWritingException("Unable to lock " + name);
474 try {
475 lck.write(content);
476 } catch (IOException ioe) {
477 throw new ObjectWritingException("Unable to write " + name, ioe);
479 if (!lck.commit())
480 throw new ObjectWritingException("Unable to write " + name);
483 private Ref readRef(final String revstr, final boolean missingOk)
484 throws IOException {
485 refreshPackredRefsCache();
486 for (int k = 0; k < refSearchPaths.length; k++) {
487 final Ref r = readRefBasic(refSearchPaths[k] + revstr);
488 if (missingOk || r.getObjectId() != null) {
489 return r;
492 return null;
495 private Ref readRefBasic(String name) throws IOException {
496 int depth = 0;
497 REF_READING: do {
498 // prefer unpacked ref to packed ref
499 final File f = new File(getDirectory(), name);
500 if (!f.isFile()) {
501 // look for packed ref, since this one doesn't exist
502 ObjectId id = packedRefs.get(name);
503 if (id != null)
504 return new Ref(name, id);
506 // no packed ref found, return blank one
507 return new Ref(name, null);
510 final BufferedReader br = new BufferedReader(new FileReader(f));
511 try {
512 final String line = br.readLine();
513 if (line == null || line.length() == 0)
514 return new Ref(name, null);
515 else if (line.startsWith("ref: ")) {
516 name = line.substring("ref: ".length());
517 continue REF_READING;
518 } else if (ObjectId.isId(line))
519 return new Ref(name, new ObjectId(line));
520 throw new IOException("Not a ref: " + name + ": " + line);
521 } finally {
522 br.close();
524 } while (depth++ < 5);
525 throw new IOException("Exceed maximum ref depth. Circular reference?");
528 public String toString() {
529 return "Repository[" + getDirectory() + "]";
532 public String getPatch() throws IOException {
533 final File ptr = new File(getDirectory(),"patches/"+getBranch()+"/applied");
534 final BufferedReader br = new BufferedReader(new FileReader(ptr));
535 String last=null;
536 try {
537 String line;
538 while ((line=br.readLine())!=null) {
539 last = line;
541 } finally {
542 br.close();
544 return last;
547 public String getFullBranch() throws IOException {
548 final File ptr = new File(getDirectory(),"HEAD");
549 final BufferedReader br = new BufferedReader(new FileReader(ptr));
550 String ref;
551 try {
552 ref = br.readLine();
553 } finally {
554 br.close();
556 if (ref.startsWith("ref: "))
557 ref = ref.substring(5);
558 return ref;
561 public String getBranch() throws IOException {
562 final File ptr = new File(getDirectory(),"HEAD");
563 final BufferedReader br = new BufferedReader(new FileReader(ptr));
564 String ref;
565 try {
566 ref = br.readLine();
567 } finally {
568 br.close();
570 if (ref.startsWith("ref: "))
571 ref = ref.substring(5);
572 if (ref.startsWith("refs/heads/"))
573 ref = ref.substring(11);
574 return ref;
577 public Collection<String> getBranches() {
578 return listRefs("heads");
581 public Collection<String> getAllRefs() {
582 return listRefs("");
585 private Collection<String> listRefs(String refSubDir) {
586 // add / to end, unless empty
587 if (refSubDir.length() > 0 && refSubDir.charAt(refSubDir.length() -1 ) != '/')
588 refSubDir += "/";
590 Collection<String> branchesRaw = listFilesRecursively(new File(refsDir, refSubDir), null);
591 ArrayList<String> branches = new ArrayList<String>();
592 for (String b : branchesRaw) {
593 branches.add("refs/" + refSubDir + b);
596 refreshPackredRefsCache();
597 Set<String> keySet = packedRefs.keySet();
598 for (String s : keySet)
599 if (s.startsWith("refs/" + refSubDir) && !branches.contains(s))
600 branches.add(s);
601 return branches;
604 public Collection<String> getTags() {
605 return listRefs("tags");
608 private Map<String,ObjectId> packedRefs = new HashMap<String,ObjectId>();
609 private long packedrefstime = 0;
611 private void refreshPackredRefsCache() {
612 File file = new File(gitDir, "packed-refs");
613 if (!file.exists()) {
614 if (packedRefs.size() > 0)
615 packedRefs = new HashMap<String,ObjectId>();
616 return;
618 if (file.lastModified() == packedrefstime)
619 return;
620 Map<String,ObjectId> newPackedRefs = new HashMap<String,ObjectId>();
621 FileReader fileReader = null;
622 try {
623 fileReader = new FileReader(file);
624 BufferedReader b=new BufferedReader(fileReader);
625 String p;
626 while ((p = b.readLine()) != null) {
627 if (p.charAt(0) == '#')
628 continue;
629 if (p.charAt(0) == '^') {
630 continue;
632 int spos = p.indexOf(' ');
633 ObjectId id = new ObjectId(p.substring(0,spos));
634 String name = p.substring(spos+1);
635 newPackedRefs.put(name, id);
637 } catch (IOException e) {
638 throw new Error("Cannot read packed refs",e);
639 } finally {
640 if (fileReader != null) {
641 try {
642 fileReader.close();
643 } catch (IOException e) {
644 // Cannot do anything more here
645 e.printStackTrace();
649 packedRefs = newPackedRefs;
653 * @return true if HEAD points to a StGit patch.
655 public boolean isStGitMode() {
656 try {
657 File file = new File(getDirectory(), "HEAD");
658 BufferedReader reader = new BufferedReader(new FileReader(file));
659 String string = reader.readLine();
660 if (!string.startsWith("ref: refs/heads/"))
661 return false;
662 String branch = string.substring("ref: refs/heads/".length());
663 File currentPatches = new File(new File(new File(getDirectory(),
664 "patches"), branch), "applied");
665 if (!currentPatches.exists())
666 return false;
667 if (currentPatches.length() == 0)
668 return false;
669 return true;
671 } catch (IOException e) {
672 e.printStackTrace();
673 return false;
677 public static class StGitPatch {
678 public StGitPatch(String patchName, ObjectId id) {
679 name = patchName;
680 gitId = id;
682 public ObjectId getGitId() {
683 return gitId;
685 public String getName() {
686 return name;
688 private String name;
689 private ObjectId gitId;
693 * @return applied patches in a map indexed on current commit id
694 * @throws IOException
696 public Map<ObjectId,StGitPatch> getAppliedPatches() throws IOException {
697 Map<ObjectId,StGitPatch> ret = new HashMap<ObjectId,StGitPatch>();
698 if (isStGitMode()) {
699 File patchDir = new File(new File(getDirectory(),"patches"),getBranch());
700 BufferedReader apr = new BufferedReader(new FileReader(new File(patchDir,"applied")));
701 for (String patchName=apr.readLine(); patchName!=null; patchName=apr.readLine()) {
702 File topFile = new File(new File(new File(patchDir,"patches"), patchName), "top");
703 BufferedReader tfr = new BufferedReader(new FileReader(topFile));
704 String objectId = tfr.readLine();
705 ObjectId id = new ObjectId(objectId);
706 ret.put(id, new StGitPatch(patchName, id));
707 tfr.close();
709 apr.close();
711 return ret;
714 private Collection<String> listFilesRecursively(File root, File start) {
715 if (start == null)
716 start = root;
717 Collection<String> ret = new ArrayList<String>();
718 File[] files = start.listFiles();
719 for (int i = 0; i < files.length; ++i) {
720 if (files[i].isDirectory())
721 ret.addAll(listFilesRecursively(root, files[i]));
722 else if (files[i].length() == 41) {
723 String name = files[i].toString().substring(
724 root.toString().length() + 1);
725 if (File.separatorChar != '/')
726 name = name.replace(File.separatorChar, '/');
727 ret.add(name);
730 return ret;
733 /** Clean up stale caches */
734 public void refreshFromDisk() {
735 packedRefs = null;
738 public GitIndex getIndex() throws IOException {
739 if (index == null) {
740 index = new GitIndex(this);
741 index.read();
742 } else {
743 index.rereadIfNecessary();
745 return index;
748 public static byte[] gitInternalSlash(byte[] bytes) {
749 if (File.separatorChar == '/')
750 return bytes;
751 for (int i=0; i<bytes.length; ++i)
752 if (bytes[i] == File.separatorChar)
753 bytes[i] = '/';
754 return bytes;