Do not actually compare content when checking modification status
[egit.git] / org.spearce.jgit / src / org / spearce / jgit / lib / GitIndex.java
blob34d0a0e9e75a4c6b20c5a5370c25fe049d0688a3
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 long lm = f.lastModified() * 1000000L;
260 if (mtime != lm)
261 modified = true;
262 mtime = f.lastModified() * 1000000L;
263 if (size != f.length())
264 modified = true;
265 if (File_canExecute(f) != FileMode.EXECUTABLE_FILE.equals(mode)) {
266 mode = FileMode.EXECUTABLE_FILE.getBits();
267 modified = true;
269 if (modified) {
270 size = (int) f.length();
271 ObjectWriter writer = new ObjectWriter(db);
272 ObjectId newsha1 = sha1 = writer.writeBlob(f);
273 if (!newsha1.equals(sha1))
274 modified = true;
275 sha1 = newsha1;
277 return modified;
280 public void write(ByteBuffer buf) {
281 int startposition = buf.position();
282 buf.putInt((int) (ctime / 1000000000L));
283 buf.putInt((int) (ctime % 1000000000L));
284 buf.putInt((int) (mtime / 1000000000L));
285 buf.putInt((int) (mtime % 1000000000L));
286 buf.putInt(dev);
287 buf.putInt(ino);
288 buf.putInt(mode);
289 buf.putInt(uid);
290 buf.putInt(gid);
291 buf.putInt(size);
292 buf.put(sha1.getBytes());
293 buf.putShort(flags);
294 buf.put(name);
295 int end = startposition
296 + ((8 + 8 + 4 + 4 + 4 + 4 + 4 + 4 + 20 + 2 + name.length + 8) & ~7);
297 int remain = end - buf.position();
298 while (remain-- > 0)
299 buf.put((byte) 0);
302 static Method canExecute;
303 static {
304 try {
305 canExecute = File.class.getMethod("canExecute", (Class[]) null);
306 } catch (SecurityException e) {
307 // TODO Auto-generated catch block
308 e.printStackTrace();
309 } catch (NoSuchMethodException e) {
310 // TODO Auto-generated catch block
311 e.printStackTrace();
316 * JDK1.6 has file.canExecute
318 * if (file.canExecute() != FileMode.EXECUTABLE_FILE.equals(mode))
319 * return true;
321 boolean File_canExecute(File f) {
322 if (canExecute != null) {
323 try {
324 return ((Boolean) canExecute.invoke(f, (Object[]) null))
325 .booleanValue();
326 } catch (IllegalArgumentException e) {
327 // TODO Auto-generated catch block
328 e.printStackTrace();
329 return false;
330 } catch (IllegalAccessException e) {
331 // TODO Auto-generated catch block
332 e.printStackTrace();
333 return false;
334 } catch (InvocationTargetException e) {
335 // TODO Auto-generated catch block
336 e.printStackTrace();
337 return false;
339 } else
340 return false;
344 * Check if an entry's content is different from the cache,
346 * File status information is used and status is same we
347 * consider the file identical to the state in the working
348 * directory. Native git uses more stat fields than we
349 * have accessible in Java.
351 * @param wd working directory to compare content with
352 * @return true if content is most likely different.
354 public boolean isModified(File wd) {
355 return isModified(wd, false);
359 * Check if an entry's content is different from the cache,
361 * File status information is used and status is same we
362 * consider the file identical to the state in the working
363 * directory. Native git uses more stat fields than we
364 * have accessible in Java.
366 * @param wd working directory to compare content with
367 * @param forceContentCheck True if the actual file content
368 * should be checked if modification time differs.
370 * @return true if content is most likely different.
372 public boolean isModified(File wd, boolean forceContentCheck) {
373 File file = getFile(wd);
374 if (!file.exists())
375 return true;
377 // JDK1.6 has file.canExecute
378 // if (file.canExecute() != FileMode.EXECUTABLE_FILE.equals(mode))
379 // return true;
380 if (FileMode.EXECUTABLE_FILE.equals(mode)) {
381 if (!File_canExecute(file))
382 return true;
383 } else {
384 if (FileMode.REGULAR_FILE.equals(mode)) {
385 if (!file.isFile())
386 return true;
387 if (File_canExecute(file))
388 return true;
389 } else {
390 if (FileMode.SYMLINK.equals(mode)) {
391 return true;
392 } else {
393 if (FileMode.TREE.equals(mode)) {
394 if (!file.isDirectory())
395 return true;
396 } else {
397 System.out.println("Does not handle mode "+mode+" ("+file+")");
398 return true;
404 long javamtime = mtime / 1000000L;
405 long lastm = file.lastModified();
406 if (file.length() != size)
407 return true;
408 if (lastm != javamtime) {
409 if (!forceContentCheck)
410 return true;
412 try {
413 InputStream is = new FileInputStream(file);
414 ObjectWriter objectWriter = new ObjectWriter(theIndex.db);
415 try {
416 ObjectId newId = objectWriter.computeBlobSha1(file
417 .length(), is);
418 boolean ret = !newId.equals(sha1);
419 theIndex.statDirty = true;
420 return ret;
421 } catch (IOException e) {
422 e.printStackTrace();
423 } finally {
424 try {
425 is.close();
426 } catch (IOException e) {
427 // can't happen, but if it does we ignore it
428 e.printStackTrace();
431 } catch (FileNotFoundException e) {
432 // should not happen because we already checked this
433 e.printStackTrace();
434 throw new Error(e);
437 return false;
440 private File getFile(File wd) {
441 return new File(wd, getName());
444 public String toString() {
445 return new String(name) + "/SHA-1(" + sha1 + ")/M:"
446 + new Date(ctime / 1000000L) + "/C:"
447 + new Date(mtime / 1000000L) + "/d" + dev + "/i" + ino
448 + "/m" + Integer.toString(mode, 8) + "/u" + uid + "/g"
449 + gid + "/s" + size + "/f" + flags + "/@" + stage;
452 public String getName() {
453 return new String(name);
456 public ObjectId getObjectId() {
457 return sha1;
461 static class Header {
462 private int signature;
464 private int version;
466 int entries;
468 public Header(ByteBuffer map) throws CorruptObjectException {
469 read(map);
472 private void read(ByteBuffer buf) throws CorruptObjectException {
473 signature = buf.getInt();
474 version = buf.getInt();
475 entries = buf.getInt();
476 if (signature != 0x44495243)
477 throw new CorruptObjectException("Index signature is invalid: "
478 + signature);
479 if (version != 2)
480 throw new CorruptObjectException(
481 "Unknow index version (or corrupt index):" + version);
484 public void write(ByteBuffer buf) {
485 buf.order(ByteOrder.BIG_ENDIAN);
486 buf.putInt(signature);
487 buf.putInt(version);
488 buf.putInt(entries);
491 public Header(Map entryset) {
492 signature = 0x44495243;
493 version = 2;
494 entries = entryset.size();
498 public void readTree(Tree t) throws IOException {
499 readTree("", t);
502 public void readTree(String prefix, Tree t) throws IOException {
503 TreeEntry[] members = t.members();
504 for (int i = 0; i < members.length; ++i) {
505 TreeEntry te = members[i];
506 String name;
507 if (prefix.length() > 0)
508 name = prefix + "/" + te.getName();
509 else
510 name = te.getName();
511 if (te instanceof Tree) {
512 readTree(name, (Tree) te);
513 } else {
514 Entry e = new Entry(te, 0, this);
515 entries.put(name.getBytes("UTF-8"), e);
520 public void checkout(File wd) throws IOException {
521 for (Iterator i = entries.values().iterator(); i.hasNext();) {
522 Entry e = (Entry) i.next();
523 if (e.stage != 0)
524 continue;
525 ObjectLoader ol = db.openBlob(e.sha1);
526 byte[] bytes = ol.getBytes();
527 File file = new File(wd, e.getName());
528 file.delete();
529 file.getParentFile().mkdirs();
530 FileChannel channel = new FileOutputStream(file).getChannel();
531 ByteBuffer buffer = ByteBuffer.wrap(bytes);
532 int j = channel.write(buffer);
533 if (j != bytes.length)
534 throw new IOException("Could not write file " + file);
535 channel.close();
539 public ObjectId writeTree() throws IOException {
540 ObjectWriter writer = new ObjectWriter(db);
541 Tree current = new Tree(db);
542 Stack trees = new Stack();
543 trees.push(current);
544 String[] prevName = new String[0];
545 for (Iterator i = entries.values().iterator(); i.hasNext();) {
546 Entry e = (Entry) i.next();
547 if (e.stage != 0)
548 continue;
549 String[] newName = splitDirPath(e.getName());
550 int c = longestCommonPath(prevName, newName);
551 while (c < trees.size() - 1) {
552 current.setId(writer.writeTree(current));
553 trees.pop();
554 current = trees.isEmpty() ? null : (Tree) trees.peek();
556 while (trees.size() < newName.length) {
557 if (!current.existsTree(newName[trees.size() - 1])) {
558 current = new Tree(current, newName[trees.size() - 1]
559 .getBytes());
560 current.getParent().addEntry(current);
561 trees.push(current);
562 } else {
563 current = (Tree) current.findTreeMember(newName[trees
564 .size() - 1]);
565 trees.push(current);
568 FileTreeEntry ne = new FileTreeEntry(current, e.sha1,
569 newName[newName.length - 1].getBytes(),
570 (e.mode & FileMode.EXECUTABLE_FILE.getBits()) == FileMode.EXECUTABLE_FILE.getBits());
571 current.addEntry(ne);
573 while (!trees.isEmpty()) {
574 current.setId(writer.writeTree(current));
575 trees.pop();
576 if (!trees.isEmpty())
577 current = (Tree) trees.peek();
579 return current.getTreeId();
582 String[] splitDirPath(String name) {
583 String[] tmp = new String[name.length() / 2 + 1];
584 int p0 = -1;
585 int p1;
586 int c = 0;
587 while ((p1 = name.indexOf('/', p0 + 1)) != -1) {
588 tmp[c++] = name.substring(p0 + 1, p1);
589 p0 = p1;
591 tmp[c++] = name.substring(p0 + 1);
592 String[] ret = new String[c];
593 for (int i = 0; i < c; ++i) {
594 ret[i] = tmp[i];
596 return ret;
599 int longestCommonPath(String[] a, String[] b) {
600 int i;
601 for (i = 0; i < a.length && i < b.length; ++i)
602 if (!a[i].equals(b[i]))
603 return i;
604 return i;
607 public Entry[] getMembers() {
608 return (Entry[]) entries.values().toArray(new Entry[entries.size()]);
611 public Entry getEntry(String path) throws UnsupportedEncodingException {
612 return (Entry) entries.get(path.getBytes("ISO-8859-1"));