1 package org
.spearce
.jgit
.lib
;
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
;
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
;
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
) {
57 if (a
.length
< b
.length
)
59 else if (a
.length
> b
.length
)
65 public GitIndex(Repository 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
) {
80 public Entry
add(File wd
, File f
) throws IOException
{
81 byte[] key
= makeKey(wd
, f
);
82 Entry e
= (Entry
) entries
.get(key
);
84 e
= new Entry(key
, f
, 0);
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();
101 if (!cacheFile
.exists()) {
107 cache
= new RandomAccessFile(cacheFile
, "r");
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");
116 header
= new Header(buffer
);
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");
130 public void write() throws IOException
{
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");
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
);
145 .update(buf
.array(), buf
.arrayOffset(), buf
.limit());
149 for (Iterator i
= entries
.values().iterator(); i
.hasNext();) {
150 Entry e
= (Entry
) i
.next();
153 newMessageDigest
.update(buf
.array(), buf
.arrayOffset(), buf
159 buf
.put(newMessageDigest
.digest());
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");
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();
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
;
196 canExecute
= File
.class.getMethod("canExecute", (Class
[]) null);
197 setExecute
= File
.class.getMethod("setExecutable", new Class
[] { Boolean
.TYPE
});
198 } catch (SecurityException e
) {
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))
211 boolean File_canExecute(File f
) {
212 if (canExecute
!= null) {
214 return ((Boolean
) canExecute
.invoke(f
, (Object
[]) null))
216 } catch (IllegalArgumentException e
) {
218 } catch (IllegalAccessException e
) {
220 } catch (InvocationTargetException e
) {
228 * JDK1.6 has file.setExecute
230 boolean File_setExecute(File f
,boolean value
) {
231 if (setExecute
!= null) {
233 return ((Boolean
) setExecute
.invoke(f
,
234 new Object
[] { new Boolean(value
) })).booleanValue();
235 } catch (IllegalArgumentException e
) {
237 } catch (IllegalAccessException e
) {
239 } catch (InvocationTargetException e
) {
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();
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);
281 private ObjectId sha1
;
289 public Entry(byte[] key
, File f
, int stage
)
291 ctime
= f
.lastModified() * 1000000L;
292 mtime
= ctime
; // we use same here
295 if (config_filemode() && File_canExecute(f
))
296 mode
= FileMode
.EXECUTABLE_FILE
.getBits();
298 mode
= FileMode
.REGULAR_FILE
.getBits();
301 size
= (int) f
.length();
302 ObjectWriter writer
= new ObjectWriter(db
);
303 sha1
= writer
.writeBlob(f
);
305 flags
= (short) ((stage
<< 12) | name
.length
); // TODO: fix flags
308 public Entry(TreeEntry f
, int stage
)
309 throws UnsupportedEncodingException
{
314 mode
= f
.getMode().getBits();
318 size
= (int) db
.openBlob(f
.getId()).getSize();
319 } catch (IOException e
) {
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);
338 byte[] sha1bytes
= new byte[Constants
.OBJECT_ID_LENGTH
];
340 sha1
= new ObjectId(sha1bytes
);
341 flags
= b
.getShort();
342 stage
= (flags
& 0x3000) >> 12;
343 name
= new byte[flags
& 0xFFF];
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;
356 mtime
= f
.lastModified() * 1000000L;
357 if (size
!= f
.length())
359 if (config_filemode()) {
360 if (File_canExecute(f
) != FileMode
.EXECUTABLE_FILE
.equals(mode
)) {
361 mode
= FileMode
.EXECUTABLE_FILE
.getBits();
366 size
= (int) f
.length();
367 ObjectWriter writer
= new ObjectWriter(db
);
368 ObjectId newsha1
= sha1
= writer
.writeBlob(f
);
369 if (!newsha1
.equals(sha1
))
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));
388 buf
.put(sha1
.getBytes());
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();
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
);
432 // JDK1.6 has file.canExecute
433 // if (file.canExecute() != FileMode.EXECUTABLE_FILE.equals(mode))
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)
442 if (FileMode
.REGULAR_FILE
.equals(mode
&~exebits
)) {
445 if (config_filemode() && File_canExecute(file
) && canExecute
!= null)
448 if (FileMode
.SYMLINK
.equals(mode
)) {
451 if (FileMode
.TREE
.equals(mode
)) {
452 if (!file
.isDirectory())
455 System
.out
.println("Does not handle mode "+mode
+" ("+file
+")");
462 if (file
.length() != size
)
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
)
477 InputStream is
= new FileInputStream(file
);
478 ObjectWriter objectWriter
= new ObjectWriter(db
);
480 ObjectId newId
= objectWriter
.computeBlobSha1(file
482 boolean ret
= !newId
.equals(sha1
);
484 } catch (IOException e
) {
489 } catch (IOException e
) {
490 // can't happen, but if it does we ignore it
494 } catch (FileNotFoundException e
) {
495 // should not happen because we already checked this
504 void forceRecheck() {
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() {
528 public int getStage() {
532 public int getSize() {
537 static class Header
{
538 private int signature
;
544 public Header(ByteBuffer map
) throws CorruptObjectException
{
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: "
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
);
567 public Header(Map entryset
) {
568 signature
= 0x44495243;
570 entries
= entryset
.size();
574 public void readTree(Tree t
) throws IOException
{
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
];
583 if (prefix
.length() > 0)
584 name
= prefix
+ "/" + te
.getName();
587 if (te
instanceof Tree
) {
588 readTree(name
, (Tree
) te
);
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);
603 public void checkout(File wd
) throws IOException
{
604 for (Iterator i
= entries
.values().iterator(); i
.hasNext();) {
605 Entry e
= (Entry
) i
.next();
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());
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
);
624 if (config_filemode() && canExecute
!= null) {
625 if (FileMode
.EXECUTABLE_FILE
.equals(e
.mode
)) {
626 if (!File_canExecute(file
))
627 File_setExecute(file
, true);
629 if (File_canExecute(file
))
630 File_setExecute(file
, false);
633 e
.mtime
= file
.lastModified() * 1000000L;
637 public ObjectId
writeTree() throws IOException
{
639 ObjectWriter writer
= new ObjectWriter(db
);
640 Tree current
= new Tree(db
);
641 Stack trees
= new Stack();
643 String
[] prevName
= new String
[0];
644 for (Iterator i
= entries
.values().iterator(); i
.hasNext();) {
645 Entry e
= (Entry
) i
.next();
648 String
[] newName
= splitDirPath(e
.getName());
649 int c
= longestCommonPath(prevName
, newName
);
650 while (c
< trees
.size() - 1) {
651 current
.setId(writer
.writeTree(current
));
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]
659 current
.getParent().addEntry(current
);
662 current
= (Tree
) current
.findTreeMember(newName
[trees
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
));
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];
686 while ((p1
= name
.indexOf('/', p0
+ 1)) != -1) {
687 tmp
[c
++] = name
.substring(p0
+ 1, p1
);
690 tmp
[c
++] = name
.substring(p0
+ 1);
691 String
[] ret
= new String
[c
];
692 for (int i
= 0; i
< c
; ++i
) {
698 int longestCommonPath(String
[] a
, String
[] b
) {
700 for (i
= 0; i
< a
.length
&& i
< b
.length
; ++i
)
701 if (!a
[i
].equals(b
[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() {