2 * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
3 * Copyright (C) 2007, Robin Rosenberg <me@lathund.dewire.com>
4 * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
5 * Copyright (C) 2008, Roger C. Soares <rogersoares@intelinet.com.br>
6 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
10 * Redistribution and use in source and binary forms, with or
11 * without modification, are permitted provided that the following
14 * - Redistributions of source code must retain the above copyright
15 * notice, this list of conditions and the following disclaimer.
17 * - Redistributions in binary form must reproduce the above
18 * copyright notice, this list of conditions and the following
19 * disclaimer in the documentation and/or other materials provided
20 * with the distribution.
22 * - Neither the name of the Git Development Community nor the
23 * names of its contributors may be used to endorse or promote
24 * products derived from this software without specific prior
27 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
28 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
29 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
30 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
31 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
32 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
33 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
34 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
35 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
36 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
37 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
38 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
39 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42 package org
.spearce
.jgit
.lib
;
45 import java
.io
.FileInputStream
;
46 import java
.io
.FileNotFoundException
;
47 import java
.io
.FileOutputStream
;
48 import java
.io
.IOException
;
49 import java
.io
.InputStream
;
50 import java
.io
.RandomAccessFile
;
51 import java
.io
.UnsupportedEncodingException
;
52 import java
.nio
.ByteBuffer
;
53 import java
.nio
.ByteOrder
;
54 import java
.nio
.channels
.FileChannel
;
55 import java
.security
.MessageDigest
;
56 import java
.util
.Comparator
;
57 import java
.util
.Date
;
58 import java
.util
.Iterator
;
60 import java
.util
.Stack
;
61 import java
.util
.TreeMap
;
63 import org
.spearce
.jgit
.errors
.CorruptObjectException
;
64 import org
.spearce
.jgit
.errors
.NotSupportedException
;
65 import org
.spearce
.jgit
.util
.FS
;
68 * A representation of the Git index.
70 * The index points to the objects currently checked out or in the process of
71 * being prepared for committing or objects involved in an unfinished merge.
73 * The abstract format is:<br/> path stage flags statdata SHA-1
75 * <li>Path is the relative path in the workdir</li>
76 * <li>stage is 0 (normally), but when
77 * merging 1 is the common ancestor version, 2 is 'our' version and 3 is 'their'
78 * version. A fully resolved merge only contains stage 0.</li>
79 * <li>flags is the object type and information of validity</li>
80 * <li>statdata is the size of this object and some other file system specifics,
81 * some of it ignored by JGit</li>
82 * <li>SHA-1 represents the content of the references object</li>
85 * An index can also contain a tree cache which we ignore for now. We drop the
86 * tree cache when writing the index.
88 public class GitIndex
{
90 /** Stage 0 represents merged entries. */
91 public static final int STAGE_0
= 0;
93 private RandomAccessFile cache
;
95 private File cacheFile
;
98 private boolean changed
;
100 // Stat information updated
101 private boolean statDirty
;
103 private Header header
;
105 private long lastCacheTime
;
107 private final Repository db
;
109 private Map entries
= new TreeMap(new Comparator() {
110 public int compare(Object arg0
, Object arg1
) {
111 byte[] a
= (byte[]) arg0
;
112 byte[] b
= (byte[]) arg1
;
113 for (int i
= 0; i
< a
.length
&& i
< b
.length
; ++i
) {
118 if (a
.length
< b
.length
)
120 else if (a
.length
> b
.length
)
127 * Construct a Git index representation.
130 public GitIndex(Repository db
) {
132 this.cacheFile
= new File(db
.getDirectory(), "index");
136 * @return true if we have modified the index in memory since reading it from disk
138 public boolean isChanged() {
139 return changed
|| statDirty
;
143 * Reread index data from disk if the index file has been changed
144 * @throws IOException
146 public void rereadIfNecessary() throws IOException
{
147 if (cacheFile
.exists() && cacheFile
.lastModified() != lastCacheTime
) {
153 * Add the content of a file to the index.
157 * @return a new or updated index entry for the path represented by f
158 * @throws IOException
160 public Entry
add(File wd
, File f
) throws IOException
{
161 byte[] key
= makeKey(wd
, f
);
162 Entry e
= (Entry
) entries
.get(key
);
164 e
= new Entry(key
, f
, 0);
173 * Remove a path from the index.
178 * the file whose path shall be removed.
179 * @return true if such a path was found (and thus removed)
181 public boolean remove(File wd
, File f
) {
182 byte[] key
= makeKey(wd
, f
);
183 return entries
.remove(key
) != null;
187 * Read the cache file into memory.
189 * @throws IOException
191 public void read() throws IOException
{
192 long t0
= System
.currentTimeMillis();
195 if (!cacheFile
.exists()) {
201 cache
= new RandomAccessFile(cacheFile
, "r");
203 FileChannel channel
= cache
.getChannel();
204 ByteBuffer buffer
= ByteBuffer
.allocateDirect((int) cacheFile
.length());
205 buffer
.order(ByteOrder
.BIG_ENDIAN
);
206 int j
= channel
.read(buffer
);
207 if (j
!= buffer
.capacity())
208 throw new IOException("Could not read index in one go, only "+j
+" out of "+buffer
.capacity()+" read");
210 header
= new Header(buffer
);
212 for (int i
= 0; i
< header
.entries
; ++i
) {
213 Entry entry
= new Entry(buffer
);
214 entries
.put(entry
.name
, entry
);
216 long t1
= System
.currentTimeMillis();
217 lastCacheTime
= cacheFile
.lastModified();
218 System
.out
.println("Read index "+cacheFile
+" in "+((t1
-t0
)/1000.0)+"s");
225 * Write content of index to disk.
227 * @throws IOException
229 public void write() throws IOException
{
231 File tmpIndex
= new File(cacheFile
.getAbsoluteFile() + ".tmp");
232 File lock
= new File(cacheFile
.getAbsoluteFile() + ".lock");
233 if (!lock
.createNewFile())
234 throw new IOException("Index file is in use");
236 FileOutputStream fileOutputStream
= new FileOutputStream(tmpIndex
);
237 FileChannel fc
= fileOutputStream
.getChannel();
238 ByteBuffer buf
= ByteBuffer
.allocate(4096);
239 MessageDigest newMessageDigest
= Constants
.newMessageDigest();
240 header
= new Header(entries
);
244 .update(buf
.array(), buf
.arrayOffset(), buf
.limit());
248 for (Iterator i
= entries
.values().iterator(); i
.hasNext();) {
249 Entry e
= (Entry
) i
.next();
252 newMessageDigest
.update(buf
.array(), buf
.arrayOffset(), buf
258 buf
.put(newMessageDigest
.digest());
262 fileOutputStream
.close();
263 if (cacheFile
.exists())
264 if (!cacheFile
.delete())
265 throw new IOException(
266 "Could not rename delete old index");
267 if (!tmpIndex
.renameTo(cacheFile
))
268 throw new IOException(
269 "Could not rename temporary index file to index");
274 throw new IOException(
275 "Could not delete lock file. Should not happen");
276 if (tmpIndex
.exists() && !tmpIndex
.delete())
277 throw new IOException(
278 "Could not delete temporary index file. Should not happen");
282 private void checkWriteOk() throws IOException
{
283 for (Iterator i
= entries
.values().iterator(); i
.hasNext();) {
284 Entry e
= (Entry
) i
.next();
285 if (e
.getStage() != 0) {
286 throw new NotSupportedException("Cannot work with other stages than zero right now. Won't write corrupt index.");
291 static boolean File_canExecute( File f
){
292 return FS
.INSTANCE
.canExecute(f
);
295 static boolean File_setExecute(File f
, boolean value
) {
296 return FS
.INSTANCE
.setExecute(f
, value
);
299 static boolean File_hasExecute() {
300 return FS
.INSTANCE
.supportsExecute();
303 static byte[] makeKey(File wd
, File f
) {
304 if (!f
.getPath().startsWith(wd
.getPath()))
305 throw new Error("Path is not in working dir");
306 String relName
= Repository
.stripWorkDir(wd
, f
);
307 return relName
.getBytes();
311 private boolean config_filemode() {
312 // temporary til we can actually set parameters. We need to be able
313 // to change this for testing.
314 if (filemode
!= null)
315 return filemode
.booleanValue();
316 RepositoryConfig config
= db
.getConfig();
317 return config
.getBoolean("core", null, "filemode", true);
320 /** An index entry */
338 private ObjectId sha1
;
344 Entry(byte[] key
, File f
, int stage
)
346 ctime
= f
.lastModified() * 1000000L;
347 mtime
= ctime
; // we use same here
350 if (config_filemode() && File_canExecute(f
))
351 mode
= FileMode
.EXECUTABLE_FILE
.getBits();
353 mode
= FileMode
.REGULAR_FILE
.getBits();
356 size
= (int) f
.length();
357 ObjectWriter writer
= new ObjectWriter(db
);
358 sha1
= writer
.writeBlob(f
);
360 flags
= (short) ((stage
<< 12) | name
.length
); // TODO: fix flags
363 Entry(TreeEntry f
, int stage
)
364 throws UnsupportedEncodingException
{
369 mode
= f
.getMode().getBits();
373 size
= (int) db
.openBlob(f
.getId()).getSize();
374 } catch (IOException e
) {
379 name
= f
.getFullName().getBytes("UTF-8");
380 flags
= (short) ((stage
<< 12) | name
.length
); // TODO: fix flags
383 Entry(ByteBuffer b
) {
384 int startposition
= b
.position();
385 ctime
= b
.getInt() * 1000000000L + (b
.getInt() % 1000000000L);
386 mtime
= b
.getInt() * 1000000000L + (b
.getInt() % 1000000000L);
393 byte[] sha1bytes
= new byte[Constants
.OBJECT_ID_LENGTH
];
395 sha1
= ObjectId
.fromRaw(sha1bytes
);
396 flags
= b
.getShort();
397 name
= new byte[flags
& 0xFFF];
400 .position(startposition
401 + ((8 + 8 + 4 + 4 + 4 + 4 + 4 + 4 + 20 + 2
402 + name
.length
+ 8) & ~
7));
406 * Update this index entry with stat and SHA-1 information if it looks
407 * like the file has been modified in the workdir.
411 * @return true if a change occurred
412 * @throws IOException
414 public boolean update(File f
) throws IOException
{
415 boolean modified
= false;
416 long lm
= f
.lastModified() * 1000000L;
419 mtime
= f
.lastModified() * 1000000L;
420 if (size
!= f
.length())
422 if (config_filemode()) {
423 if (File_canExecute(f
) != FileMode
.EXECUTABLE_FILE
.equals(mode
)) {
424 mode
= FileMode
.EXECUTABLE_FILE
.getBits();
429 size
= (int) f
.length();
430 ObjectWriter writer
= new ObjectWriter(db
);
431 ObjectId newsha1
= sha1
= writer
.writeBlob(f
);
432 if (!newsha1
.equals(sha1
))
439 void write(ByteBuffer buf
) {
440 int startposition
= buf
.position();
441 buf
.putInt((int) (ctime
/ 1000000000L));
442 buf
.putInt((int) (ctime
% 1000000000L));
443 buf
.putInt((int) (mtime
/ 1000000000L));
444 buf
.putInt((int) (mtime
% 1000000000L));
454 int end
= startposition
455 + ((8 + 8 + 4 + 4 + 4 + 4 + 4 + 4 + 20 + 2 + name
.length
+ 8) & ~
7);
456 int remain
= end
- buf
.position();
462 * Check if an entry's content is different from the cache,
464 * File status information is used and status is same we
465 * consider the file identical to the state in the working
466 * directory. Native git uses more stat fields than we
467 * have accessible in Java.
469 * @param wd working directory to compare content with
470 * @return true if content is most likely different.
472 public boolean isModified(File wd
) {
473 return isModified(wd
, false);
477 * Check if an entry's content is different from the cache,
479 * File status information is used and status is same we
480 * consider the file identical to the state in the working
481 * directory. Native git uses more stat fields than we
482 * have accessible in Java.
484 * @param wd working directory to compare content with
485 * @param forceContentCheck True if the actual file content
486 * should be checked if modification time differs.
488 * @return true if content is most likely different.
490 public boolean isModified(File wd
, boolean forceContentCheck
) {
492 if (isAssumedValid())
495 if (isUpdateNeeded())
498 File file
= getFile(wd
);
502 // JDK1.6 has file.canExecute
503 // if (file.canExecute() != FileMode.EXECUTABLE_FILE.equals(mode))
505 final int exebits
= FileMode
.EXECUTABLE_FILE
.getBits()
506 ^ FileMode
.REGULAR_FILE
.getBits();
508 if (config_filemode() && FileMode
.EXECUTABLE_FILE
.equals(mode
)) {
509 if (!File_canExecute(file
)&& File_hasExecute())
512 if (FileMode
.REGULAR_FILE
.equals(mode
&~exebits
)) {
515 if (config_filemode() && File_canExecute(file
) && File_hasExecute())
518 if (FileMode
.SYMLINK
.equals(mode
)) {
521 if (FileMode
.TREE
.equals(mode
)) {
522 if (!file
.isDirectory())
525 System
.out
.println("Does not handle mode "+mode
+" ("+file
+")");
532 if (file
.length() != size
)
535 // Git under windows only stores seconds so we round the timestmap
536 // Java gives us if it looks like the timestamp in index is seconds
537 // only. Otherwise we compare the timestamp at millisecond prevision.
538 long javamtime
= mtime
/ 1000000L;
539 long lastm
= file
.lastModified();
540 if (javamtime
% 1000 == 0)
541 lastm
= lastm
- lastm
% 1000;
542 if (lastm
!= javamtime
) {
543 if (!forceContentCheck
)
547 InputStream is
= new FileInputStream(file
);
548 ObjectWriter objectWriter
= new ObjectWriter(db
);
550 ObjectId newId
= objectWriter
.computeBlobSha1(file
552 boolean ret
= !newId
.equals(sha1
);
554 } catch (IOException e
) {
559 } catch (IOException e
) {
560 // can't happen, but if it does we ignore it
564 } catch (FileNotFoundException e
) {
565 // should not happen because we already checked this
574 void forceRecheck() {
578 private File
getFile(File wd
) {
579 return new File(wd
, getName());
582 public String
toString() {
583 return new String(name
) + "/SHA-1(" + sha1
+ ")/M:"
584 + new Date(ctime
/ 1000000L) + "/C:"
585 + new Date(mtime
/ 1000000L) + "/d" + dev
+ "/i" + ino
586 + "/m" + Integer
.toString(mode
, 8) + "/u" + uid
+ "/g"
587 + gid
+ "/s" + size
+ "/f" + flags
+ "/@" + getStage();
591 * @return path name for this entry
593 public String
getName() {
594 return new String(name
);
598 * @return path name for this entry as byte array, hopefully UTF-8 encoded
600 public byte[] getNameUTF8() {
605 * @return SHA-1 of the entry managed by this index
607 public ObjectId
getObjectId() {
612 * @return the stage this entry is in
614 public int getStage() {
615 return (flags
& 0x3000) >> 12;
619 * @return size of disk object
621 public int getSize() {
626 * @return true if this entry shall be assumed valid
628 public boolean isAssumedValid() {
629 return (flags
& 0x8000) != 0;
633 * @return true if this entry should be checked for changes
635 public boolean isUpdateNeeded() {
636 return (flags
& 0x4000) != 0;
640 * Set whether to always assume this entry valid
642 * @param assumeValid true to ignore changes
644 public void setAssumeValid(boolean assumeValid
) {
652 * Set whether this entry must be checked
654 * @param updateNeeded
656 public void setUpdateNeeded(boolean updateNeeded
) {
664 * Return raw file mode bits. See {@link FileMode}
665 * @return file mode bits
667 public int getModeBits() {
672 static class Header
{
673 private int signature
;
679 Header(ByteBuffer map
) throws CorruptObjectException
{
683 private void read(ByteBuffer buf
) throws CorruptObjectException
{
684 signature
= buf
.getInt();
685 version
= buf
.getInt();
686 entries
= buf
.getInt();
687 if (signature
!= 0x44495243)
688 throw new CorruptObjectException("Index signature is invalid: "
691 throw new CorruptObjectException(
692 "Unknow index version (or corrupt index):" + version
);
695 void write(ByteBuffer buf
) {
696 buf
.order(ByteOrder
.BIG_ENDIAN
);
697 buf
.putInt(signature
);
702 Header(Map entryset
) {
703 signature
= 0x44495243;
705 entries
= entryset
.size();
709 void readTree(Tree t
) throws IOException
{
713 void readTree(String prefix
, Tree t
) throws IOException
{
714 TreeEntry
[] members
= t
.members();
715 for (int i
= 0; i
< members
.length
; ++i
) {
716 TreeEntry te
= members
[i
];
718 if (prefix
.length() > 0)
719 name
= prefix
+ "/" + te
.getName();
722 if (te
instanceof Tree
) {
723 readTree(name
, (Tree
) te
);
725 Entry e
= new Entry(te
, 0);
726 entries
.put(name
.getBytes("UTF-8"), e
);
732 * Add tree entry to index
733 * @param te tree entry
734 * @return new or modified index entry
735 * @throws IOException
737 public Entry
addEntry(TreeEntry te
) throws IOException
{
738 byte[] key
= te
.getFullName().getBytes("UTF-8");
739 Entry e
= new Entry(te
, 0);
745 * Check out content of the content represented by the index
749 * @throws IOException
751 public void checkout(File wd
) throws IOException
{
752 for (Iterator i
= entries
.values().iterator(); i
.hasNext();) {
753 Entry e
= (Entry
) i
.next();
754 if (e
.getStage() != 0)
756 checkoutEntry(wd
, e
);
761 * Check out content of the specified index entry
764 * @param e index entry
765 * @throws IOException
767 public void checkoutEntry(File wd
, Entry e
) throws IOException
{
768 ObjectLoader ol
= db
.openBlob(e
.sha1
);
769 byte[] bytes
= ol
.getBytes();
770 File file
= new File(wd
, e
.getName());
772 file
.getParentFile().mkdirs();
773 FileChannel channel
= new FileOutputStream(file
).getChannel();
774 ByteBuffer buffer
= ByteBuffer
.wrap(bytes
);
775 int j
= channel
.write(buffer
);
776 if (j
!= bytes
.length
)
777 throw new IOException("Could not write file " + file
);
779 if (config_filemode() && File_hasExecute()) {
780 if (FileMode
.EXECUTABLE_FILE
.equals(e
.mode
)) {
781 if (!File_canExecute(file
))
782 File_setExecute(file
, true);
784 if (File_canExecute(file
))
785 File_setExecute(file
, false);
788 e
.mtime
= file
.lastModified() * 1000000L;
793 * Construct and write tree out of index.
795 * @return SHA-1 of the constructed tree
797 * @throws IOException
799 public ObjectId
writeTree() throws IOException
{
801 ObjectWriter writer
= new ObjectWriter(db
);
802 Tree current
= new Tree(db
);
803 Stack trees
= new Stack();
805 String
[] prevName
= new String
[0];
806 for (Iterator i
= entries
.values().iterator(); i
.hasNext();) {
807 Entry e
= (Entry
) i
.next();
808 if (e
.getStage() != 0)
810 String
[] newName
= splitDirPath(e
.getName());
811 int c
= longestCommonPath(prevName
, newName
);
812 while (c
< trees
.size() - 1) {
813 current
.setId(writer
.writeTree(current
));
815 current
= trees
.isEmpty() ?
null : (Tree
) trees
.peek();
817 while (trees
.size() < newName
.length
) {
818 if (!current
.existsTree(newName
[trees
.size() - 1])) {
819 current
= new Tree(current
, newName
[trees
.size() - 1]
821 current
.getParent().addEntry(current
);
824 current
= (Tree
) current
.findTreeMember(newName
[trees
829 FileTreeEntry ne
= new FileTreeEntry(current
, e
.sha1
,
830 newName
[newName
.length
- 1].getBytes(),
831 (e
.mode
& FileMode
.EXECUTABLE_FILE
.getBits()) == FileMode
.EXECUTABLE_FILE
.getBits());
832 current
.addEntry(ne
);
834 while (!trees
.isEmpty()) {
835 current
.setId(writer
.writeTree(current
));
837 if (!trees
.isEmpty())
838 current
= (Tree
) trees
.peek();
840 return current
.getTreeId();
843 String
[] splitDirPath(String name
) {
844 String
[] tmp
= new String
[name
.length() / 2 + 1];
848 while ((p1
= name
.indexOf('/', p0
+ 1)) != -1) {
849 tmp
[c
++] = name
.substring(p0
+ 1, p1
);
852 tmp
[c
++] = name
.substring(p0
+ 1);
853 String
[] ret
= new String
[c
];
854 for (int i
= 0; i
< c
; ++i
) {
860 int longestCommonPath(String
[] a
, String
[] b
) {
862 for (i
= 0; i
< a
.length
&& i
< b
.length
; ++i
)
863 if (!a
[i
].equals(b
[i
]))
869 * Return the members of the index sorted by the unsigned byte
870 * values of the path names.
872 * Small beware: Unaccounted for are unmerged entries. You may want
873 * to abort if members with stage != 0 are found if you are doing
874 * any updating operations. All stages will be found after one another
875 * here later. Currently only one stage per name is returned.
877 * @return The index entries sorted
879 public Entry
[] getMembers() {
880 return (Entry
[]) entries
.values().toArray(new Entry
[entries
.size()]);
884 * Look up an entry with the specified path.
887 * @return index entry for the path or null if not in index.
888 * @throws UnsupportedEncodingException
890 public Entry
getEntry(String path
) throws UnsupportedEncodingException
{
891 return (Entry
) entries
.get(Repository
.gitInternalSlash(path
.getBytes("ISO-8859-1")));
895 * @return The repository holding this index.
897 public Repository
getRepository() {