2 * Copyright (C) 2006 Shawn Pearce <spearce@spearce.org>
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public
6 * License, version 2, as published by the Free Software Foundation.
8 * This library is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 * General Public License for more details.
13 * You should have received a copy of the GNU General Public
14 * License along with this library; if not, write to the Free Software
15 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
17 package org
.spearce
.jgit
.lib
;
19 import java
.io
.BufferedReader
;
21 import java
.io
.FileNotFoundException
;
22 import java
.io
.FileReader
;
23 import java
.io
.FilenameFilter
;
24 import java
.io
.IOException
;
25 import java
.util
.ArrayList
;
26 import java
.util
.Collection
;
27 import java
.util
.HashMap
;
30 import org
.spearce
.jgit
.errors
.IncorrectObjectTypeException
;
31 import org
.spearce
.jgit
.errors
.RevisionSyntaxException
;
32 import org
.spearce
.jgit
.stgit
.StGitPatch
;
33 import org
.spearce
.jgit
.util
.FS
;
36 * Represents a Git repository. A repository holds all objects and refs used for
37 * managing source code (could by any type of file, but source code is what
38 * SCM's are typically used for).
40 * In Git terms all data is stored in GIT_DIR, typically a directory called
41 * .git. A work tree is maintained unless the repository is a bare repository.
42 * Typically the .git directory is located at the root of the work dir.
47 * <li>objects/ - objects</li>
48 * <li>refs/ - tags and heads</li>
49 * <li>config - configuration</li>
50 * <li>info/ - more configurations</li>
55 * This implementation only handles a subtly undocumented subset of git features.
58 public class Repository
{
59 private final File gitDir
;
61 private final File
[] objectsDirs
;
63 private final RepositoryConfig config
;
65 private final RefDatabase refs
;
67 private PackFile
[] packs
;
69 private final WindowCache windows
;
71 private GitIndex index
;
74 * Construct a representation of this git repo managing a Git repository.
80 public Repository(final File d
) throws IOException
{
85 * Construct a representation of a Git repository.
88 * cache this repository's data will be cached through during
89 * access. May be shared with another repository, or null to
90 * indicate this repository should allocate its own private
93 * GIT_DIR (the location of the repository metadata).
95 * the repository appears to already exist but cannot be
98 public Repository(final WindowCache wc
, final File d
) throws IOException
{
99 gitDir
= d
.getAbsoluteFile();
101 objectsDirs
= readObjectsDirs(FS
.resolve(gitDir
, "objects"),
102 new ArrayList
<File
>()).toArray(new File
[0]);
103 } catch (IOException e
) {
104 IOException ex
= new IOException("Cannot find all object dirs for " + gitDir
);
108 refs
= new RefDatabase(this);
109 packs
= new PackFile
[0];
110 config
= new RepositoryConfig(this);
112 final boolean isExisting
= objectsDirs
[0].exists();
115 final String repositoryFormatVersion
= getConfig().getString(
116 "core", null, "repositoryFormatVersion");
117 if (!"0".equals(repositoryFormatVersion
)) {
118 throw new IOException("Unknown repository format \""
119 + repositoryFormatVersion
+ "\"; expected \"0\".");
122 getConfig().create();
124 windows
= wc
!= null ? wc
: new WindowCache(getConfig());
129 private Collection
<File
> readObjectsDirs(File objectsDir
, Collection
<File
> ret
) throws IOException
{
131 final File altFile
= FS
.resolve(objectsDir
, "info/alternates");
132 if (altFile
.exists()) {
133 BufferedReader ar
= new BufferedReader(new FileReader(altFile
));
134 for (String alt
=ar
.readLine(); alt
!=null; alt
=ar
.readLine()) {
135 readObjectsDirs(FS
.resolve(objectsDir
, alt
), ret
);
143 * Create a new Git repository initializing the necessary files and
146 * @throws IOException
148 public void create() throws IOException
{
149 if (gitDir
.exists()) {
150 throw new IllegalStateException("Repository already exists: "
157 objectsDirs
[0].mkdirs();
158 new File(objectsDirs
[0], "pack").mkdir();
159 new File(objectsDirs
[0], "info").mkdir();
161 new File(gitDir
, "branches").mkdir();
162 new File(gitDir
, "remotes").mkdir();
163 final String master
= Constants
.HEADS_PREFIX
+ "/" + Constants
.MASTER
;
164 refs
.link(Constants
.HEAD
, master
);
166 getConfig().create();
173 public File
getDirectory() {
178 * @return the directory containg the objects owned by this repository.
180 public File
getObjectsDirectory() {
181 return objectsDirs
[0];
185 * @return the configuration of this repository
187 public RepositoryConfig
getConfig() {
192 * @return the cache needed for accessing packed objects in this repository.
194 public WindowCache
getWindowCache() {
199 * Construct a filename where the loose object having a specified SHA-1
200 * should be stored. If the object is stored in a shared repository the path
201 * to the alternative repo will be returned. If the object is not yet store
202 * a usable path in this repo will be returned. It is assumed that callers
203 * will look for objects in a pack first.
206 * @return suggested file name
208 public File
toFile(final AnyObjectId objectId
) {
209 final String n
= objectId
.toString();
210 String d
=n
.substring(0, 2);
211 String f
=n
.substring(2);
212 for (int i
=0; i
<objectsDirs
.length
; ++i
) {
213 File ret
= new File(new File(objectsDirs
[i
], d
), f
);
217 return new File(new File(objectsDirs
[0], d
), f
);
222 * @return true if the specified object is stored in this repo or any of the
223 * known shared repositories.
225 public boolean hasObject(final AnyObjectId objectId
) {
226 int k
= packs
.length
;
229 if (packs
[--k
].hasObject(objectId
))
233 return toFile(objectId
).isFile();
238 * SHA-1 of an object.
240 * @return a {@link ObjectLoader} for accessing the data of the named
241 * object, or null if the object does not exist.
242 * @throws IOException
244 public ObjectLoader
openObject(final AnyObjectId id
)
246 return openObject(new WindowCursor(),id
);
251 * temporary working space associated with the calling thread.
253 * SHA-1 of an object.
255 * @return a {@link ObjectLoader} for accessing the data of the named
256 * object, or null if the object does not exist.
257 * @throws IOException
259 public ObjectLoader
openObject(final WindowCursor curs
, final AnyObjectId id
)
261 int k
= packs
.length
;
265 final ObjectLoader ol
= packs
[--k
].get(curs
, id
);
268 } catch (IOException ioe
) {
269 // This shouldn't happen unless the pack was corrupted
270 // after we opened it or the VM runs out of memory. This is
271 // a know problem with memory mapped I/O in java and have
272 // been noticed with JDK < 1.6. Tell the gc that now is a good
273 // time to collect and try once more.
277 final ObjectLoader ol
= packs
[k
].get(curs
, id
);
280 } catch (IOException ioe2
) {
281 ioe2
.printStackTrace();
282 ioe
.printStackTrace();
283 // Still fails.. that's BAD, maybe the pack has
284 // been corrupted after all, or the gc didn't manage
285 // to release enough previously mmaped areas.
291 return new UnpackedObjectLoader(this, id
.toObjectId());
292 } catch (FileNotFoundException fnfe
) {
300 * @return an {@link ObjectLoader} for accessing the data of a named blob
301 * @throws IOException
303 public ObjectLoader
openBlob(final ObjectId id
) throws IOException
{
304 return openObject(id
);
310 * @return an {@link ObjectLoader} for accessing the data of a named tree
311 * @throws IOException
313 public ObjectLoader
openTree(final ObjectId id
) throws IOException
{
314 return openObject(id
);
318 * Access a Commit object using a symbolic reference. This reference may
319 * be a SHA-1 or ref in combination with a number of symbols translating
320 * from one ref or SHA1-1 to another, such as HEAD^ etc.
322 * @param revstr a reference to a git commit object
323 * @return a Commit named by the specified string
324 * @throws IOException for I/O error or unexpected object type.
326 * @see #resolve(String)
328 public Commit
mapCommit(final String revstr
) throws IOException
{
329 final ObjectId id
= resolve(revstr
);
330 return id
!= null ?
mapCommit(id
) : null;
334 * Access any type of Git object by id and
337 * SHA-1 of object to read
338 * @param refName optional, only relevant for simple tags
339 * @return The Git object if found or null
340 * @throws IOException
342 public Object
mapObject(final ObjectId id
, final String refName
) throws IOException
{
343 final ObjectLoader or
= openObject(id
);
344 final byte[] raw
= or
.getBytes();
345 if (or
.getType() == Constants
.OBJ_TREE
)
346 return makeTree(id
, raw
);
347 if (or
.getType() == Constants
.OBJ_COMMIT
)
348 return makeCommit(id
, raw
);
349 if (or
.getType() == Constants
.OBJ_TAG
)
350 return makeTag(id
, refName
, raw
);
351 if (or
.getType() == Constants
.OBJ_BLOB
)
357 * Access a Commit by SHA'1 id.
359 * @return Commit or null
360 * @throws IOException for I/O error or unexpected object type.
362 public Commit
mapCommit(final ObjectId id
) throws IOException
{
363 final ObjectLoader or
= openObject(id
);
366 final byte[] raw
= or
.getBytes();
367 if (Constants
.OBJ_COMMIT
== or
.getType())
368 return new Commit(this, id
, raw
);
369 throw new IncorrectObjectTypeException(id
, Constants
.TYPE_COMMIT
);
372 private Commit
makeCommit(final ObjectId id
, final byte[] raw
) {
373 Commit ret
= new Commit(this, id
, raw
);
378 * Access a Tree object using a symbolic reference. This reference may
379 * be a SHA-1 or ref in combination with a number of symbols translating
380 * from one ref or SHA1-1 to another, such as HEAD^{tree} etc.
382 * @param revstr a reference to a git commit object
383 * @return a Tree named by the specified string
384 * @throws IOException
386 * @see #resolve(String)
388 public Tree
mapTree(final String revstr
) throws IOException
{
389 final ObjectId id
= resolve(revstr
);
390 return id
!= null ?
mapTree(id
) : null;
394 * Access a Tree by SHA'1 id.
396 * @return Tree or null
397 * @throws IOException for I/O error or unexpected object type.
399 public Tree
mapTree(final ObjectId id
) throws IOException
{
400 final ObjectLoader or
= openObject(id
);
403 final byte[] raw
= or
.getBytes();
404 if (Constants
.OBJ_TREE
== or
.getType()) {
405 return new Tree(this, id
, raw
);
407 if (Constants
.OBJ_COMMIT
== or
.getType())
408 return mapTree(ObjectId
.fromString(raw
, 5));
409 throw new IncorrectObjectTypeException(id
, Constants
.TYPE_TREE
);
412 private Tree
makeTree(final ObjectId id
, final byte[] raw
) throws IOException
{
413 Tree ret
= new Tree(this, id
, raw
);
417 private Tag
makeTag(final ObjectId id
, final String refName
, final byte[] raw
) {
418 Tag ret
= new Tag(this, id
, refName
, raw
);
423 * Access a tag by symbolic name.
426 * @return a Tag or null
427 * @throws IOException on I/O error or unexpected type
429 public Tag
mapTag(String revstr
) throws IOException
{
430 final ObjectId id
= resolve(revstr
);
431 return id
!= null ?
mapTag(revstr
, id
) : null;
435 * Access a Tag by SHA'1 id
438 * @return Commit or null
439 * @throws IOException for I/O error or unexpected object type.
441 public Tag
mapTag(final String refName
, final ObjectId id
) throws IOException
{
442 final ObjectLoader or
= openObject(id
);
445 final byte[] raw
= or
.getBytes();
446 if (Constants
.OBJ_TAG
== or
.getType())
447 return new Tag(this, id
, refName
, raw
);
448 return new Tag(this, id
, refName
, null);
452 * Create a command to update (or create) a ref in this repository.
455 * name of the ref the caller wants to modify.
456 * @return an update command. The caller must finish populating this command
457 * and then invoke one of the update methods to actually make a
459 * @throws IOException
460 * a symbolic ref was passed in and could not be resolved back
461 * to the base ref, as the symbolic ref could not be read.
463 public RefUpdate
updateRef(final String ref
) throws IOException
{
464 return refs
.newUpdate(ref
);
468 * Parse a git revision string and return an object id.
470 * Currently supported is combinations of these.
472 * <li>SHA-1 - a SHA-1</li>
473 * <li>refs/... - a ref name</li>
474 * <li>ref^n - nth parent reference</li>
475 * <li>ref~n - distance via parent reference</li>
476 * <li>ref@{n} - nth version of ref</li>
477 * <li>ref^{tree} - tree references by ref</li>
478 * <li>ref^{commit} - commit references by ref</li>
483 * <li>timestamps in reflogs, ref@{full or relative timestamp}</li>
484 * <li>abbreviated SHA-1's</li>
487 * @param revstr A git object references expression
488 * @return an ObjectId
489 * @throws IOException on serious errors
491 public ObjectId
resolve(final String revstr
) throws IOException
{
492 char[] rev
= revstr
.toCharArray();
494 ObjectId refId
= null;
495 for (int i
= 0; i
< rev
.length
; ++i
) {
499 String refstr
= new String(rev
,0,i
);
500 refId
= resolveSimple(refstr
);
504 if (i
+ 1 < rev
.length
) {
505 switch (rev
[i
+ 1]) {
517 ref
= mapObject(refId
, null);
518 if (!(ref
instanceof Commit
))
519 throw new IncorrectObjectTypeException(refId
, Constants
.TYPE_COMMIT
);
520 for (j
=i
+1; j
<rev
.length
; ++j
) {
521 if (!Character
.isDigit(rev
[j
]))
524 String parentnum
= new String(rev
, i
+1, j
-i
-1);
525 int pnum
= Integer
.parseInt(parentnum
);
527 refId
= ((Commit
)ref
).getParentIds()[pnum
- 1];
533 for (k
=i
+2; k
<rev
.length
; ++k
) {
535 item
= new String(rev
, i
+2, k
-i
-2);
541 if (item
.equals("tree")) {
542 ref
= mapObject(refId
, null);
543 while (ref
instanceof Tag
) {
545 refId
= t
.getObjId();
546 ref
= mapObject(refId
, null);
548 if (ref
instanceof Treeish
)
549 refId
= ((Treeish
)ref
).getTreeId();
551 throw new IncorrectObjectTypeException(refId
, Constants
.TYPE_TREE
);
553 else if (item
.equals("commit")) {
554 ref
= mapObject(refId
, null);
555 while (ref
instanceof Tag
) {
557 refId
= t
.getObjId();
558 ref
= mapObject(refId
, null);
560 if (!(ref
instanceof Commit
))
561 throw new IncorrectObjectTypeException(refId
, Constants
.TYPE_COMMIT
);
563 else if (item
.equals("blob")) {
564 ref
= mapObject(refId
, null);
565 while (ref
instanceof Tag
) {
567 refId
= t
.getObjId();
568 ref
= mapObject(refId
, null);
570 if (!(ref
instanceof byte[]))
571 throw new IncorrectObjectTypeException(refId
, Constants
.TYPE_COMMIT
);
573 else if (item
.equals("")) {
574 ref
= mapObject(refId
, null);
575 if (ref
instanceof Tag
)
576 refId
= ((Tag
)ref
).getObjId();
582 throw new RevisionSyntaxException(revstr
);
584 throw new RevisionSyntaxException(revstr
);
587 ref
= mapObject(refId
, null);
588 if (ref
instanceof Commit
)
589 refId
= ((Commit
)ref
).getParentIds()[0];
591 throw new IncorrectObjectTypeException(refId
, Constants
.TYPE_COMMIT
);
595 ref
= mapObject(refId
, null);
596 if (ref
instanceof Commit
)
597 refId
= ((Commit
)ref
).getParentIds()[0];
599 throw new IncorrectObjectTypeException(refId
, Constants
.TYPE_COMMIT
);
604 String refstr
= new String(rev
,0,i
);
605 refId
= resolveSimple(refstr
);
606 ref
= mapCommit(refId
);
609 for (l
= i
+ 1; l
< rev
.length
; ++l
) {
610 if (!Character
.isDigit(rev
[l
]))
613 String distnum
= new String(rev
, i
+1, l
-i
-1);
614 int dist
= Integer
.parseInt(distnum
);
616 refId
= ((Commit
)ref
).getParentIds()[0];
617 ref
= mapCommit(refId
);
625 for (m
=i
+2; m
<rev
.length
; ++m
) {
627 time
= new String(rev
, i
+2, m
-i
-2);
632 throw new RevisionSyntaxException("reflogs not yet supported by revision parser yet", revstr
);
637 throw new RevisionSyntaxException(revstr
);
641 refId
= resolveSimple(revstr
);
645 private ObjectId
resolveSimple(final String revstr
) throws IOException
{
646 if (ObjectId
.isId(revstr
))
647 return ObjectId
.fromString(revstr
);
648 final Ref r
= refs
.readRef(revstr
);
649 return r
!= null ? r
.getObjectId() : null;
653 * Close all resources used by this repository
655 public void close() {
660 for (int k
= packs
.length
- 1; k
>= 0; k
--) {
663 packs
= new PackFile
[0];
667 * Add a single existing pack to the list of available pack files.
670 * path of the pack file to open.
672 * path of the corresponding index file.
673 * @throws IOException
674 * index file could not be opened, read, or is not recognized as
675 * a Git pack file index.
677 public void openPack(final File pack
, final File idx
) throws IOException
{
678 final String p
= pack
.getName();
679 final String i
= idx
.getName();
680 if (p
.length() != 50 || !p
.startsWith("pack-") || !p
.endsWith(".pack"))
681 throw new IllegalArgumentException("Not a valid pack " + pack
);
682 if (i
.length() != 49 || !i
.startsWith("pack-") || !i
.endsWith(".idx"))
683 throw new IllegalArgumentException("Not a valid pack " + idx
);
684 if (!p
.substring(0,45).equals(i
.substring(0,45)))
685 throw new IllegalArgumentException("Pack " + pack
686 + "does not match index " + idx
);
688 final PackFile
[] cur
= packs
;
689 final PackFile
[] arr
= new PackFile
[cur
.length
+ 1];
690 System
.arraycopy(cur
, 0, arr
, 1, cur
.length
);
691 arr
[0] = new PackFile(this, idx
, pack
);
696 * Scan the object dirs, including alternates for packs
699 public void scanForPacks() {
700 final ArrayList
<PackFile
> p
= new ArrayList
<PackFile
>();
701 for (int i
=0; i
<objectsDirs
.length
; ++i
)
702 scanForPacks(new File(objectsDirs
[i
], "pack"), p
);
703 final PackFile
[] arr
= new PackFile
[p
.size()];
708 private void scanForPacks(final File packDir
, Collection
<PackFile
> packList
) {
709 final String
[] idxList
= packDir
.list(new FilenameFilter() {
710 public boolean accept(final File baseDir
, final String n
) {
711 // Must match "pack-[0-9a-f]{40}.idx" to be an index.
712 return n
.length() == 49 && n
.endsWith(".idx")
713 && n
.startsWith("pack-");
716 if (idxList
!= null) {
717 for (final String indexName
: idxList
) {
718 final String n
= indexName
.substring(0, indexName
.length() - 4);
719 final File idxFile
= new File(packDir
, n
+ ".idx");
720 final File packFile
= new File(packDir
, n
+ ".pack");
722 packList
.add(new PackFile(this, idxFile
, packFile
));
723 } catch (IOException ioe
) {
724 // Whoops. That's not a pack!
726 ioe
.printStackTrace();
733 * Writes a symref (e.g. HEAD) to disk
735 * @param name symref name
736 * @param target pointed to ref
737 * @throws IOException
739 public void writeSymref(final String name
, final String target
)
741 refs
.link(name
, target
);
744 public String
toString() {
745 return "Repository[" + getDirectory() + "]";
749 * @return name of topmost Stacked Git patch.
750 * @throws IOException
752 public String
getPatch() throws IOException
{
753 final File ptr
= new File(getDirectory(),"patches/"+getBranch()+"/applied");
754 final BufferedReader br
= new BufferedReader(new FileReader(ptr
));
758 while ((line
=br
.readLine())!=null) {
768 * @return name of current branch
769 * @throws IOException
771 public String
getFullBranch() throws IOException
{
772 final File ptr
= new File(getDirectory(),"HEAD");
773 final BufferedReader br
= new BufferedReader(new FileReader(ptr
));
780 if (ref
.startsWith("ref: "))
781 ref
= ref
.substring(5);
786 * @return name of current branch.
787 * @throws IOException
789 public String
getBranch() throws IOException
{
791 final File ptr
= new File(getDirectory(), Constants
.HEAD
);
792 final BufferedReader br
= new BufferedReader(new FileReader(ptr
));
799 if (ref
.startsWith("ref: "))
800 ref
= ref
.substring(5);
801 if (ref
.startsWith("refs/heads/"))
802 ref
= ref
.substring(11);
804 } catch (FileNotFoundException e
) {
805 final File ptr
= new File(getDirectory(),"head-name");
806 final BufferedReader br
= new BufferedReader(new FileReader(ptr
));
818 * @return all known refs (heads, tags, remotes).
820 public Map
<String
, Ref
> getAllRefs() {
821 return refs
.getAllRefs();
825 * @return all tags; key is short tag name ("v1.0") and value of the entry
826 * contains the ref with the full tag name ("refs/tags/v1.0").
828 public Map
<String
, Ref
> getTags() {
829 return refs
.getTags();
833 * @return true if HEAD points to a StGit patch.
835 public boolean isStGitMode() {
837 File file
= new File(getDirectory(), "HEAD");
838 BufferedReader reader
= new BufferedReader(new FileReader(file
));
839 String string
= reader
.readLine();
840 if (!string
.startsWith("ref: refs/heads/"))
842 String branch
= string
.substring("ref: refs/heads/".length());
843 File currentPatches
= new File(new File(new File(getDirectory(),
844 "patches"), branch
), "applied");
845 if (!currentPatches
.exists())
847 if (currentPatches
.length() == 0)
851 } catch (IOException e
) {
858 * @return applied patches in a map indexed on current commit id
859 * @throws IOException
861 public Map
<ObjectId
,StGitPatch
> getAppliedPatches() throws IOException
{
862 Map
<ObjectId
,StGitPatch
> ret
= new HashMap
<ObjectId
,StGitPatch
>();
864 File patchDir
= new File(new File(getDirectory(),"patches"),getBranch());
865 BufferedReader apr
= new BufferedReader(new FileReader(new File(patchDir
,"applied")));
866 for (String patchName
=apr
.readLine(); patchName
!=null; patchName
=apr
.readLine()) {
867 File topFile
= new File(new File(new File(patchDir
,"patches"), patchName
), "top");
868 BufferedReader tfr
= new BufferedReader(new FileReader(topFile
));
869 String objectId
= tfr
.readLine();
870 ObjectId id
= ObjectId
.fromString(objectId
);
871 ret
.put(id
, new StGitPatch(patchName
, id
));
879 /** Clean up stale caches */
880 public void refreshFromDisk() {
885 * @return a representation of the index associated with this repo
886 * @throws IOException
888 public GitIndex
getIndex() throws IOException
{
890 index
= new GitIndex(this);
893 index
.rereadIfNecessary();
898 static byte[] gitInternalSlash(byte[] bytes
) {
899 if (File
.separatorChar
== '/')
901 for (int i
=0; i
<bytes
.length
; ++i
)
902 if (bytes
[i
] == File
.separatorChar
)
908 * @return an important state
910 public RepositoryState
getRepositoryState() {
911 if (new File(getWorkDir(), ".dotest").exists())
912 return RepositoryState
.REBASING
;
913 if (new File(gitDir
,".dotest-merge").exists())
914 return RepositoryState
.REBASING_INTERACTIVE
;
915 if (new File(gitDir
,"MERGE_HEAD").exists())
916 return RepositoryState
.MERGING
;
917 if (new File(gitDir
,"BISECT_LOG").exists())
918 return RepositoryState
.BISECTING
;
919 return RepositoryState
.SAFE
;
923 * Check validty of a ref name. It must not contain character that has
924 * a special meaning in a Git object reference expression. Some other
925 * dangerous characters are also excluded.
929 * @return true if refName is a valid ref name
931 public static boolean isValidRefName(final String refName
) {
932 final int len
= refName
.length();
934 for (int i
=0; i
<len
; ++i
) {
935 char c
= refName
.charAt(i
);
953 case '~': case '^': case ':':
965 * String work dir and return normalized repository path
968 * @param f File whose path shall be stripp off it's workdir
969 * @return normalized repository relative path
971 public static String
stripWorkDir(File wd
, File f
) {
972 String relName
= f
.getPath().substring(wd
.getPath().length() + 1);
973 relName
= relName
.replace(File
.separatorChar
, '/');
978 * @return the workdir file, i.e. where the files are checked out
980 public File
getWorkDir() {
981 return getDirectory().getParentFile();