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
.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
;
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
;
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
) {
55 if (a
.length
< b
.length
)
57 else if (a
.length
> b
.length
)
63 public GitIndex(Repository 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
) {
78 public void add(File wd
, File f
) throws IOException
{
79 byte[] key
= Entry
.makeKey(wd
, f
);
80 Entry e
= (Entry
) entries
.get(key
);
82 e
= new Entry(key
, f
, 0, this);
89 public void remove(File wd
, File f
) {
90 byte[] key
= Entry
.makeKey(wd
, f
);
94 public void read() throws IOException
{
95 long t0
= System
.currentTimeMillis();
98 cache
= new RandomAccessFile(cacheFile
, "r");
100 MappedByteBuffer map
= cache
.getChannel().map(MapMode
.READ_ONLY
, 0,
102 map
.order(ByteOrder
.BIG_ENDIAN
);
103 header
= new Header(map
);
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");
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");
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
);
131 .update(buf
.array(), buf
.arrayOffset(), buf
.limit());
135 for (Iterator i
= entries
.values().iterator(); i
.hasNext();) {
136 Entry e
= (Entry
) i
.next();
139 newMessageDigest
.update(buf
.array(), buf
.arrayOffset(), buf
145 buf
.put(newMessageDigest
.digest());
149 fileOutputStream
.close();
150 if (!tmpIndex
.renameTo(cacheFile
))
151 throw new IOException(
152 "Could not rename temporary index file to index");
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
{
182 private ObjectId sha1
;
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
)
203 ctime
= f
.lastModified() * 1000000L;
204 mtime
= ctime
; // we use same here
207 mode
= FileMode
.REGULAR_FILE
.getBits();
210 size
= (int) f
.length();
211 ObjectWriter writer
= new ObjectWriter(theIndex
.db
);
212 sha1
= writer
.writeBlob(f
);
214 flags
= (short) ((stage
<< 12) | name
.length
); // TODO: fix flags
217 public Entry(TreeEntry f
, int stage
, GitIndex index
)
218 throws UnsupportedEncodingException
{
224 mode
= f
.getMode().getBits();
229 name
= f
.getFullName().getBytes("UTF-8");
230 flags
= (short) ((stage
<< 12) | name
.length
); // TODO: fix flags
233 Entry(GitIndex index
, ByteBuffer b
) {
235 int startposition
= b
.position();
236 ctime
= b
.getInt() * 1000000000L + (b
.getInt() % 1000000000L);
237 mtime
= b
.getInt() * 1000000000L + (b
.getInt() % 1000000000L);
244 byte[] sha1bytes
= new byte[Constants
.OBJECT_ID_LENGTH
];
246 sha1
= new ObjectId(sha1bytes
);
247 flags
= b
.getShort();
248 stage
= (flags
& 0x4000) >> 12;
249 name
= new byte[flags
& 0xFFF];
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;
262 mtime
= f
.lastModified() * 1000000L;
263 if (size
!= f
.length())
265 if (File_canExecute(f
) != FileMode
.EXECUTABLE_FILE
.equals(mode
)) {
266 mode
= FileMode
.EXECUTABLE_FILE
.getBits();
270 size
= (int) f
.length();
271 ObjectWriter writer
= new ObjectWriter(db
);
272 ObjectId newsha1
= sha1
= writer
.writeBlob(f
);
273 if (!newsha1
.equals(sha1
))
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));
292 buf
.put(sha1
.getBytes());
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();
302 static Method canExecute
;
305 canExecute
= File
.class.getMethod("canExecute", (Class
[]) null);
306 } catch (SecurityException e
) {
307 // TODO Auto-generated catch block
309 } catch (NoSuchMethodException e
) {
310 // TODO Auto-generated catch block
316 * JDK1.6 has file.canExecute
318 * if (file.canExecute() != FileMode.EXECUTABLE_FILE.equals(mode))
321 boolean File_canExecute(File f
) {
322 if (canExecute
!= null) {
324 return ((Boolean
) canExecute
.invoke(f
, (Object
[]) null))
326 } catch (IllegalArgumentException e
) {
327 // TODO Auto-generated catch block
330 } catch (IllegalAccessException e
) {
331 // TODO Auto-generated catch block
334 } catch (InvocationTargetException e
) {
335 // TODO Auto-generated catch block
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
);
377 // JDK1.6 has file.canExecute
378 // if (file.canExecute() != FileMode.EXECUTABLE_FILE.equals(mode))
380 if (FileMode
.EXECUTABLE_FILE
.equals(mode
)) {
381 if (!File_canExecute(file
))
384 if (FileMode
.REGULAR_FILE
.equals(mode
)) {
387 if (File_canExecute(file
))
390 if (FileMode
.SYMLINK
.equals(mode
)) {
393 if (FileMode
.TREE
.equals(mode
)) {
394 if (!file
.isDirectory())
397 System
.out
.println("Does not handle mode "+mode
+" ("+file
+")");
404 long javamtime
= mtime
/ 1000000L;
405 long lastm
= file
.lastModified();
406 if (file
.length() != size
)
408 if (lastm
!= javamtime
) {
409 if (!forceContentCheck
)
413 InputStream is
= new FileInputStream(file
);
414 ObjectWriter objectWriter
= new ObjectWriter(theIndex
.db
);
416 ObjectId newId
= objectWriter
.computeBlobSha1(file
418 boolean ret
= !newId
.equals(sha1
);
419 theIndex
.statDirty
= true;
421 } catch (IOException e
) {
426 } catch (IOException e
) {
427 // can't happen, but if it does we ignore it
431 } catch (FileNotFoundException e
) {
432 // should not happen because we already checked this
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() {
461 static class Header
{
462 private int signature
;
468 public Header(ByteBuffer map
) throws CorruptObjectException
{
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: "
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
);
491 public Header(Map entryset
) {
492 signature
= 0x44495243;
494 entries
= entryset
.size();
498 public void readTree(Tree t
) throws IOException
{
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
];
507 if (prefix
.length() > 0)
508 name
= prefix
+ "/" + te
.getName();
511 if (te
instanceof Tree
) {
512 readTree(name
, (Tree
) te
);
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();
525 ObjectLoader ol
= db
.openBlob(e
.sha1
);
526 byte[] bytes
= ol
.getBytes();
527 File file
= new File(wd
, e
.getName());
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
);
539 public ObjectId
writeTree() throws IOException
{
540 ObjectWriter writer
= new ObjectWriter(db
);
541 Tree current
= new Tree(db
);
542 Stack trees
= new Stack();
544 String
[] prevName
= new String
[0];
545 for (Iterator i
= entries
.values().iterator(); i
.hasNext();) {
546 Entry e
= (Entry
) i
.next();
549 String
[] newName
= splitDirPath(e
.getName());
550 int c
= longestCommonPath(prevName
, newName
);
551 while (c
< trees
.size() - 1) {
552 current
.setId(writer
.writeTree(current
));
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]
560 current
.getParent().addEntry(current
);
563 current
= (Tree
) current
.findTreeMember(newName
[trees
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
));
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];
587 while ((p1
= name
.indexOf('/', p0
+ 1)) != -1) {
588 tmp
[c
++] = name
.substring(p0
+ 1, p1
);
591 tmp
[c
++] = name
.substring(p0
+ 1);
592 String
[] ret
= new String
[c
];
593 for (int i
= 0; i
< c
; ++i
) {
599 int longestCommonPath(String
[] a
, String
[] b
) {
601 for (i
= 0; i
< a
.length
&& i
< b
.length
; ++i
)
602 if (!a
[i
].equals(b
[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"));