Remember last time of index file access so we do not reread ot all the time
[egit.git] / org.spearce.jgit / src / org / spearce / jgit / lib / GitIndex.java
blobe22dd25a48c2f458a1ce847f2d68e06bd27f8c20
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.MappedByteBuffer;
16 import java.nio.channels.FileChannel;
17 import java.nio.channels.FileChannel.MapMode;
18 import java.security.MessageDigest;
19 import java.util.Comparator;
20 import java.util.Date;
21 import java.util.Iterator;
22 import java.util.Map;
23 import java.util.Stack;
24 import java.util.TreeMap;
26 import org.spearce.jgit.errors.CorruptObjectException;
28 public class GitIndex {
30 private RandomAccessFile cache;
32 private File cacheFile;
34 // Index is modified
35 private boolean changed;
37 // Stat information updated
38 private boolean statDirty;
40 private Header header;
42 private long lastCacheTime;
44 private final Repository db;
46 private Map entries = new TreeMap(new Comparator() {
47 public int compare(Object arg0, Object arg1) {
48 byte[] a = (byte[]) arg0;
49 byte[] b = (byte[]) arg1;
50 for (int i = 0; i < a.length && i < b.length; ++i) {
51 int c = a[i] - b[i];
52 if (c != 0)
53 return c;
55 if (a.length < b.length)
56 return -1;
57 else if (a.length > b.length)
58 return 1;
59 return 0;
61 });
63 public GitIndex(Repository db) {
64 this.db = db;
65 this.cacheFile = new File(db.getDirectory(), "index");
68 public boolean isChanged() {
69 return changed || statDirty;
72 public void rereadIfNecessary() throws IOException {
73 if (cacheFile.exists() && cacheFile.lastModified() != lastCacheTime) {
74 read();
78 public void add(File wd, File f) throws IOException {
79 byte[] key = Entry.makeKey(wd, f);
80 Entry e = (Entry) entries.get(key);
81 if (e == null) {
82 e = new Entry(key, f, 0, this);
83 entries.put(key, e);
84 } else {
85 e.update(f, db);
89 public void remove(File wd, File f) {
90 byte[] key = Entry.makeKey(wd, f);
91 entries.remove(key);
94 public void read() throws IOException {
95 long t0 = System.currentTimeMillis();
96 changed = false;
97 statDirty = false;
98 cache = new RandomAccessFile(cacheFile, "r");
99 try {
100 MappedByteBuffer map = cache.getChannel().map(MapMode.READ_ONLY, 0,
101 cacheFile.length());
102 map.order(ByteOrder.BIG_ENDIAN);
103 header = new Header(map);
104 entries.clear();
105 for (int i = 0; i < header.entries; ++i) {
106 Entry entry = new Entry(this, map);
107 entries.put(entry.name, entry);
109 long t1 = System.currentTimeMillis();
110 lastCacheTime = cacheFile.lastModified();
111 System.out.println("Read index "+cacheFile+" in "+((t1-t0)/1000.0)+"s");
112 } finally {
113 cache.close();
117 public void write() throws IOException {
118 File tmpIndex = new File(cacheFile.getAbsoluteFile() + ".tmp");
119 File lock = new File(cacheFile.getAbsoluteFile() + ".lock");
120 if (!lock.createNewFile())
121 throw new IOException("Index file is in use");
122 try {
123 FileOutputStream fileOutputStream = new FileOutputStream(tmpIndex);
124 FileChannel fc = fileOutputStream.getChannel();
125 ByteBuffer buf = ByteBuffer.allocate(4096);
126 MessageDigest newMessageDigest = Constants.newMessageDigest();
127 header = new Header(entries);
128 header.write(buf);
129 buf.flip();
130 newMessageDigest
131 .update(buf.array(), buf.arrayOffset(), buf.limit());
132 fc.write(buf);
133 buf.flip();
134 buf.clear();
135 for (Iterator i = entries.values().iterator(); i.hasNext();) {
136 Entry e = (Entry) i.next();
137 e.write(buf);
138 buf.flip();
139 newMessageDigest.update(buf.array(), buf.arrayOffset(), buf
140 .limit());
141 fc.write(buf);
142 buf.flip();
143 buf.clear();
145 buf.put(newMessageDigest.digest());
146 buf.flip();
147 fc.write(buf);
148 fc.close();
149 fileOutputStream.close();
150 if (!tmpIndex.renameTo(cacheFile))
151 throw new IOException(
152 "Could not rename temporary index file to index");
153 changed = false;
154 statDirty = false;
155 } finally {
156 if (!lock.delete())
157 throw new IOException(
158 "Could not delete lock file. Should not happen");
159 if (tmpIndex.exists() && !tmpIndex.delete())
160 throw new IOException(
161 "Could not delete temporary index file. Should not happen");
165 public static class Entry {
166 private long ctime;
168 private long mtime;
170 private int dev;
172 private int ino;
174 private int mode;
176 private int uid;
178 private int gid;
180 private int size;
182 private ObjectId sha1;
184 private short flags;
186 private byte[] name;
188 private int stage;
190 private GitIndex theIndex;
192 static byte[] makeKey(File wd, File f) {
193 if (!f.getPath().startsWith(wd.getPath()))
194 throw new Error("Path is not in working dir");
195 String relName = f.getPath().substring(wd.getPath().length() + 1)
196 .replace(File.separatorChar, '/');
197 return relName.getBytes();
200 public Entry(byte[] key, File f, int stage, GitIndex index)
201 throws IOException {
202 theIndex = index;
203 ctime = f.lastModified() * 1000000L;
204 mtime = ctime; // we use same here
205 dev = -1;
206 ino = -1;
207 mode = FileMode.REGULAR_FILE.getBits();
208 uid = -1;
209 gid = -1;
210 size = (int) f.length();
211 ObjectWriter writer = new ObjectWriter(theIndex.db);
212 sha1 = writer.writeBlob(f);
213 name = key;
214 flags = (short) ((stage << 12) | name.length); // TODO: fix flags
217 public Entry(TreeEntry f, int stage, GitIndex index)
218 throws UnsupportedEncodingException {
219 theIndex = index;
220 ctime = -1; // hmm
221 mtime = -1;
222 dev = -1;
223 ino = -1;
224 mode = f.getMode().getBits();
225 uid = -1;
226 gid = -1;
227 size = -1;
228 sha1 = f.getId();
229 name = f.getFullName().getBytes("UTF-8");
230 flags = (short) ((stage << 12) | name.length); // TODO: fix flags
233 Entry(GitIndex index, ByteBuffer b) {
234 theIndex = index;
235 int startposition = b.position();
236 ctime = b.getInt() * 1000000000L + (b.getInt() % 1000000000L);
237 mtime = b.getInt() * 1000000000L + (b.getInt() % 1000000000L);
238 dev = b.getInt();
239 ino = b.getInt();
240 mode = b.getInt();
241 uid = b.getInt();
242 gid = b.getInt();
243 size = b.getInt();
244 byte[] sha1bytes = new byte[Constants.OBJECT_ID_LENGTH];
245 b.get(sha1bytes);
246 sha1 = new ObjectId(sha1bytes);
247 flags = b.getShort();
248 stage = (flags & 0x4000) >> 12;
249 name = new byte[flags & 0xFFF];
250 b.get(name);
252 .position(startposition
253 + ((8 + 8 + 4 + 4 + 4 + 4 + 4 + 4 + 20 + 2
254 + name.length + 8) & ~7));
257 public boolean update(File f, Repository db) throws IOException {
258 boolean modified = false;
259 if (mtime != f.lastModified())
260 modified = true;
261 mtime = f.lastModified() * 1000000L;
262 if (size != f.length())
263 modified = true;
264 size = (int) f.length();
265 ObjectWriter writer = new ObjectWriter(db);
266 ObjectId newsha1 = sha1 = writer.writeBlob(f);
267 if (!newsha1.equals(sha1))
268 modified = true;
269 sha1 = newsha1;
270 return modified;
273 public void write(ByteBuffer buf) {
274 int startposition = buf.position();
275 buf.putInt((int) (ctime / 1000000000L));
276 buf.putInt((int) (ctime % 1000000000L));
277 buf.putInt((int) (mtime / 1000000000L));
278 buf.putInt((int) (mtime % 1000000000L));
279 buf.putInt(dev);
280 buf.putInt(ino);
281 buf.putInt(mode);
282 buf.putInt(uid);
283 buf.putInt(gid);
284 buf.putInt(size);
285 buf.put(sha1.getBytes());
286 buf.putShort(flags);
287 buf.put(name);
288 int end = startposition
289 + ((8 + 8 + 4 + 4 + 4 + 4 + 4 + 4 + 20 + 2 + name.length + 8) & ~7);
290 int remain = end - buf.position();
291 while (remain-- > 0)
292 buf.put((byte) 0);
295 static Method canExecute;
296 static {
297 try {
298 canExecute = File.class.getMethod("canExecute", (Class[]) null);
299 } catch (SecurityException e) {
300 // TODO Auto-generated catch block
301 e.printStackTrace();
302 } catch (NoSuchMethodException e) {
303 // TODO Auto-generated catch block
304 e.printStackTrace();
309 * JDK1.6 has file.canExecute
311 * if (file.canExecute() != FileMode.EXECUTABLE_FILE.equals(mode))
312 * return true;
314 boolean File_canExecute(File f) {
315 if (canExecute != null) {
316 try {
317 return ((Boolean) canExecute.invoke(f, (Object[]) null))
318 .booleanValue();
319 } catch (IllegalArgumentException e) {
320 // TODO Auto-generated catch block
321 e.printStackTrace();
322 return false;
323 } catch (IllegalAccessException e) {
324 // TODO Auto-generated catch block
325 e.printStackTrace();
326 return false;
327 } catch (InvocationTargetException e) {
328 // TODO Auto-generated catch block
329 e.printStackTrace();
330 return false;
332 } else
333 return false;
336 public boolean isModified(File wd) {
337 File file = getFile(wd);
338 if (!file.exists())
339 return true;
341 // JDK1.6 has file.canExecute
342 // if (file.canExecute() != FileMode.EXECUTABLE_FILE.equals(mode))
343 // return true;
344 if (FileMode.EXECUTABLE_FILE.equals(mode)) {
345 if (!File_canExecute(file))
346 return true;
347 } else {
348 if (FileMode.REGULAR_FILE.equals(mode)) {
349 if (!file.isFile())
350 return true;
351 if (File_canExecute(file))
352 return true;
353 } else {
354 if (FileMode.SYMLINK.equals(mode)) {
355 return true;
356 } else {
357 if (FileMode.TREE.equals(mode)) {
358 if (!file.isDirectory())
359 return true;
360 } else {
361 System.out.println("Does not handle mode "+mode+" ("+file+")");
362 return true;
368 long javamtime = mtime / 1000000L;
369 long lastm = file.lastModified();
370 if (file.length() != size)
371 return true;
372 if (lastm != javamtime) {
373 try {
374 InputStream is = new FileInputStream(file);
375 ObjectWriter objectWriter = new ObjectWriter(theIndex.db);
376 try {
377 ObjectId newId = objectWriter.computeBlobSha1(file
378 .length(), is);
379 boolean ret = !newId.equals(sha1);
380 theIndex.statDirty = true;
381 return ret;
382 } catch (IOException e) {
383 e.printStackTrace();
384 } finally {
385 try {
386 is.close();
387 } catch (IOException e) {
388 // can't happen, but if it does we ignore it
389 e.printStackTrace();
392 } catch (FileNotFoundException e) {
393 // should not happen because we already checked this
394 e.printStackTrace();
395 throw new Error(e);
398 return false;
401 private File getFile(File wd) {
402 return new File(wd, getName());
405 public String toString() {
406 return new String(name) + "/SHA-1(" + sha1 + ")/M:"
407 + new Date(ctime / 1000000L) + "/C:"
408 + new Date(mtime / 1000000L) + "/d" + dev + "/i" + ino
409 + "/m" + Integer.toString(mode, 8) + "/u" + uid + "/g"
410 + gid + "/s" + size + "/f" + flags + "/@" + stage;
413 public String getName() {
414 return new String(name);
417 public ObjectId getObjectId() {
418 return sha1;
422 static class Header {
423 private int signature;
425 private int version;
427 int entries;
429 public Header(ByteBuffer map) throws CorruptObjectException {
430 read(map);
433 private void read(ByteBuffer buf) throws CorruptObjectException {
434 signature = buf.getInt();
435 version = buf.getInt();
436 entries = buf.getInt();
437 if (signature != 0x44495243)
438 throw new CorruptObjectException("Index signature is invalid: "
439 + signature);
440 if (version != 2)
441 throw new CorruptObjectException(
442 "Unknow index version (or corrupt index):" + version);
445 public void write(ByteBuffer buf) {
446 buf.order(ByteOrder.BIG_ENDIAN);
447 buf.putInt(signature);
448 buf.putInt(version);
449 buf.putInt(entries);
452 public Header(Map entryset) {
453 signature = 0x44495243;
454 version = 2;
455 entries = entryset.size();
459 public void readTree(Tree t) throws IOException {
460 readTree("", t);
463 public void readTree(String prefix, Tree t) throws IOException {
464 TreeEntry[] members = t.members();
465 for (int i = 0; i < members.length; ++i) {
466 TreeEntry te = members[i];
467 String name;
468 if (prefix.length() > 0)
469 name = prefix + "/" + te.getName();
470 else
471 name = te.getName();
472 if (te instanceof Tree) {
473 readTree(name, (Tree) te);
474 } else {
475 Entry e = new Entry(te, 0, this);
476 entries.put(name.getBytes("UTF-8"), e);
481 public void checkout(File wd) throws IOException {
482 for (Iterator i = entries.values().iterator(); i.hasNext();) {
483 Entry e = (Entry) i.next();
484 if (e.stage != 0)
485 continue;
486 ObjectLoader ol = db.openBlob(e.sha1);
487 byte[] bytes = ol.getBytes();
488 File file = new File(wd, e.getName());
489 file.delete();
490 file.getParentFile().mkdirs();
491 FileChannel channel = new FileOutputStream(file).getChannel();
492 ByteBuffer buffer = ByteBuffer.wrap(bytes);
493 int j = channel.write(buffer);
494 if (j != bytes.length)
495 throw new IOException("Could not write file " + file);
496 channel.close();
500 public ObjectId writeTree(File wd) throws IOException {
501 ObjectWriter writer = new ObjectWriter(db);
502 Tree current = new Tree(db);
503 Stack trees = new Stack();
504 trees.push(current);
505 String[] prevName = new String[0];
506 for (Iterator i = entries.values().iterator(); i.hasNext();) {
507 Entry e = (Entry) i.next();
508 if (e.stage != 0)
509 continue;
510 String[] newName = splitDirPath(e.getName());
511 int c = longestCommonPath(prevName, newName);
512 while (c < trees.size() - 1) {
513 current.setId(writer.writeTree(current));
514 trees.pop();
515 current = trees.isEmpty() ? null : (Tree) trees.peek();
517 while (trees.size() < newName.length) {
518 if (!current.existsTree(newName[trees.size() - 1])) {
519 current = new Tree(current, newName[trees.size() - 1]
520 .getBytes());
521 current.getParent().addEntry(current);
522 trees.push(current);
523 } else {
524 current = (Tree) current.findTreeMember(newName[trees
525 .size() - 1]);
526 trees.push(current);
529 FileTreeEntry ne = new FileTreeEntry(current, e.sha1,
530 newName[newName.length - 1].getBytes(),
531 (e.mode & FileMode.EXECUTABLE_FILE.getBits()) == FileMode.EXECUTABLE_FILE.getBits());
532 current.addEntry(ne);
534 while (!trees.isEmpty()) {
535 current.setId(writer.writeTree(current));
536 trees.pop();
537 if (!trees.isEmpty())
538 current = (Tree) trees.peek();
540 return current.getTreeId();
543 String[] splitDirPath(String name) {
544 String[] tmp = new String[name.length() / 2 + 1];
545 int p0 = -1;
546 int p1;
547 int c = 0;
548 while ((p1 = name.indexOf('/', p0 + 1)) != -1) {
549 tmp[c++] = name.substring(p0 + 1, p1);
550 p0 = p1;
552 tmp[c++] = name.substring(p0 + 1);
553 String[] ret = new String[c];
554 for (int i = 0; i < c; ++i) {
555 ret[i] = tmp[i];
557 return ret;
560 int longestCommonPath(String[] a, String[] b) {
561 int i;
562 for (i = 0; i < a.length && i < b.length; ++i)
563 if (!a[i].equals(b[i]))
564 return i;
565 return i;
568 public Entry[] getMembers() {
569 return (Entry[]) entries.values().toArray(new Entry[entries.size()]);
572 public Entry getEntry(String path) throws UnsupportedEncodingException {
573 return (Entry) entries.get(path.getBytes("ISO-8859-1"));