Treat the absence of a Git index as an empty index.
[egit.git] / org.spearce.jgit / src / org / spearce / jgit / lib / GitIndex.java
blob3afa6fb5d774a0dcb6ce533c1a15441ab0528345
1 package org.spearce.jgit.lib;
3 import java.io.File;
4 import java.io.FileInputStream;
5 import java.io.FileNotFoundException;
6 import java.io.FileOutputStream;
7 import java.io.IOException;
8 import java.io.InputStream;
9 import java.io.RandomAccessFile;
10 import java.io.UnsupportedEncodingException;
11 import java.lang.reflect.InvocationTargetException;
12 import java.lang.reflect.Method;
13 import java.nio.ByteBuffer;
14 import java.nio.ByteOrder;
15 import java.nio.channels.FileChannel;
16 import java.security.MessageDigest;
17 import java.util.Comparator;
18 import java.util.Date;
19 import java.util.Iterator;
20 import java.util.Map;
21 import java.util.Stack;
22 import java.util.TreeMap;
24 import org.spearce.jgit.errors.CorruptObjectException;
25 import org.spearce.jgit.errors.NotSupportedException;
27 public class GitIndex {
29 /** Stage 0 represents merged entries. */
30 public static final int STAGE_0 = 0;
32 private RandomAccessFile cache;
34 private File cacheFile;
36 // Index is modified
37 private boolean changed;
39 // Stat information updated
40 private boolean statDirty;
42 private Header header;
44 private long lastCacheTime;
46 private final Repository db;
48 private Map entries = new TreeMap(new Comparator() {
49 public int compare(Object arg0, Object arg1) {
50 byte[] a = (byte[]) arg0;
51 byte[] b = (byte[]) arg1;
52 for (int i = 0; i < a.length && i < b.length; ++i) {
53 int c = a[i] - b[i];
54 if (c != 0)
55 return c;
57 if (a.length < b.length)
58 return -1;
59 else if (a.length > b.length)
60 return 1;
61 return 0;
63 });
65 public GitIndex(Repository db) {
66 this.db = db;
67 this.cacheFile = new File(db.getDirectory(), "index");
70 public boolean isChanged() {
71 return changed || statDirty;
74 public void rereadIfNecessary() throws IOException {
75 if (cacheFile.exists() && cacheFile.lastModified() != lastCacheTime) {
76 read();
80 public Entry add(File wd, File f) throws IOException {
81 byte[] key = makeKey(wd, f);
82 Entry e = (Entry) entries.get(key);
83 if (e == null) {
84 e = new Entry(key, f, 0);
85 entries.put(key, e);
86 } else {
87 e.update(f);
89 return e;
92 public boolean remove(File wd, File f) {
93 byte[] key = makeKey(wd, f);
94 return entries.remove(key) != null;
97 public void read() throws IOException {
98 long t0 = System.currentTimeMillis();
99 changed = false;
100 statDirty = false;
101 if (!cacheFile.exists()) {
102 header = null;
103 entries.clear();
104 lastCacheTime = 0;
105 return;
107 cache = new RandomAccessFile(cacheFile, "r");
108 try {
109 FileChannel channel = cache.getChannel();
110 ByteBuffer buffer = ByteBuffer.allocateDirect((int) cacheFile.length());
111 buffer.order(ByteOrder.BIG_ENDIAN);
112 int j = channel.read(buffer);
113 if (j != buffer.capacity())
114 throw new IOException("Could not read index in one go, only "+j+" out of "+buffer.capacity()+" read");
115 buffer.flip();
116 header = new Header(buffer);
117 entries.clear();
118 for (int i = 0; i < header.entries; ++i) {
119 Entry entry = new Entry(buffer);
120 entries.put(entry.name, entry);
122 long t1 = System.currentTimeMillis();
123 lastCacheTime = cacheFile.lastModified();
124 System.out.println("Read index "+cacheFile+" in "+((t1-t0)/1000.0)+"s");
125 } finally {
126 cache.close();
130 public void write() throws IOException {
131 checkWriteOk();
132 File tmpIndex = new File(cacheFile.getAbsoluteFile() + ".tmp");
133 File lock = new File(cacheFile.getAbsoluteFile() + ".lock");
134 if (!lock.createNewFile())
135 throw new IOException("Index file is in use");
136 try {
137 FileOutputStream fileOutputStream = new FileOutputStream(tmpIndex);
138 FileChannel fc = fileOutputStream.getChannel();
139 ByteBuffer buf = ByteBuffer.allocate(4096);
140 MessageDigest newMessageDigest = Constants.newMessageDigest();
141 header = new Header(entries);
142 header.write(buf);
143 buf.flip();
144 newMessageDigest
145 .update(buf.array(), buf.arrayOffset(), buf.limit());
146 fc.write(buf);
147 buf.flip();
148 buf.clear();
149 for (Iterator i = entries.values().iterator(); i.hasNext();) {
150 Entry e = (Entry) i.next();
151 e.write(buf);
152 buf.flip();
153 newMessageDigest.update(buf.array(), buf.arrayOffset(), buf
154 .limit());
155 fc.write(buf);
156 buf.flip();
157 buf.clear();
159 buf.put(newMessageDigest.digest());
160 buf.flip();
161 fc.write(buf);
162 fc.close();
163 fileOutputStream.close();
164 if (cacheFile.exists())
165 if (!cacheFile.delete())
166 throw new IOException(
167 "Could not rename delete old index");
168 if (!tmpIndex.renameTo(cacheFile))
169 throw new IOException(
170 "Could not rename temporary index file to index");
171 changed = false;
172 statDirty = false;
173 } finally {
174 if (!lock.delete())
175 throw new IOException(
176 "Could not delete lock file. Should not happen");
177 if (tmpIndex.exists() && !tmpIndex.delete())
178 throw new IOException(
179 "Could not delete temporary index file. Should not happen");
183 private void checkWriteOk() throws IOException {
184 for (Iterator i = entries.values().iterator(); i.hasNext();) {
185 Entry e = (Entry) i.next();
186 if (e.stage != 0) {
187 throw new NotSupportedException("Cannot work with other stages than zero right now. Won't write corrupt index.");
192 static Method canExecute;
193 static Method setExecute;
194 static {
195 try {
196 canExecute = File.class.getMethod("canExecute", (Class[]) null);
197 setExecute = File.class.getMethod("setExecutable", new Class[] { Boolean.TYPE });
198 } catch (SecurityException e) {
199 e.printStackTrace();
200 } catch (NoSuchMethodException e) {
201 System.out.println("This vm cannot handle execute file permission. Feature disabled");
206 * JDK1.6 has file.canExecute
208 * if (file.canExecute() != FileMode.EXECUTABLE_FILE.equals(mode))
209 * return true;
211 boolean File_canExecute(File f) {
212 if (canExecute != null) {
213 try {
214 return ((Boolean) canExecute.invoke(f, (Object[]) null))
215 .booleanValue();
216 } catch (IllegalArgumentException e) {
217 throw new Error(e);
218 } catch (IllegalAccessException e) {
219 throw new Error(e);
220 } catch (InvocationTargetException e) {
221 throw new Error(e);
223 } else
224 return false;
228 * JDK1.6 has file.setExecute
230 boolean File_setExecute(File f,boolean value) {
231 if (setExecute != null) {
232 try {
233 return ((Boolean) setExecute.invoke(f,
234 new Object[] { new Boolean(value) })).booleanValue();
235 } catch (IllegalArgumentException e) {
236 throw new Error(e);
237 } catch (IllegalAccessException e) {
238 throw new Error(e);
239 } catch (InvocationTargetException e) {
240 throw new Error(e);
242 } else
243 return false;
246 static byte[] makeKey(File wd, File f) {
247 if (!f.getPath().startsWith(wd.getPath()))
248 throw new Error("Path is not in working dir");
249 String relName = f.getPath().substring(wd.getPath().length() + 1)
250 .replace(File.separatorChar, '/');
251 return relName.getBytes();
254 Boolean filemode;
255 private boolean config_filemode() {
256 // temporary til we can actually set parameters. We need to be able
257 // to change this for testing.
258 if (filemode != null)
259 return filemode.booleanValue();
260 RepositoryConfig config = db.getConfig();
261 return config.getBoolean("core", "filemode", true);
264 public class Entry {
265 private long ctime;
267 private long mtime;
269 private int dev;
271 private int ino;
273 private int mode;
275 private int uid;
277 private int gid;
279 private int size;
281 private ObjectId sha1;
283 private short flags;
285 private byte[] name;
287 private int stage;
289 public Entry(byte[] key, File f, int stage)
290 throws IOException {
291 ctime = f.lastModified() * 1000000L;
292 mtime = ctime; // we use same here
293 dev = -1;
294 ino = -1;
295 if (config_filemode() && File_canExecute(f))
296 mode = FileMode.EXECUTABLE_FILE.getBits();
297 else
298 mode = FileMode.REGULAR_FILE.getBits();
299 uid = -1;
300 gid = -1;
301 size = (int) f.length();
302 ObjectWriter writer = new ObjectWriter(db);
303 sha1 = writer.writeBlob(f);
304 name = key;
305 flags = (short) ((stage << 12) | name.length); // TODO: fix flags
308 public Entry(TreeEntry f, int stage)
309 throws UnsupportedEncodingException {
310 ctime = -1; // hmm
311 mtime = -1;
312 dev = -1;
313 ino = -1;
314 mode = f.getMode().getBits();
315 uid = -1;
316 gid = -1;
317 try {
318 size = (int) db.openBlob(f.getId()).getSize();
319 } catch (IOException e) {
320 e.printStackTrace();
321 size = -1;
323 sha1 = f.getId();
324 name = f.getFullName().getBytes("UTF-8");
325 flags = (short) ((stage << 12) | name.length); // TODO: fix flags
328 Entry(ByteBuffer b) {
329 int startposition = b.position();
330 ctime = b.getInt() * 1000000000L + (b.getInt() % 1000000000L);
331 mtime = b.getInt() * 1000000000L + (b.getInt() % 1000000000L);
332 dev = b.getInt();
333 ino = b.getInt();
334 mode = b.getInt();
335 uid = b.getInt();
336 gid = b.getInt();
337 size = b.getInt();
338 byte[] sha1bytes = new byte[Constants.OBJECT_ID_LENGTH];
339 b.get(sha1bytes);
340 sha1 = new ObjectId(sha1bytes);
341 flags = b.getShort();
342 stage = (flags & 0x3000) >> 12;
343 name = new byte[flags & 0xFFF];
344 b.get(name);
346 .position(startposition
347 + ((8 + 8 + 4 + 4 + 4 + 4 + 4 + 4 + 20 + 2
348 + name.length + 8) & ~7));
351 public boolean update(File f) throws IOException {
352 boolean modified = false;
353 long lm = f.lastModified() * 1000000L;
354 if (mtime != lm)
355 modified = true;
356 mtime = f.lastModified() * 1000000L;
357 if (size != f.length())
358 modified = true;
359 if (config_filemode()) {
360 if (File_canExecute(f) != FileMode.EXECUTABLE_FILE.equals(mode)) {
361 mode = FileMode.EXECUTABLE_FILE.getBits();
362 modified = true;
365 if (modified) {
366 size = (int) f.length();
367 ObjectWriter writer = new ObjectWriter(db);
368 ObjectId newsha1 = sha1 = writer.writeBlob(f);
369 if (!newsha1.equals(sha1))
370 modified = true;
371 sha1 = newsha1;
373 return modified;
376 public void write(ByteBuffer buf) {
377 int startposition = buf.position();
378 buf.putInt((int) (ctime / 1000000000L));
379 buf.putInt((int) (ctime % 1000000000L));
380 buf.putInt((int) (mtime / 1000000000L));
381 buf.putInt((int) (mtime % 1000000000L));
382 buf.putInt(dev);
383 buf.putInt(ino);
384 buf.putInt(mode);
385 buf.putInt(uid);
386 buf.putInt(gid);
387 buf.putInt(size);
388 buf.put(sha1.getBytes());
389 buf.putShort(flags);
390 buf.put(name);
391 int end = startposition
392 + ((8 + 8 + 4 + 4 + 4 + 4 + 4 + 4 + 20 + 2 + name.length + 8) & ~7);
393 int remain = end - buf.position();
394 while (remain-- > 0)
395 buf.put((byte) 0);
399 * Check if an entry's content is different from the cache,
401 * File status information is used and status is same we
402 * consider the file identical to the state in the working
403 * directory. Native git uses more stat fields than we
404 * have accessible in Java.
406 * @param wd working directory to compare content with
407 * @return true if content is most likely different.
409 public boolean isModified(File wd) {
410 return isModified(wd, false);
414 * Check if an entry's content is different from the cache,
416 * File status information is used and status is same we
417 * consider the file identical to the state in the working
418 * directory. Native git uses more stat fields than we
419 * have accessible in Java.
421 * @param wd working directory to compare content with
422 * @param forceContentCheck True if the actual file content
423 * should be checked if modification time differs.
425 * @return true if content is most likely different.
427 public boolean isModified(File wd, boolean forceContentCheck) {
428 File file = getFile(wd);
429 if (!file.exists())
430 return true;
432 // JDK1.6 has file.canExecute
433 // if (file.canExecute() != FileMode.EXECUTABLE_FILE.equals(mode))
434 // return true;
435 final int exebits = FileMode.EXECUTABLE_FILE.getBits()
436 ^ FileMode.REGULAR_FILE.getBits();
438 if (config_filemode() && FileMode.EXECUTABLE_FILE.equals(mode)) {
439 if (!File_canExecute(file)&& canExecute != null)
440 return true;
441 } else {
442 if (FileMode.REGULAR_FILE.equals(mode&~exebits)) {
443 if (!file.isFile())
444 return true;
445 if (config_filemode() && File_canExecute(file) && canExecute != null)
446 return true;
447 } else {
448 if (FileMode.SYMLINK.equals(mode)) {
449 return true;
450 } else {
451 if (FileMode.TREE.equals(mode)) {
452 if (!file.isDirectory())
453 return true;
454 } else {
455 System.out.println("Does not handle mode "+mode+" ("+file+")");
456 return true;
462 if (file.length() != size)
463 return true;
465 // Git under windows only stores seconds so we round the timestmap
466 // Java gives us if it looks like the timestamp in index is seconds
467 // only. Otherwise we compare the timestamp at millisecond prevision.
468 long javamtime = mtime / 1000000L;
469 long lastm = file.lastModified();
470 if (javamtime % 1000 == 0)
471 lastm = lastm - lastm % 1000;
472 if (lastm != javamtime) {
473 if (!forceContentCheck)
474 return true;
476 try {
477 InputStream is = new FileInputStream(file);
478 ObjectWriter objectWriter = new ObjectWriter(db);
479 try {
480 ObjectId newId = objectWriter.computeBlobSha1(file
481 .length(), is);
482 boolean ret = !newId.equals(sha1);
483 return ret;
484 } catch (IOException e) {
485 e.printStackTrace();
486 } finally {
487 try {
488 is.close();
489 } catch (IOException e) {
490 // can't happen, but if it does we ignore it
491 e.printStackTrace();
494 } catch (FileNotFoundException e) {
495 // should not happen because we already checked this
496 e.printStackTrace();
497 throw new Error(e);
500 return false;
503 // for testing
504 void forceRecheck() {
505 mtime = -1;
508 private File getFile(File wd) {
509 return new File(wd, getName());
512 public String toString() {
513 return new String(name) + "/SHA-1(" + sha1 + ")/M:"
514 + new Date(ctime / 1000000L) + "/C:"
515 + new Date(mtime / 1000000L) + "/d" + dev + "/i" + ino
516 + "/m" + Integer.toString(mode, 8) + "/u" + uid + "/g"
517 + gid + "/s" + size + "/f" + flags + "/@" + stage;
520 public String getName() {
521 return new String(name);
524 public ObjectId getObjectId() {
525 return sha1;
528 public int getStage() {
529 return stage;
532 public int getSize() {
533 return size;
537 static class Header {
538 private int signature;
540 private int version;
542 int entries;
544 public Header(ByteBuffer map) throws CorruptObjectException {
545 read(map);
548 private void read(ByteBuffer buf) throws CorruptObjectException {
549 signature = buf.getInt();
550 version = buf.getInt();
551 entries = buf.getInt();
552 if (signature != 0x44495243)
553 throw new CorruptObjectException("Index signature is invalid: "
554 + signature);
555 if (version != 2)
556 throw new CorruptObjectException(
557 "Unknow index version (or corrupt index):" + version);
560 public void write(ByteBuffer buf) {
561 buf.order(ByteOrder.BIG_ENDIAN);
562 buf.putInt(signature);
563 buf.putInt(version);
564 buf.putInt(entries);
567 public Header(Map entryset) {
568 signature = 0x44495243;
569 version = 2;
570 entries = entryset.size();
574 public void readTree(Tree t) throws IOException {
575 readTree("", t);
578 public void readTree(String prefix, Tree t) throws IOException {
579 TreeEntry[] members = t.members();
580 for (int i = 0; i < members.length; ++i) {
581 TreeEntry te = members[i];
582 String name;
583 if (prefix.length() > 0)
584 name = prefix + "/" + te.getName();
585 else
586 name = te.getName();
587 if (te instanceof Tree) {
588 readTree(name, (Tree) te);
589 } else {
590 Entry e = new Entry(te, 0);
591 entries.put(name.getBytes("UTF-8"), e);
596 public Entry addEntry(TreeEntry te) throws IOException {
597 byte[] key = te.getFullName().getBytes("UTF-8");
598 Entry e = new Entry(te, 0);
599 entries.put(key, e);
600 return e;
603 public void checkout(File wd) throws IOException {
604 for (Iterator i = entries.values().iterator(); i.hasNext();) {
605 Entry e = (Entry) i.next();
606 if (e.stage != 0)
607 continue;
608 checkoutEntry(wd, e);
612 public void checkoutEntry(File wd, Entry e) throws IOException {
613 ObjectLoader ol = db.openBlob(e.sha1);
614 byte[] bytes = ol.getBytes();
615 File file = new File(wd, e.getName());
616 file.delete();
617 file.getParentFile().mkdirs();
618 FileChannel channel = new FileOutputStream(file).getChannel();
619 ByteBuffer buffer = ByteBuffer.wrap(bytes);
620 int j = channel.write(buffer);
621 if (j != bytes.length)
622 throw new IOException("Could not write file " + file);
623 channel.close();
624 if (config_filemode() && canExecute != null) {
625 if (FileMode.EXECUTABLE_FILE.equals(e.mode)) {
626 if (!File_canExecute(file))
627 File_setExecute(file, true);
628 } else {
629 if (File_canExecute(file))
630 File_setExecute(file, false);
633 e.mtime = file.lastModified() * 1000000L;
634 e.ctime = e.mtime;
637 public ObjectId writeTree() throws IOException {
638 checkWriteOk();
639 ObjectWriter writer = new ObjectWriter(db);
640 Tree current = new Tree(db);
641 Stack trees = new Stack();
642 trees.push(current);
643 String[] prevName = new String[0];
644 for (Iterator i = entries.values().iterator(); i.hasNext();) {
645 Entry e = (Entry) i.next();
646 if (e.stage != 0)
647 continue;
648 String[] newName = splitDirPath(e.getName());
649 int c = longestCommonPath(prevName, newName);
650 while (c < trees.size() - 1) {
651 current.setId(writer.writeTree(current));
652 trees.pop();
653 current = trees.isEmpty() ? null : (Tree) trees.peek();
655 while (trees.size() < newName.length) {
656 if (!current.existsTree(newName[trees.size() - 1])) {
657 current = new Tree(current, newName[trees.size() - 1]
658 .getBytes());
659 current.getParent().addEntry(current);
660 trees.push(current);
661 } else {
662 current = (Tree) current.findTreeMember(newName[trees
663 .size() - 1]);
664 trees.push(current);
667 FileTreeEntry ne = new FileTreeEntry(current, e.sha1,
668 newName[newName.length - 1].getBytes(),
669 (e.mode & FileMode.EXECUTABLE_FILE.getBits()) == FileMode.EXECUTABLE_FILE.getBits());
670 current.addEntry(ne);
672 while (!trees.isEmpty()) {
673 current.setId(writer.writeTree(current));
674 trees.pop();
675 if (!trees.isEmpty())
676 current = (Tree) trees.peek();
678 return current.getTreeId();
681 String[] splitDirPath(String name) {
682 String[] tmp = new String[name.length() / 2 + 1];
683 int p0 = -1;
684 int p1;
685 int c = 0;
686 while ((p1 = name.indexOf('/', p0 + 1)) != -1) {
687 tmp[c++] = name.substring(p0 + 1, p1);
688 p0 = p1;
690 tmp[c++] = name.substring(p0 + 1);
691 String[] ret = new String[c];
692 for (int i = 0; i < c; ++i) {
693 ret[i] = tmp[i];
695 return ret;
698 int longestCommonPath(String[] a, String[] b) {
699 int i;
700 for (i = 0; i < a.length && i < b.length; ++i)
701 if (!a[i].equals(b[i]))
702 return i;
703 return i;
707 * Return the members of the index sorted by the unsigned byte
708 * values of the path names.
710 * Small beware: Unaccounted for are unmerged entries. You may want
711 * to abort if members with stage != 0 are found if you are doing
712 * any updating operations. All stages will be found after one another
713 * here later. Currenly only one stage per name is returned.
715 * @return The index entries sorted
717 public Entry[] getMembers() {
718 return (Entry[]) entries.values().toArray(new Entry[entries.size()]);
721 public Entry getEntry(String path) throws UnsupportedEncodingException {
722 return (Entry) entries.get(Repository.gitInternalSlash(path.getBytes("ISO-8859-1")));
725 public Repository getRepository() {
726 return db;