Make attempt to refresh when merge conflicts exists more user friendly
[egit.git] / org.spearce.jgit / src / org / spearce / jgit / lib / GitIndex.java
blobd2e78f9d3a71a393e7c2b76d33ce0f03e9409b40
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 void 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);
91 public boolean remove(File wd, File f) {
92 byte[] key = makeKey(wd, f);
93 return entries.remove(key) != null;
96 public void read() throws IOException {
97 long t0 = System.currentTimeMillis();
98 changed = false;
99 statDirty = false;
100 cache = new RandomAccessFile(cacheFile, "r");
101 try {
102 FileChannel channel = cache.getChannel();
103 ByteBuffer buffer = ByteBuffer.allocateDirect((int) cacheFile.length());
104 buffer.order(ByteOrder.BIG_ENDIAN);
105 int j = channel.read(buffer);
106 if (j != buffer.capacity())
107 throw new IOException("Could not read index in one go, only "+j+" out of "+buffer.capacity()+" read");
108 buffer.flip();
109 header = new Header(buffer);
110 entries.clear();
111 for (int i = 0; i < header.entries; ++i) {
112 Entry entry = new Entry(buffer);
113 entries.put(entry.name, entry);
115 long t1 = System.currentTimeMillis();
116 lastCacheTime = cacheFile.lastModified();
117 System.out.println("Read index "+cacheFile+" in "+((t1-t0)/1000.0)+"s");
118 } finally {
119 cache.close();
123 public void write() throws IOException {
124 checkWriteOk();
125 File tmpIndex = new File(cacheFile.getAbsoluteFile() + ".tmp");
126 File lock = new File(cacheFile.getAbsoluteFile() + ".lock");
127 if (!lock.createNewFile())
128 throw new IOException("Index file is in use");
129 try {
130 FileOutputStream fileOutputStream = new FileOutputStream(tmpIndex);
131 FileChannel fc = fileOutputStream.getChannel();
132 ByteBuffer buf = ByteBuffer.allocate(4096);
133 MessageDigest newMessageDigest = Constants.newMessageDigest();
134 header = new Header(entries);
135 header.write(buf);
136 buf.flip();
137 newMessageDigest
138 .update(buf.array(), buf.arrayOffset(), buf.limit());
139 fc.write(buf);
140 buf.flip();
141 buf.clear();
142 for (Iterator i = entries.values().iterator(); i.hasNext();) {
143 Entry e = (Entry) i.next();
144 e.write(buf);
145 buf.flip();
146 newMessageDigest.update(buf.array(), buf.arrayOffset(), buf
147 .limit());
148 fc.write(buf);
149 buf.flip();
150 buf.clear();
152 buf.put(newMessageDigest.digest());
153 buf.flip();
154 fc.write(buf);
155 fc.close();
156 fileOutputStream.close();
157 if (cacheFile.exists())
158 if (!cacheFile.delete())
159 throw new IOException(
160 "Could not rename delete old index");
161 if (!tmpIndex.renameTo(cacheFile))
162 throw new IOException(
163 "Could not rename temporary index file to index");
164 changed = false;
165 statDirty = false;
166 } finally {
167 if (!lock.delete())
168 throw new IOException(
169 "Could not delete lock file. Should not happen");
170 if (tmpIndex.exists() && !tmpIndex.delete())
171 throw new IOException(
172 "Could not delete temporary index file. Should not happen");
176 private void checkWriteOk() throws IOException {
177 for (Iterator i = entries.values().iterator(); i.hasNext();) {
178 Entry e = (Entry) i.next();
179 if (e.stage != 0) {
180 throw new NotSupportedException("Cannot work with other stages than zero right now. Won't write corrupt index.");
185 static Method canExecute;
186 static Method setExecute;
187 static {
188 try {
189 canExecute = File.class.getMethod("canExecute", (Class[]) null);
190 setExecute = File.class.getMethod("setExecutable", new Class[] { Boolean.TYPE });
191 } catch (SecurityException e) {
192 e.printStackTrace();
193 } catch (NoSuchMethodException e) {
194 System.out.println("This vm cannot handle execute file permission. Feature disabled");
199 * JDK1.6 has file.canExecute
201 * if (file.canExecute() != FileMode.EXECUTABLE_FILE.equals(mode))
202 * return true;
204 boolean File_canExecute(File f) {
205 if (canExecute != null) {
206 try {
207 return ((Boolean) canExecute.invoke(f, (Object[]) null))
208 .booleanValue();
209 } catch (IllegalArgumentException e) {
210 throw new Error(e);
211 } catch (IllegalAccessException e) {
212 throw new Error(e);
213 } catch (InvocationTargetException e) {
214 throw new Error(e);
216 } else
217 return false;
221 * JDK1.6 has file.setExecute
223 boolean File_setExecute(File f,boolean value) {
224 if (setExecute != null) {
225 try {
226 return ((Boolean) setExecute.invoke(f,
227 new Object[] { new Boolean(value) })).booleanValue();
228 } catch (IllegalArgumentException e) {
229 throw new Error(e);
230 } catch (IllegalAccessException e) {
231 throw new Error(e);
232 } catch (InvocationTargetException e) {
233 throw new Error(e);
235 } else
236 return false;
239 static byte[] makeKey(File wd, File f) {
240 if (!f.getPath().startsWith(wd.getPath()))
241 throw new Error("Path is not in working dir");
242 String relName = f.getPath().substring(wd.getPath().length() + 1)
243 .replace(File.separatorChar, '/');
244 return relName.getBytes();
247 Boolean filemode;
248 private boolean config_filemode() {
249 // temporary til we can actually set parameters. We need to be able
250 // to change this for testing.
251 if (filemode != null)
252 return filemode.booleanValue();
253 RepositoryConfig config = db.getConfig();
254 return config.getBoolean("core", "filemode", true);
257 public class Entry {
258 private long ctime;
260 private long mtime;
262 private int dev;
264 private int ino;
266 private int mode;
268 private int uid;
270 private int gid;
272 private int size;
274 private ObjectId sha1;
276 private short flags;
278 private byte[] name;
280 private int stage;
282 public Entry(byte[] key, File f, int stage)
283 throws IOException {
284 ctime = f.lastModified() * 1000000L;
285 mtime = ctime; // we use same here
286 dev = -1;
287 ino = -1;
288 if (config_filemode() && File_canExecute(f))
289 mode = FileMode.EXECUTABLE_FILE.getBits();
290 else
291 mode = FileMode.REGULAR_FILE.getBits();
292 uid = -1;
293 gid = -1;
294 size = (int) f.length();
295 ObjectWriter writer = new ObjectWriter(db);
296 sha1 = writer.writeBlob(f);
297 name = key;
298 flags = (short) ((stage << 12) | name.length); // TODO: fix flags
301 public Entry(TreeEntry f, int stage)
302 throws UnsupportedEncodingException {
303 ctime = -1; // hmm
304 mtime = -1;
305 dev = -1;
306 ino = -1;
307 mode = f.getMode().getBits();
308 uid = -1;
309 gid = -1;
310 size = -1;
311 sha1 = f.getId();
312 name = f.getFullName().getBytes("UTF-8");
313 flags = (short) ((stage << 12) | name.length); // TODO: fix flags
316 Entry(ByteBuffer b) {
317 int startposition = b.position();
318 ctime = b.getInt() * 1000000000L + (b.getInt() % 1000000000L);
319 mtime = b.getInt() * 1000000000L + (b.getInt() % 1000000000L);
320 dev = b.getInt();
321 ino = b.getInt();
322 mode = b.getInt();
323 uid = b.getInt();
324 gid = b.getInt();
325 size = b.getInt();
326 byte[] sha1bytes = new byte[Constants.OBJECT_ID_LENGTH];
327 b.get(sha1bytes);
328 sha1 = new ObjectId(sha1bytes);
329 flags = b.getShort();
330 stage = (flags & 0x3000) >> 12;
331 name = new byte[flags & 0xFFF];
332 b.get(name);
334 .position(startposition
335 + ((8 + 8 + 4 + 4 + 4 + 4 + 4 + 4 + 20 + 2
336 + name.length + 8) & ~7));
339 public boolean update(File f) throws IOException {
340 boolean modified = false;
341 long lm = f.lastModified() * 1000000L;
342 if (mtime != lm)
343 modified = true;
344 mtime = f.lastModified() * 1000000L;
345 if (size != f.length())
346 modified = true;
347 if (config_filemode()) {
348 if (File_canExecute(f) != FileMode.EXECUTABLE_FILE.equals(mode)) {
349 mode = FileMode.EXECUTABLE_FILE.getBits();
350 modified = true;
353 if (modified) {
354 size = (int) f.length();
355 ObjectWriter writer = new ObjectWriter(db);
356 ObjectId newsha1 = sha1 = writer.writeBlob(f);
357 if (!newsha1.equals(sha1))
358 modified = true;
359 sha1 = newsha1;
361 return modified;
364 public void write(ByteBuffer buf) {
365 int startposition = buf.position();
366 buf.putInt((int) (ctime / 1000000000L));
367 buf.putInt((int) (ctime % 1000000000L));
368 buf.putInt((int) (mtime / 1000000000L));
369 buf.putInt((int) (mtime % 1000000000L));
370 buf.putInt(dev);
371 buf.putInt(ino);
372 buf.putInt(mode);
373 buf.putInt(uid);
374 buf.putInt(gid);
375 buf.putInt(size);
376 buf.put(sha1.getBytes());
377 buf.putShort(flags);
378 buf.put(name);
379 int end = startposition
380 + ((8 + 8 + 4 + 4 + 4 + 4 + 4 + 4 + 20 + 2 + name.length + 8) & ~7);
381 int remain = end - buf.position();
382 while (remain-- > 0)
383 buf.put((byte) 0);
387 * Check if an entry's content is different from the cache,
389 * File status information is used and status is same we
390 * consider the file identical to the state in the working
391 * directory. Native git uses more stat fields than we
392 * have accessible in Java.
394 * @param wd working directory to compare content with
395 * @return true if content is most likely different.
397 public boolean isModified(File wd) {
398 return isModified(wd, false);
402 * Check if an entry's content is different from the cache,
404 * File status information is used and status is same we
405 * consider the file identical to the state in the working
406 * directory. Native git uses more stat fields than we
407 * have accessible in Java.
409 * @param wd working directory to compare content with
410 * @param forceContentCheck True if the actual file content
411 * should be checked if modification time differs.
413 * @return true if content is most likely different.
415 public boolean isModified(File wd, boolean forceContentCheck) {
416 File file = getFile(wd);
417 if (!file.exists())
418 return true;
420 // JDK1.6 has file.canExecute
421 // if (file.canExecute() != FileMode.EXECUTABLE_FILE.equals(mode))
422 // return true;
423 final int exebits = FileMode.EXECUTABLE_FILE.getBits()
424 ^ FileMode.REGULAR_FILE.getBits();
426 if (config_filemode() && FileMode.EXECUTABLE_FILE.equals(mode)) {
427 if (!File_canExecute(file)&& canExecute != null)
428 return true;
429 } else {
430 if (FileMode.REGULAR_FILE.equals(mode&~exebits)) {
431 if (!file.isFile())
432 return true;
433 if (config_filemode() && File_canExecute(file) && canExecute != null)
434 return true;
435 } else {
436 if (FileMode.SYMLINK.equals(mode)) {
437 return true;
438 } else {
439 if (FileMode.TREE.equals(mode)) {
440 if (!file.isDirectory())
441 return true;
442 } else {
443 System.out.println("Does not handle mode "+mode+" ("+file+")");
444 return true;
450 if (file.length() != size)
451 return true;
453 // Git under windows only stores seconds so we round the timestmap
454 // Java gives us if it looks like the timestamp in index is seconds
455 // only. Otherwise we compare the timestamp at millisecond prevision.
456 long javamtime = mtime / 1000000L;
457 long lastm = file.lastModified();
458 if (javamtime % 1000 == 0)
459 lastm = lastm - lastm % 1000;
460 if (lastm != javamtime) {
461 if (!forceContentCheck)
462 return true;
464 try {
465 InputStream is = new FileInputStream(file);
466 ObjectWriter objectWriter = new ObjectWriter(db);
467 try {
468 ObjectId newId = objectWriter.computeBlobSha1(file
469 .length(), is);
470 boolean ret = !newId.equals(sha1);
471 return ret;
472 } catch (IOException e) {
473 e.printStackTrace();
474 } finally {
475 try {
476 is.close();
477 } catch (IOException e) {
478 // can't happen, but if it does we ignore it
479 e.printStackTrace();
482 } catch (FileNotFoundException e) {
483 // should not happen because we already checked this
484 e.printStackTrace();
485 throw new Error(e);
488 return false;
491 private File getFile(File wd) {
492 return new File(wd, getName());
495 public String toString() {
496 return new String(name) + "/SHA-1(" + sha1 + ")/M:"
497 + new Date(ctime / 1000000L) + "/C:"
498 + new Date(mtime / 1000000L) + "/d" + dev + "/i" + ino
499 + "/m" + Integer.toString(mode, 8) + "/u" + uid + "/g"
500 + gid + "/s" + size + "/f" + flags + "/@" + stage;
503 public String getName() {
504 return new String(name);
507 public ObjectId getObjectId() {
508 return sha1;
511 public int getStage() {
512 return stage;
516 static class Header {
517 private int signature;
519 private int version;
521 int entries;
523 public Header(ByteBuffer map) throws CorruptObjectException {
524 read(map);
527 private void read(ByteBuffer buf) throws CorruptObjectException {
528 signature = buf.getInt();
529 version = buf.getInt();
530 entries = buf.getInt();
531 if (signature != 0x44495243)
532 throw new CorruptObjectException("Index signature is invalid: "
533 + signature);
534 if (version != 2)
535 throw new CorruptObjectException(
536 "Unknow index version (or corrupt index):" + version);
539 public void write(ByteBuffer buf) {
540 buf.order(ByteOrder.BIG_ENDIAN);
541 buf.putInt(signature);
542 buf.putInt(version);
543 buf.putInt(entries);
546 public Header(Map entryset) {
547 signature = 0x44495243;
548 version = 2;
549 entries = entryset.size();
553 public void readTree(Tree t) throws IOException {
554 readTree("", t);
557 public void readTree(String prefix, Tree t) throws IOException {
558 TreeEntry[] members = t.members();
559 for (int i = 0; i < members.length; ++i) {
560 TreeEntry te = members[i];
561 String name;
562 if (prefix.length() > 0)
563 name = prefix + "/" + te.getName();
564 else
565 name = te.getName();
566 if (te instanceof Tree) {
567 readTree(name, (Tree) te);
568 } else {
569 Entry e = new Entry(te, 0);
570 entries.put(name.getBytes("UTF-8"), e);
575 public void checkout(File wd) throws IOException {
576 for (Iterator i = entries.values().iterator(); i.hasNext();) {
577 Entry e = (Entry) i.next();
578 if (e.stage != 0)
579 continue;
580 ObjectLoader ol = db.openBlob(e.sha1);
581 byte[] bytes = ol.getBytes();
582 File file = new File(wd, e.getName());
583 file.delete();
584 file.getParentFile().mkdirs();
585 FileChannel channel = new FileOutputStream(file).getChannel();
586 ByteBuffer buffer = ByteBuffer.wrap(bytes);
587 int j = channel.write(buffer);
588 if (j != bytes.length)
589 throw new IOException("Could not write file " + file);
590 channel.close();
591 if (config_filemode() && canExecute != null) {
592 if (FileMode.EXECUTABLE_FILE.equals(e.mode)) {
593 if (!File_canExecute(file))
594 File_setExecute(file, true);
595 } else {
596 if (File_canExecute(file))
597 File_setExecute(file, false);
600 e.mtime = file.lastModified() * 1000000L;
601 e.ctime = e.mtime;
605 public ObjectId writeTree() throws IOException {
606 checkWriteOk();
607 ObjectWriter writer = new ObjectWriter(db);
608 Tree current = new Tree(db);
609 Stack trees = new Stack();
610 trees.push(current);
611 String[] prevName = new String[0];
612 for (Iterator i = entries.values().iterator(); i.hasNext();) {
613 Entry e = (Entry) i.next();
614 if (e.stage != 0)
615 continue;
616 String[] newName = splitDirPath(e.getName());
617 int c = longestCommonPath(prevName, newName);
618 while (c < trees.size() - 1) {
619 current.setId(writer.writeTree(current));
620 trees.pop();
621 current = trees.isEmpty() ? null : (Tree) trees.peek();
623 while (trees.size() < newName.length) {
624 if (!current.existsTree(newName[trees.size() - 1])) {
625 current = new Tree(current, newName[trees.size() - 1]
626 .getBytes());
627 current.getParent().addEntry(current);
628 trees.push(current);
629 } else {
630 current = (Tree) current.findTreeMember(newName[trees
631 .size() - 1]);
632 trees.push(current);
635 FileTreeEntry ne = new FileTreeEntry(current, e.sha1,
636 newName[newName.length - 1].getBytes(),
637 (e.mode & FileMode.EXECUTABLE_FILE.getBits()) == FileMode.EXECUTABLE_FILE.getBits());
638 current.addEntry(ne);
640 while (!trees.isEmpty()) {
641 current.setId(writer.writeTree(current));
642 trees.pop();
643 if (!trees.isEmpty())
644 current = (Tree) trees.peek();
646 return current.getTreeId();
649 String[] splitDirPath(String name) {
650 String[] tmp = new String[name.length() / 2 + 1];
651 int p0 = -1;
652 int p1;
653 int c = 0;
654 while ((p1 = name.indexOf('/', p0 + 1)) != -1) {
655 tmp[c++] = name.substring(p0 + 1, p1);
656 p0 = p1;
658 tmp[c++] = name.substring(p0 + 1);
659 String[] ret = new String[c];
660 for (int i = 0; i < c; ++i) {
661 ret[i] = tmp[i];
663 return ret;
666 int longestCommonPath(String[] a, String[] b) {
667 int i;
668 for (i = 0; i < a.length && i < b.length; ++i)
669 if (!a[i].equals(b[i]))
670 return i;
671 return i;
674 public Entry[] getMembers() {
675 return (Entry[]) entries.values().toArray(new Entry[entries.size()]);
678 public Entry getEntry(String path) throws UnsupportedEncodingException {
679 return (Entry) entries.get(Repository.gitInternalSlash(path.getBytes("ISO-8859-1")));
682 public Repository getRepository() {
683 return db;