James Moger
2016-01-25 252dc07d7f85cc344b5919bb7c6166ef84b2102e
commit | author | age
f13c4c 1 /*
JM 2  * Copyright 2011 gitblit.com.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
5fe7df 16 package com.gitblit.utils;
JM 17
18 import java.io.File;
19 import java.io.IOException;
0f47b2 20 import java.text.DecimalFormat;
a2709d 21 import java.text.MessageFormat;
5fe7df 22 import java.util.ArrayList;
168566 23 import java.util.Arrays;
5fe7df 24 import java.util.Collections;
JM 25 import java.util.Date;
26 import java.util.HashMap;
99f359 27 import java.util.Iterator;
5fe7df 28 import java.util.List;
JM 29 import java.util.Map;
2a7306 30 import java.util.Map.Entry;
46f33f 31 import java.util.regex.Matcher;
0adceb 32 import java.util.regex.Pattern;
5fe7df 33
e85277 34 import org.apache.commons.io.filefilter.TrueFileFilter;
168566 35 import org.eclipse.jgit.api.CloneCommand;
JM 36 import org.eclipse.jgit.api.FetchCommand;
f5d0ad 37 import org.eclipse.jgit.api.Git;
99f359 38 import org.eclipse.jgit.api.TagCommand;
5d9bd7 39 import org.eclipse.jgit.api.errors.GitAPIException;
87c3d7 40 import org.eclipse.jgit.diff.DiffEntry;
f5d0ad 41 import org.eclipse.jgit.diff.DiffEntry.ChangeType;
87c3d7 42 import org.eclipse.jgit.diff.DiffFormatter;
JM 43 import org.eclipse.jgit.diff.RawTextComparator;
5fe7df 44 import org.eclipse.jgit.errors.ConfigInvalidException;
98ce17 45 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
46f33f 46 import org.eclipse.jgit.errors.LargeObjectException;
98ce17 47 import org.eclipse.jgit.errors.MissingObjectException;
JM 48 import org.eclipse.jgit.errors.StopWalkException;
eb870f 49 import org.eclipse.jgit.lib.BlobBasedConfig;
7b0895 50 import org.eclipse.jgit.lib.CommitBuilder;
5fe7df 51 import org.eclipse.jgit.lib.Constants;
JM 52 import org.eclipse.jgit.lib.FileMode;
53 import org.eclipse.jgit.lib.ObjectId;
7b0895 54 import org.eclipse.jgit.lib.ObjectInserter;
5fe7df 55 import org.eclipse.jgit.lib.ObjectLoader;
JM 56 import org.eclipse.jgit.lib.PersonIdent;
57 import org.eclipse.jgit.lib.Ref;
7b0895 58 import org.eclipse.jgit.lib.RefUpdate;
JM 59 import org.eclipse.jgit.lib.RefUpdate.Result;
5fe7df 60 import org.eclipse.jgit.lib.Repository;
a125cf 61 import org.eclipse.jgit.lib.RepositoryCache.FileKey;
690070 62 import org.eclipse.jgit.lib.StoredConfig;
7b0895 63 import org.eclipse.jgit.lib.TreeFormatter;
d2ab7a 64 import org.eclipse.jgit.merge.MergeStrategy;
JM 65 import org.eclipse.jgit.merge.RecursiveMerger;
5fe7df 66 import org.eclipse.jgit.revwalk.RevBlob;
JM 67 import org.eclipse.jgit.revwalk.RevCommit;
68 import org.eclipse.jgit.revwalk.RevObject;
45c0d6 69 import org.eclipse.jgit.revwalk.RevSort;
5fe7df 70 import org.eclipse.jgit.revwalk.RevTree;
JM 71 import org.eclipse.jgit.revwalk.RevWalk;
2f1c77 72 import org.eclipse.jgit.revwalk.filter.CommitTimeRevFilter;
98ce17 73 import org.eclipse.jgit.revwalk.filter.RevFilter;
2b7682 74 import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
831469 75 import org.eclipse.jgit.transport.CredentialsProvider;
168566 76 import org.eclipse.jgit.transport.FetchResult;
JM 77 import org.eclipse.jgit.transport.RefSpec;
5fe7df 78 import org.eclipse.jgit.treewalk.TreeWalk;
f602a2 79 import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
c1c3c6 80 import org.eclipse.jgit.treewalk.filter.OrTreeFilter;
5fe7df 81 import org.eclipse.jgit.treewalk.filter.PathFilter;
JM 82 import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
c1c3c6 83 import org.eclipse.jgit.treewalk.filter.PathSuffixFilter;
87c3d7 84 import org.eclipse.jgit.treewalk.filter.TreeFilter;
a125cf 85 import org.eclipse.jgit.util.FS;
5fe7df 86 import org.slf4j.Logger;
JM 87 import org.slf4j.LoggerFactory;
88
46f33f 89 import com.gitblit.GitBlit;
d2ab7a 90 import com.gitblit.GitBlitException;
46f33f 91 import com.gitblit.manager.GitblitManager;
PM 92 import com.gitblit.models.FilestoreModel;
a125cf 93 import com.gitblit.models.GitNote;
1f9dae 94 import com.gitblit.models.PathModel;
f1720c 95 import com.gitblit.models.PathModel.PathChangeModel;
1f9dae 96 import com.gitblit.models.RefModel;
eb870f 97 import com.gitblit.models.SubmoduleModel;
46f33f 98 import com.gitblit.servlet.FilestoreServlet;
a59232 99 import com.google.common.base.Strings;
5fe7df 100
88598b 101 /**
JM 102  * Collection of static methods for retrieving information from a repository.
699e71 103  *
88598b 104  * @author James Moger
699e71 105  *
88598b 106  */
5fe7df 107 public class JGitUtils {
JM 108
424fe1 109     static final Logger LOGGER = LoggerFactory.getLogger(JGitUtils.class);
a2709d 110
JM 111     /**
112      * Log an error message and exception.
699e71 113      *
a2709d 114      * @param t
JM 115      * @param repository
116      *            if repository is not null it MUST be the {0} parameter in the
117      *            pattern.
118      * @param pattern
119      * @param objects
120      */
121     private static void error(Throwable t, Repository repository, String pattern, Object... objects) {
122         List<Object> parameters = new ArrayList<Object>();
123         if (objects != null && objects.length > 0) {
124             for (Object o : objects) {
125                 parameters.add(o);
126             }
127         }
128         if (repository != null) {
129             parameters.add(0, repository.getDirectory().getAbsolutePath());
130         }
131         LOGGER.error(MessageFormat.format(pattern, parameters.toArray()), t);
132     }
a125cf 133
ed21d2 134     /**
JM 135      * Returns the displayable name of the person in the form "Real Name <email
136      * address>".  If the email address is empty, just "Real Name" is returned.
699e71 137      *
ed21d2 138      * @param person
JM 139      * @return "Real Name <email address>" or "Real Name"
140      */
a125cf 141     public static String getDisplayName(PersonIdent person) {
JM 142         if (StringUtils.isEmpty(person.getEmailAddress())) {
143             return person.getName();
144         }
145         final StringBuilder r = new StringBuilder();
146         r.append(person.getName());
147         r.append(" <");
148         r.append(person.getEmailAddress());
149         r.append('>');
150         return r.toString().trim();
151     }
bc9d4a 152
ed21d2 153     /**
831469 154      * Encapsulates the result of cloning or pulling from a repository.
JM 155      */
156     public static class CloneResult {
f6740d 157         public String name;
831469 158         public FetchResult fetchResult;
JM 159         public boolean createdRepository;
160     }
161
162     /**
ed21d2 163      * Clone or Fetch a repository. If the local repository does not exist,
JM 164      * clone is called. If the repository does exist, fetch is called. By
165      * default the clone/fetch retrieves the remote heads, tags, and notes.
699e71 166      *
ed21d2 167      * @param repositoriesFolder
JM 168      * @param name
169      * @param fromUrl
831469 170      * @return CloneResult
ed21d2 171      * @throws Exception
JM 172      */
831469 173     public static CloneResult cloneRepository(File repositoriesFolder, String name, String fromUrl)
008322 174             throws Exception {
d7fb20 175         return cloneRepository(repositoriesFolder, name, fromUrl, true, null);
831469 176     }
JM 177
178     /**
179      * Clone or Fetch a repository. If the local repository does not exist,
180      * clone is called. If the repository does exist, fetch is called. By
181      * default the clone/fetch retrieves the remote heads, tags, and notes.
699e71 182      *
831469 183      * @param repositoriesFolder
JM 184      * @param name
185      * @param fromUrl
d7fb20 186      * @param bare
831469 187      * @param credentialsProvider
JM 188      * @return CloneResult
189      * @throws Exception
190      */
f6740d 191     public static CloneResult cloneRepository(File repositoriesFolder, String name, String fromUrl,
JM 192             boolean bare, CredentialsProvider credentialsProvider) throws Exception {
831469 193         CloneResult result = new CloneResult();
f6740d 194         if (bare) {
JM 195             // bare repository, ensure .git suffix
196             if (!name.toLowerCase().endsWith(Constants.DOT_GIT_EXT)) {
197                 name += Constants.DOT_GIT_EXT;
198             }
199         } else {
200             // normal repository, strip .git suffix
201             if (name.toLowerCase().endsWith(Constants.DOT_GIT_EXT)) {
202                 name = name.substring(0, name.indexOf(Constants.DOT_GIT_EXT));
203             }
168566 204         }
f6740d 205         result.name = name;
JM 206
168566 207         File folder = new File(repositoriesFolder, name);
JM 208         if (folder.exists()) {
209             File gitDir = FileKey.resolve(new File(repositoriesFolder, name), FS.DETECTED);
843c42 210             Repository repository = new FileRepositoryBuilder().setGitDir(gitDir).build();
831469 211             result.fetchResult = fetchRepository(credentialsProvider, repository);
168566 212             repository.close();
JM 213         } else {
214             CloneCommand clone = new CloneCommand();
d7fb20 215             clone.setBare(bare);
168566 216             clone.setCloneAllBranches(true);
JM 217             clone.setURI(fromUrl);
218             clone.setDirectory(folder);
831469 219             if (credentialsProvider != null) {
JM 220                 clone.setCredentialsProvider(credentialsProvider);
221             }
2ef0a3 222             Repository repository = clone.call().getRepository();
699e71 223
168566 224             // Now we have to fetch because CloneCommand doesn't fetch
JM 225             // refs/notes nor does it allow manual RefSpec.
831469 226             result.createdRepository = true;
JM 227             result.fetchResult = fetchRepository(credentialsProvider, repository);
168566 228             repository.close();
JM 229         }
230         return result;
231     }
232
ed21d2 233     /**
JM 234      * Fetch updates from the remote repository. If refSpecs is unspecifed,
235      * remote heads, tags, and notes are retrieved.
699e71 236      *
831469 237      * @param credentialsProvider
ed21d2 238      * @param repository
JM 239      * @param refSpecs
240      * @return FetchResult
241      * @throws Exception
242      */
831469 243     public static FetchResult fetchRepository(CredentialsProvider credentialsProvider,
JM 244             Repository repository, RefSpec... refSpecs) throws Exception {
168566 245         Git git = new Git(repository);
JM 246         FetchCommand fetch = git.fetch();
247         List<RefSpec> specs = new ArrayList<RefSpec>();
248         if (refSpecs == null || refSpecs.length == 0) {
249             specs.add(new RefSpec("+refs/heads/*:refs/remotes/origin/*"));
250             specs.add(new RefSpec("+refs/tags/*:refs/tags/*"));
251             specs.add(new RefSpec("+refs/notes/*:refs/notes/*"));
252         } else {
253             specs.addAll(Arrays.asList(refSpecs));
254         }
831469 255         if (credentialsProvider != null) {
JM 256             fetch.setCredentialsProvider(credentialsProvider);
257         }
168566 258         fetch.setRefSpecs(specs);
2548a7 259         FetchResult fetchRes = fetch.call();
JM 260         return fetchRes;
168566 261     }
JM 262
ed21d2 263     /**
JM 264      * Creates a bare repository.
699e71 265      *
ed21d2 266      * @param repositoriesFolder
JM 267      * @param name
268      * @return Repository
269      */
168566 270     public static Repository createRepository(File repositoriesFolder, String name) {
e85277 271         return createRepository(repositoriesFolder, name, "FALSE");
f5d0ad 272     }
5fe7df 273
8a67d9 274     /**
FZ 275      * Creates a bare, shared repository.
1dee25 276      *
8a67d9 277      * @param repositoriesFolder
FZ 278      * @param name
279      * @param shared
280      *          the setting for the --shared option of "git init".
281      * @return Repository
282      */
283     public static Repository createRepository(File repositoriesFolder, String name, String shared) {
5fe7df 284         try {
e85277 285             Repository repo = null;
FZ 286             try {
287                 Git git = Git.init().setDirectory(new File(repositoriesFolder, name)).setBare(true).call();
288                 repo = git.getRepository();
289             } catch (GitAPIException e) {
290                 throw new RuntimeException(e);
291             }
690070 292
8a67d9 293             GitConfigSharedRepository sharedRepository = new GitConfigSharedRepository(shared);
FZ 294             if (sharedRepository.isShared()) {
295                 StoredConfig config = repo.getConfig();
296                 config.setString("core", null, "sharedRepository", sharedRepository.getValue());
297                 config.setBoolean("receive", null, "denyNonFastforwards", true);
298                 config.save();
690070 299
8a67d9 300                 if (! JnaUtils.isWindows()) {
e85277 301                     Iterator<File> iter = org.apache.commons.io.FileUtils.iterateFilesAndDirs(repo.getDirectory(),
FZ 302                             TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE);
303                     // Adjust permissions on file/directory
304                     while (iter.hasNext()) {
305                         adjustSharedPerm(iter.next(), sharedRepository);
306                     }
8a67d9 307                 }
FZ 308             }
690070 309
8a67d9 310             return repo;
FZ 311         } catch (IOException e) {
ed21d2 312             throw new RuntimeException(e);
JM 313         }
314     }
b86562 315
e85277 316     private enum GitConfigSharedRepositoryValue
FZ 317     {
8a67d9 318         UMASK("0", 0), FALSE("0", 0), OFF("0", 0), NO("0", 0),
FZ 319         GROUP("1", 0660), TRUE("1", 0660), ON("1", 0660), YES("1", 0660),
320         ALL("2", 0664), WORLD("2", 0664), EVERYBODY("2", 0664),
321         Oxxx(null, -1);
690070 322
8a67d9 323         private String configValue;
FZ 324         private int permValue;
325         private GitConfigSharedRepositoryValue(String config, int perm) { configValue = config; permValue = perm; };
690070 326
8a67d9 327         public String getConfigValue() { return configValue; };
FZ 328         public int getPerm() { return permValue; };
690070 329
8a67d9 330     }
e85277 331
8a67d9 332     private static class GitConfigSharedRepository
FZ 333     {
334         private int intValue;
e85277 335         private GitConfigSharedRepositoryValue enumValue;
690070 336
e85277 337         GitConfigSharedRepository(String s) {
8a67d9 338             if ( s == null || s.trim().isEmpty() ) {
FZ 339                 enumValue = GitConfigSharedRepositoryValue.GROUP;
340             }
341             else {
342                 try {
343                     // Try one of the string values
344                     enumValue = GitConfigSharedRepositoryValue.valueOf(s.trim().toUpperCase());
345                 } catch (IllegalArgumentException  iae) {
346                     try {
347                         // Try if this is an octal number
348                         int i = Integer.parseInt(s, 8);
349                         if ( (i & 0600) != 0600 ) {
350                             String msg = String.format("Problem with core.sharedRepository filemode value (0%03o).\nThe owner of files must always have read and write permissions.", i);
351                             throw new IllegalArgumentException(msg);
352                         }
353                         intValue = i & 0666;
354                         enumValue = GitConfigSharedRepositoryValue.Oxxx;
355                     } catch (NumberFormatException nfe) {
356                         throw new IllegalArgumentException("Bad configuration value for 'shared': '" + s + "'");
357                     }
358                 }
359             }
360         }
690070 361
e85277 362         String getValue() {
8b5730 363             if ( enumValue == GitConfigSharedRepositoryValue.Oxxx ) {
FZ 364                 if (intValue == 0) return "0";
365                 return String.format("0%o", intValue);
366             }
8a67d9 367             return enumValue.getConfigValue();
FZ 368         }
690070 369
e85277 370         int getPerm() {
8a67d9 371             if ( enumValue == GitConfigSharedRepositoryValue.Oxxx ) return intValue;
FZ 372             return enumValue.getPerm();
373         }
374
e85277 375         boolean isCustom() {
FZ 376             return enumValue == GitConfigSharedRepositoryValue.Oxxx;
377         }
378
379         boolean isShared() {
8a67d9 380             return (enumValue.getPerm() > 0) || enumValue == GitConfigSharedRepositoryValue.Oxxx;
FZ 381         }
382     }
690070 383
FZ 384
1dee25 385     /**
FZ 386      * Adjust file permissions of a file/directory for shared repositories
387      *
388      * @param path
389      *             File that should get its permissions changed.
390      * @param configShared
391      *             Configuration string value for the shared mode.
392      * @return Upon successful completion, a value of 0 is returned. Otherwise, a value of -1 is returned.
393      */
e85277 394     public static int adjustSharedPerm(File path, String configShared) {
FZ 395         return adjustSharedPerm(path, new GitConfigSharedRepository(configShared));
396     }
397
398
1dee25 399     /**
FZ 400      * Adjust file permissions of a file/directory for shared repositories
401      *
402      * @param path
403      *             File that should get its permissions changed.
404      * @param configShared
405      *             Configuration setting for the shared mode.
406      * @return Upon successful completion, a value of 0 is returned. Otherwise, a value of -1 is returned.
407      */
e85277 408     public static int adjustSharedPerm(File path, GitConfigSharedRepository configShared) {
FZ 409         if (! configShared.isShared()) return 0;
1dee25 410         if (! path.exists()) return -1;
e85277 411
FZ 412         int perm = configShared.getPerm();
b72444 413         JnaUtils.Filestat stat = JnaUtils.getFilestat(path);
FZ 414         if (stat == null) return -1;
415         int mode = stat.mode;
e85277 416         if (mode < 0) return -1;
FZ 417
b72444 418         // Now, here is the kicker: Under Linux, chmod'ing a sgid file whose guid is different from the process'
FZ 419         // effective guid will reset the sgid flag of the file. Since there is no way to get the sgid flag back in
420         // that case, we decide to rather not touch is and getting the right permissions will have to be achieved
421         // in a different way, e.g. by using an appropriate umask for the Gitblit process.
422         if (System.getProperty("os.name").toLowerCase().startsWith("linux")) {
423             if ( ((mode & (JnaUtils.S_ISGID | JnaUtils.S_ISUID)) != 0)
424                 && stat.gid != JnaUtils.getegid() ) {
425                 LOGGER.debug("Not adjusting permissions to prevent clearing suid/sgid bits for '" + path + "'" );
426                 return 0;
427             }
428         }
429
e85277 430         // If the owner has no write access, delete it from group and other, too.
FZ 431         if ((mode & JnaUtils.S_IWUSR) == 0) perm &= ~0222;
432         // If the owner has execute access, set it for all blocks that have read access.
433         if ((mode & JnaUtils.S_IXUSR) == JnaUtils.S_IXUSR) perm |= (perm & 0444) >> 2;
434
435         if (configShared.isCustom()) {
436             // Use the custom value for access permissions.
8b5730 437             mode = (mode & ~0777) | perm;
e85277 438         }
FZ 439         else {
440             // Just add necessary bits to existing permissions.
441             mode |= perm;
442         }
443
444         if (path.isDirectory()) {
445             mode |= (mode & 0444) >> 2;
446             mode |= JnaUtils.S_ISGID;
447         }
448
449         return JnaUtils.setFilemode(path, mode);
450     }
451
452
ed21d2 453     /**
JM 454      * Returns a list of repository names in the specified folder.
699e71 455      *
ed21d2 456      * @param repositoriesFolder
b86562 457      * @param onlyBare
JM 458      *            if true, only bare repositories repositories are listed. If
459      *            false all repositories are included.
ed21d2 460      * @param searchSubfolders
JM 461      *            recurse into subfolders to find grouped repositories
65f55e 462      * @param depth
JM 463      *            optional recursion depth, -1 = infinite recursion
0adceb 464      * @param exclusions
JM 465      *            list of regex exclusions for matching to folder names
ed21d2 466      * @return list of repository names
JM 467      */
b86562 468     public static List<String> getRepositoryList(File repositoriesFolder, boolean onlyBare,
0adceb 469             boolean searchSubfolders, int depth, List<String> exclusions) {
5fe7df 470         List<String> list = new ArrayList<String>();
a125cf 471         if (repositoriesFolder == null || !repositoriesFolder.exists()) {
JM 472             return list;
473         }
bb55f5 474         List<Pattern> patterns = new ArrayList<Pattern>();
JM 475         if (!ArrayUtils.isEmpty(exclusions)) {
476             for (String regex : exclusions) {
477                 patterns.add(Pattern.compile(regex));
478             }
479         }
a125cf 480         list.addAll(getRepositoryList(repositoriesFolder.getAbsolutePath(), repositoriesFolder,
bb55f5 481                 onlyBare, searchSubfolders, depth, patterns));
94750e 482         StringUtils.sortRepositorynames(list);
f2c4fa 483         list.remove(".git"); // issue-256
5fe7df 484         return list;
JM 485     }
486
ed21d2 487     /**
JM 488      * Recursive function to find git repositories.
699e71 489      *
ed21d2 490      * @param basePath
JM 491      *            basePath is stripped from the repository name as repositories
492      *            are relative to this path
493      * @param searchFolder
b86562 494      * @param onlyBare
JM 495      *            if true only bare repositories will be listed. if false all
496      *            repositories are included.
ed21d2 497      * @param searchSubfolders
JM 498      *            recurse into subfolders to find grouped repositories
65f55e 499      * @param depth
JM 500      *            recursion depth, -1 = infinite recursion
bb55f5 501      * @param patterns
JM 502      *            list of regex patterns for matching to folder names
ed21d2 503      * @return
JM 504      */
a125cf 505     private static List<String> getRepositoryList(String basePath, File searchFolder,
bb55f5 506             boolean onlyBare, boolean searchSubfolders, int depth, List<Pattern> patterns) {
1aa6e0 507         File baseFile = new File(basePath);
5fe7df 508         List<String> list = new ArrayList<String>();
65f55e 509         if (depth == 0) {
JM 510             return list;
0adceb 511         }
699e71 512
65f55e 513         int nextDepth = (depth == -1) ? -1 : depth - 1;
a125cf 514         for (File file : searchFolder.listFiles()) {
JM 515             if (file.isDirectory()) {
0adceb 516                 boolean exclude = false;
JM 517                 for (Pattern pattern : patterns) {
518                     String path = FileUtils.getRelativePath(baseFile, file).replace('\\',  '/');
bb55f5 519                     if (pattern.matcher(path).matches()) {
0adceb 520                         LOGGER.debug(MessageFormat.format("excluding {0} because of rule {1}", path, pattern.pattern()));
JM 521                         exclude = true;
522                         break;
523                     }
524                 }
525                 if (exclude) {
526                     // skip to next file
527                     continue;
528                 }
529
a125cf 530                 File gitDir = FileKey.resolve(new File(searchFolder, file.getName()), FS.DETECTED);
JM 531                 if (gitDir != null) {
b86562 532                     if (onlyBare && gitDir.getName().equals(".git")) {
a125cf 533                         continue;
f5d0ad 534                     }
236001 535                     if (gitDir.equals(file) || gitDir.getParentFile().equals(file)) {
JM 536                         // determine repository name relative to base path
537                         String repository = FileUtils.getRelativePath(baseFile, file);
538                         list.add(repository);
539                     } else if (searchSubfolders && file.canRead()) {
540                         // look for repositories in subfolders
0adceb 541                         list.addAll(getRepositoryList(basePath, file, onlyBare, searchSubfolders,
bb55f5 542                                 nextDepth, patterns));
236001 543                     }
1c30da 544                 } else if (searchSubfolders && file.canRead()) {
a125cf 545                     // look for repositories in subfolders
0adceb 546                     list.addAll(getRepositoryList(basePath, file, onlyBare, searchSubfolders,
bb55f5 547                             nextDepth, patterns));
5fe7df 548                 }
JM 549             }
550         }
551         return list;
45c0d6 552     }
JM 553
ed21d2 554     /**
JM 555      * Returns the first commit on a branch. If the repository does not exist or
556      * is empty, null is returned.
699e71 557      *
ed21d2 558      * @param repository
JM 559      * @param branch
560      *            if unspecified, HEAD is assumed.
561      * @return RevCommit
562      */
563     public static RevCommit getFirstCommit(Repository repository, String branch) {
564         if (!hasCommits(repository)) {
bc9d4a 565             return null;
JM 566         }
a125cf 567         RevCommit commit = null;
45c0d6 568         try {
a2709d 569             // resolve branch
JM 570             ObjectId branchObject;
571             if (StringUtils.isEmpty(branch)) {
572                 branchObject = getDefaultBranch(repository);
573             } else {
574                 branchObject = repository.resolve(branch);
575             }
576
ed21d2 577             RevWalk walk = new RevWalk(repository);
45c0d6 578             walk.sort(RevSort.REVERSE);
a2709d 579             RevCommit head = walk.parseCommit(branchObject);
45c0d6 580             walk.markStart(head);
a125cf 581             commit = walk.next();
45c0d6 582             walk.dispose();
JM 583         } catch (Throwable t) {
a2709d 584             error(t, repository, "{0} failed to determine first commit");
45c0d6 585         }
a125cf 586         return commit;
45c0d6 587     }
JM 588
ed21d2 589     /**
JM 590      * Returns the date of the first commit on a branch. If the repository does
591      * not exist, Date(0) is returned. If the repository does exist bit is
592      * empty, the last modified date of the repository folder is returned.
699e71 593      *
ed21d2 594      * @param repository
JM 595      * @param branch
596      *            if unspecified, HEAD is assumed.
597      * @return Date of the first commit on a branch
598      */
599     public static Date getFirstChange(Repository repository, String branch) {
600         RevCommit commit = getFirstCommit(repository, branch);
f1720c 601         if (commit == null) {
ed21d2 602             if (repository == null || !repository.getDirectory().exists()) {
f1720c 603                 return new Date(0);
f5d0ad 604             }
f1720c 605             // fresh repository
ed21d2 606             return new Date(repository.getDirectory().lastModified());
45c0d6 607         }
f1720c 608         return getCommitDate(commit);
5fe7df 609     }
JM 610
ed21d2 611     /**
JM 612      * Determine if a repository has any commits. This is determined by checking
b1dba7 613      * the for loose and packed objects.
699e71 614      *
ed21d2 615      * @param repository
JM 616      * @return true if the repository has commits
617      */
618     public static boolean hasCommits(Repository repository) {
a2709d 619         if (repository != null && repository.getDirectory().exists()) {
b1dba7 620             return (new File(repository.getDirectory(), "objects").list().length > 2)
330247 621                     || (new File(repository.getDirectory(), "objects/pack").list().length > 0);
1f9dae 622         }
db653a 623         return false;
bc9d4a 624     }
699e71 625
5c5b7a 626     /**
JM 627      * Encapsulates the result of cloning or pulling from a repository.
628      */
629     public static class LastChange {
630         public Date when;
631         public String who;
699e71 632
5c5b7a 633         LastChange() {
699e71 634             when = new Date(0);
5c5b7a 635         }
699e71 636
5c5b7a 637         LastChange(long lastModified) {
JM 638             this.when = new Date(lastModified);
639         }
640     }
bc9d4a 641
ed21d2 642     /**
5c5b7a 643      * Returns the date and author of the most recent commit on a branch. If the
JM 644      * repository does not exist Date(0) is returned. If it does exist but is
645      * empty, the last modified date of the repository folder is returned.
699e71 646      *
ed21d2 647      * @param repository
5c5b7a 648      * @return a LastChange object
ed21d2 649      */
5c5b7a 650     public static LastChange getLastChange(Repository repository) {
ed21d2 651         if (!hasCommits(repository)) {
1f9dae 652             // null repository
ed21d2 653             if (repository == null) {
5c5b7a 654                 return new LastChange();
1f9dae 655             }
f5d0ad 656             // fresh repository
5c5b7a 657             return new LastChange(repository.getDirectory().lastModified());
f5d0ad 658         }
a2709d 659
4fea45 660         List<RefModel> branchModels = getLocalBranches(repository, true, -1);
JM 661         if (branchModels.size() > 0) {
662             // find most recent branch update
699e71 663             LastChange lastChange = new LastChange();
4fea45 664             for (RefModel branchModel : branchModels) {
5c5b7a 665                 if (branchModel.getDate().after(lastChange.when)) {
JM 666                     lastChange.when = branchModel.getDate();
667                     lastChange.who = branchModel.getAuthorIdent().getName();
4fea45 668                 }
JM 669             }
670             return lastChange;
671         }
699e71 672
4fea45 673         // default to the repository folder modification date
5c5b7a 674         return new LastChange(repository.getDirectory().lastModified());
5fe7df 675     }
JM 676
ed21d2 677     /**
JM 678      * Retrieves a Java Date from a Git commit.
699e71 679      *
ed21d2 680      * @param commit
a2709d 681      * @return date of the commit or Date(0) if the commit is null
ed21d2 682      */
a125cf 683     public static Date getCommitDate(RevCommit commit) {
a2709d 684         if (commit == null) {
JM 685             return new Date(0);
686         }
a125cf 687         return new Date(commit.getCommitTime() * 1000L);
JM 688     }
689
ed21d2 690     /**
098bcd 691      * Retrieves a Java Date from a Git commit.
699e71 692      *
098bcd 693      * @param commit
JM 694      * @return date of the commit or Date(0) if the commit is null
695      */
696     public static Date getAuthorDate(RevCommit commit) {
697         if (commit == null) {
698             return new Date(0);
699         }
a59232 700         if (commit.getAuthorIdent() != null) {
JM 701             return commit.getAuthorIdent().getWhen();
702         }
703         return getCommitDate(commit);
098bcd 704     }
JM 705
706     /**
ed21d2 707      * Returns the specified commit from the repository. If the repository does
JM 708      * not exist or is empty, null is returned.
699e71 709      *
ed21d2 710      * @param repository
JM 711      * @param objectId
712      *            if unspecified, HEAD is assumed.
713      * @return RevCommit
714      */
715     public static RevCommit getCommit(Repository repository, String objectId) {
716         if (!hasCommits(repository)) {
bc9d4a 717             return null;
JM 718         }
a125cf 719         RevCommit commit = null;
115551 720         RevWalk walk = null;
5fe7df 721         try {
a2709d 722             // resolve object id
JM 723             ObjectId branchObject;
a771f1 724             if (StringUtils.isEmpty(objectId) || "HEAD".equalsIgnoreCase(objectId)) {
a2709d 725                 branchObject = getDefaultBranch(repository);
JM 726             } else {
727                 branchObject = repository.resolve(objectId);
608ece 728             }
115551 729             if (branchObject == null) {
JM 730                 return null;
731             }
732             walk = new RevWalk(repository);
a2709d 733             RevCommit rev = walk.parseCommit(branchObject);
5fe7df 734             commit = rev;
JM 735         } catch (Throwable t) {
a2709d 736             error(t, repository, "{0} failed to get commit {1}", objectId);
115551 737         } finally {
JM 738             if (walk != null) {
739                 walk.dispose();
740             }
5fe7df 741         }
JM 742         return commit;
743     }
744
ed21d2 745     /**
JM 746      * Retrieves the raw byte content of a file in the specified tree.
699e71 747      *
ed21d2 748      * @param repository
JM 749      * @param tree
750      *            if null, the RevTree from HEAD is assumed.
751      * @param path
752      * @return content as a byte []
753      */
e4e682 754     public static byte[] getByteContent(Repository repository, RevTree tree, final String path, boolean throwError) {
ed21d2 755         RevWalk rw = new RevWalk(repository);
JM 756         TreeWalk tw = new TreeWalk(repository);
5fe7df 757         tw.setFilter(PathFilterGroup.createFromStrings(Collections.singleton(path)));
a125cf 758         byte[] content = null;
5fe7df 759         try {
4ab184 760             if (tree == null) {
a2709d 761                 ObjectId object = getDefaultBranch(repository);
02ede7 762                 if (object == null)
RR 763                     return null;
4ab184 764                 RevCommit commit = rw.parseCommit(object);
JM 765                 tree = commit.getTree();
a125cf 766             }
4ab184 767             tw.reset(tree);
5fe7df 768             while (tw.next()) {
JM 769                 if (tw.isSubtree() && !path.equals(tw.getPathString())) {
770                     tw.enterSubtree();
771                     continue;
772                 }
773                 ObjectId entid = tw.getObjectId(0);
774                 FileMode entmode = tw.getFileMode(0);
749110 775                 if (entmode != FileMode.GITLINK) {
2a5e65 776                     ObjectLoader ldr = repository.open(entid, Constants.OBJ_BLOB);
JM 777                     content = ldr.getCachedBytes();
a125cf 778                 }
5fe7df 779             }
JM 780         } catch (Throwable t) {
e4e682 781             if (throwError) {
JM 782                 error(t, repository, "{0} can't find {1} in tree {2}", path, tree.name());
783             }
5fe7df 784         } finally {
a125cf 785             rw.dispose();
a1cee6 786             tw.close();
5fe7df 787         }
a125cf 788         return content;
5fe7df 789     }
JM 790
ed21d2 791     /**
JM 792      * Returns the UTF-8 string content of a file in the specified tree.
699e71 793      *
ed21d2 794      * @param repository
JM 795      * @param tree
796      *            if null, the RevTree from HEAD is assumed.
797      * @param blobPath
ae9e15 798      * @param charsets optional
ed21d2 799      * @return UTF-8 string content
JM 800      */
ae9e15 801     public static String getStringContent(Repository repository, RevTree tree, String blobPath, String... charsets) {
e4e682 802         byte[] content = getByteContent(repository, tree, blobPath, true);
4ab184 803         if (content == null) {
JM 804             return null;
805         }
ae9e15 806         return StringUtils.decodeString(content, charsets);
4ab184 807     }
JM 808
ed21d2 809     /**
JM 810      * Gets the raw byte content of the specified blob object.
699e71 811      *
ed21d2 812      * @param repository
JM 813      * @param objectId
814      * @return byte [] blob content
815      */
816     public static byte[] getByteContent(Repository repository, String objectId) {
817         RevWalk rw = new RevWalk(repository);
4ab184 818         byte[] content = null;
JM 819         try {
820             RevBlob blob = rw.lookupBlob(ObjectId.fromString(objectId));
ed21d2 821             ObjectLoader ldr = repository.open(blob.getId(), Constants.OBJ_BLOB);
2a5e65 822             content = ldr.getCachedBytes();
4ab184 823         } catch (Throwable t) {
a2709d 824             error(t, repository, "{0} can't find blob {1}", objectId);
4ab184 825         } finally {
JM 826             rw.dispose();
827         }
828         return content;
829     }
830
ed21d2 831     /**
JM 832      * Gets the UTF-8 string content of the blob specified by objectId.
699e71 833      *
ed21d2 834      * @param repository
JM 835      * @param objectId
ae9e15 836      * @param charsets optional
ed21d2 837      * @return UTF-8 string content
JM 838      */
ae9e15 839     public static String getStringContent(Repository repository, String objectId, String... charsets) {
ed21d2 840         byte[] content = getByteContent(repository, objectId);
a125cf 841         if (content == null) {
JM 842             return null;
843         }
ae9e15 844         return StringUtils.decodeString(content, charsets);
5fe7df 845     }
JM 846
ed21d2 847     /**
JM 848      * Returns the list of files in the specified folder at the specified
849      * commit. If the repository does not exist or is empty, an empty list is
850      * returned.
699e71 851      *
ed21d2 852      * @param repository
JM 853      * @param path
854      *            if unspecified, root folder is assumed.
855      * @param commit
856      *            if null, HEAD is assumed.
857      * @return list of files in specified path
858      */
859     public static List<PathModel> getFilesInPath(Repository repository, String path,
860             RevCommit commit) {
5fe7df 861         List<PathModel> list = new ArrayList<PathModel>();
ed21d2 862         if (!hasCommits(repository)) {
f5d0ad 863             return list;
JM 864         }
a125cf 865         if (commit == null) {
a2709d 866             commit = getCommit(repository, null);
a125cf 867         }
ed21d2 868         final TreeWalk tw = new TreeWalk(repository);
5fe7df 869         try {
a125cf 870             tw.addTree(commit.getTree());
ed21d2 871             if (!StringUtils.isEmpty(path)) {
JM 872                 PathFilter f = PathFilter.create(path);
a125cf 873                 tw.setFilter(f);
JM 874                 tw.setRecursive(false);
5fe7df 875                 boolean foundFolder = false;
a125cf 876                 while (tw.next()) {
JM 877                     if (!foundFolder && tw.isSubtree()) {
878                         tw.enterSubtree();
5fe7df 879                     }
ed21d2 880                     if (tw.getPathString().equals(path)) {
5fe7df 881                         foundFolder = true;
JM 882                         continue;
883                     }
884                     if (foundFolder) {
ed21d2 885                         list.add(getPathModel(tw, path, commit));
5fe7df 886                     }
JM 887                 }
888             } else {
a125cf 889                 tw.setRecursive(false);
JM 890                 while (tw.next()) {
891                     list.add(getPathModel(tw, null, commit));
5fe7df 892                 }
JM 893             }
894         } catch (IOException e) {
a2709d 895             error(e, repository, "{0} failed to get files for commit {1}", commit.getName());
5fe7df 896         } finally {
a1cee6 897             tw.close();
5fe7df 898         }
JM 899         Collections.sort(list);
900         return list;
901     }
699e71 902
ed21d2 903     /**
a9a2ff 904      * Returns the list of files in the specified folder at the specified
MC 905      * commit. If the repository does not exist or is empty, an empty list is
906      * returned.
907      *
908      * This is modified version that implements path compression feature.
909      *
910      * @param repository
911      * @param path
912      *            if unspecified, root folder is assumed.
913      * @param commit
914      *            if null, HEAD is assumed.
915      * @return list of files in specified path
916      */
917     public static List<PathModel> getFilesInPath2(Repository repository, String path, RevCommit commit) {
918
919         List<PathModel> list = new ArrayList<PathModel>();
920         if (!hasCommits(repository)) {
921             return list;
922         }
923         if (commit == null) {
924             commit = getCommit(repository, null);
925         }
926         final TreeWalk tw = new TreeWalk(repository);
927         try {
928
929             tw.addTree(commit.getTree());
930             final boolean isPathEmpty = Strings.isNullOrEmpty(path);
931
932             if (!isPathEmpty) {
933                 PathFilter f = PathFilter.create(path);
934                 tw.setFilter(f);
935             }
936
937             tw.setRecursive(true);
938             List<String> paths = new ArrayList<>();
939
940             while (tw.next()) {
941                     String child = isPathEmpty ? tw.getPathString()
942                             : tw.getPathString().replaceFirst(String.format("%s/", path), "");
943                     paths.add(child);
944             }
945
946             for(String p: PathUtils.compressPaths(paths)) {
947                 String pathString = isPathEmpty ? p : String.format("%s/%s", path, p);
948                 list.add(getPathModel(repository, pathString, path, commit));
949             }
950
951         } catch (IOException e) {
952             error(e, repository, "{0} failed to get files for commit {1}", commit.getName());
953         } finally {
a1cee6 954             tw.close();
a9a2ff 955         }
MC 956         Collections.sort(list);
957         return list;
958     }
959
960     /**
ed21d2 961      * Returns the list of files changed in a specified commit. If the
JM 962      * repository does not exist or is empty, an empty list is returned.
699e71 963      *
ed21d2 964      * @param repository
JM 965      * @param commit
966      *            if null, HEAD is assumed.
967      * @return list of files changed in a commit
968      */
969     public static List<PathChangeModel> getFilesInCommit(Repository repository, RevCommit commit) {
319342 970         return getFilesInCommit(repository, commit, true);
JM 971     }
972
973     /**
974      * Returns the list of files changed in a specified commit. If the
975      * repository does not exist or is empty, an empty list is returned.
699e71 976      *
319342 977      * @param repository
JM 978      * @param commit
979      *            if null, HEAD is assumed.
980      * @param calculateDiffStat
981      *            if true, each PathChangeModel will have insertions/deletions
982      * @return list of files changed in a commit
983      */
984     public static List<PathChangeModel> getFilesInCommit(Repository repository, RevCommit commit, boolean calculateDiffStat) {
9bc17d 985         List<PathChangeModel> list = new ArrayList<PathChangeModel>();
ed21d2 986         if (!hasCommits(repository)) {
JM 987             return list;
988         }
989         RevWalk rw = new RevWalk(repository);
5fe7df 990         try {
a125cf 991             if (commit == null) {
a2709d 992                 ObjectId object = getDefaultBranch(repository);
a125cf 993                 commit = rw.parseCommit(object);
85c2e6 994             }
5fe7df 995
f1720c 996             if (commit.getParentCount() == 0) {
ed21d2 997                 TreeWalk tw = new TreeWalk(repository);
5450d0 998                 tw.reset();
JM 999                 tw.setRecursive(true);
1000                 tw.addTree(commit.getTree());
a125cf 1001                 while (tw.next()) {
46f33f 1002                     long size = 0;
PM 1003                     FilestoreModel filestoreItem = null;
1004                     ObjectId objectId = tw.getObjectId(0);
1005                     
1006                     try {
1007                         if (!tw.isSubtree() && (tw.getFileMode(0) != FileMode.GITLINK)) {
1008
1009                             size = tw.getObjectReader().getObjectSize(objectId, Constants.OBJ_BLOB);
1010
1011                             if (isPossibleFilestoreItem(size)) {
1012                                 filestoreItem = getFilestoreItem(tw.getObjectReader().open(objectId));
1013                             }
1014                         }
1015                     } catch (Throwable t) {
1016                         error(t, null, "failed to retrieve blob size for " + tw.getPathString());
1017                     }
1018                     
1019                     list.add(new PathChangeModel(tw.getPathString(), tw.getPathString(),filestoreItem, size, tw
1020                             .getRawMode(0), objectId.getName(), commit.getId().getName(),
eb870f 1021                             ChangeType.ADD));
f1720c 1022                 }
a1cee6 1023                 tw.close();
f1720c 1024             } else {
JM 1025                 RevCommit parent = rw.parseCommit(commit.getParent(0).getId());
46f33f 1026                 DiffStatFormatter df = new DiffStatFormatter(commit.getName(), repository);
ed21d2 1027                 df.setRepository(repository);
5450d0 1028                 df.setDiffComparator(RawTextComparator.DEFAULT);
f1720c 1029                 df.setDetectRenames(true);
5450d0 1030                 List<DiffEntry> diffs = df.scan(parent.getTree(), commit.getTree());
f1720c 1031                 for (DiffEntry diff : diffs) {
319342 1032                     // create the path change model
46f33f 1033                     PathChangeModel pcm = PathChangeModel.from(diff, commit.getName(), repository);
PM 1034                         
1035                         if (calculateDiffStat) {
319342 1036                         // update file diffstats
JM 1037                         df.format(diff);
1038                         PathChangeModel pathStat = df.getDiffStat().getPath(pcm.path);
1039                         if (pathStat != null) {
1040                             pcm.insertions = pathStat.insertions;
1041                             pcm.deletions = pathStat.deletions;
1042                         }
f1720c 1043                     }
319342 1044                     list.add(pcm);
9bc17d 1045                 }
5fe7df 1046             }
87c3d7 1047         } catch (Throwable t) {
a2709d 1048             error(t, repository, "{0} failed to determine files in commit!");
a125cf 1049         } finally {
85c2e6 1050             rw.dispose();
5fe7df 1051         }
c1c3c6 1052         return list;
JM 1053     }
bc9d4a 1054
ed21d2 1055     /**
80d532 1056      * Returns the list of files changed in a specified commit. If the
JM 1057      * repository does not exist or is empty, an empty list is returned.
699e71 1058      *
80d532 1059      * @param repository
JM 1060      * @param startCommit
1061      *            earliest commit
1062      * @param endCommit
1063      *            most recent commit. if null, HEAD is assumed.
1064      * @return list of files changed in a commit range
1065      */
131da2 1066     public static List<PathChangeModel> getFilesInRange(Repository repository, String startCommit, String endCommit) {
JM 1067         List<PathChangeModel> list = new ArrayList<PathChangeModel>();
1068         if (!hasCommits(repository)) {
1069             return list;
1070         }
1071         try {
1072             ObjectId startRange = repository.resolve(startCommit);
1073             ObjectId endRange = repository.resolve(endCommit);
1074             RevWalk rw = new RevWalk(repository);
1075             RevCommit start = rw.parseCommit(startRange);
1076             RevCommit end = rw.parseCommit(endRange);
1077             list.addAll(getFilesInRange(repository, start, end));
a1cee6 1078             rw.close();
131da2 1079         } catch (Throwable t) {
JM 1080             error(t, repository, "{0} failed to determine files in range {1}..{2}!", startCommit, endCommit);
1081         }
1082         return list;
1083     }
1084
1085     /**
1086      * Returns the list of files changed in a specified commit. If the
1087      * repository does not exist or is empty, an empty list is returned.
1088      *
1089      * @param repository
1090      * @param startCommit
1091      *            earliest commit
1092      * @param endCommit
1093      *            most recent commit. if null, HEAD is assumed.
1094      * @return list of files changed in a commit range
1095      */
80d532 1096     public static List<PathChangeModel> getFilesInRange(Repository repository, RevCommit startCommit, RevCommit endCommit) {
JM 1097         List<PathChangeModel> list = new ArrayList<PathChangeModel>();
1098         if (!hasCommits(repository)) {
1099             return list;
1100         }
1101         try {
1102             DiffFormatter df = new DiffFormatter(null);
1103             df.setRepository(repository);
1104             df.setDiffComparator(RawTextComparator.DEFAULT);
1105             df.setDetectRenames(true);
1106
1107             List<DiffEntry> diffEntries = df.scan(startCommit.getTree(), endCommit.getTree());
1108             for (DiffEntry diff : diffEntries) {
46f33f 1109                 PathChangeModel pcm = PathChangeModel.from(diff,  endCommit.getName(), repository);
319342 1110                 list.add(pcm);
699e71 1111             }
80d532 1112             Collections.sort(list);
JM 1113         } catch (Throwable t) {
1114             error(t, repository, "{0} failed to determine files in range {1}..{2}!", startCommit, endCommit);
1115         }
1116         return list;
1117     }
1118     /**
0f43a5 1119      * Returns the list of files in the repository on the default branch that
JM 1120      * match one of the specified extensions. This is a CASE-SENSITIVE search.
1121      * If the repository does not exist or is empty, an empty list is returned.
699e71 1122      *
ed21d2 1123      * @param repository
JM 1124      * @param extensions
1125      * @return list of files in repository with a matching extension
1126      */
1127     public static List<PathModel> getDocuments(Repository repository, List<String> extensions) {
0f43a5 1128         return getDocuments(repository, extensions, null);
JM 1129     }
1130
1131     /**
1132      * Returns the list of files in the repository in the specified commit that
1133      * match one of the specified extensions. This is a CASE-SENSITIVE search.
1134      * If the repository does not exist or is empty, an empty list is returned.
699e71 1135      *
0f43a5 1136      * @param repository
JM 1137      * @param extensions
1138      * @param objectId
1139      * @return list of files in repository with a matching extension
1140      */
1141     public static List<PathModel> getDocuments(Repository repository, List<String> extensions,
1142             String objectId) {
c1c3c6 1143         List<PathModel> list = new ArrayList<PathModel>();
ed21d2 1144         if (!hasCommits(repository)) {
JM 1145             return list;
1146         }
0f43a5 1147         RevCommit commit = getCommit(repository, objectId);
ed21d2 1148         final TreeWalk tw = new TreeWalk(repository);
c1c3c6 1149         try {
a125cf 1150             tw.addTree(commit.getTree());
c1c3c6 1151             if (extensions != null && extensions.size() > 0) {
0f43a5 1152                 List<TreeFilter> suffixFilters = new ArrayList<TreeFilter>();
bc9d4a 1153                 for (String extension : extensions) {
c1c3c6 1154                     if (extension.charAt(0) == '.') {
1feb04 1155                         suffixFilters.add(PathSuffixFilter.create(extension));
c1c3c6 1156                     } else {
JM 1157                         // escape the . since this is a regexp filter
1feb04 1158                         suffixFilters.add(PathSuffixFilter.create("." + extension));
c1c3c6 1159                     }
JM 1160                 }
0f43a5 1161                 TreeFilter filter;
JM 1162                 if (suffixFilters.size() == 1) {
1163                     filter = suffixFilters.get(0);
1164                 } else {
1165                     filter = OrTreeFilter.create(suffixFilters);
1166                 }
a125cf 1167                 tw.setFilter(filter);
JM 1168                 tw.setRecursive(true);
1169             }
1170             while (tw.next()) {
1171                 list.add(getPathModel(tw, null, commit));
c1c3c6 1172             }
JM 1173         } catch (IOException e) {
a2709d 1174             error(e, repository, "{0} failed to get documents for commit {1}", commit.getName());
c1c3c6 1175         } finally {
a1cee6 1176             tw.close();
c1c3c6 1177         }
JM 1178         Collections.sort(list);
5fe7df 1179         return list;
87c3d7 1180     }
JM 1181
ed21d2 1182     /**
JM 1183      * Returns a path model of the current file in the treewalk.
699e71 1184      *
ed21d2 1185      * @param tw
JM 1186      * @param basePath
1187      * @param commit
1188      * @return a path model of the current file in the treewalk
1189      */
a125cf 1190     private static PathModel getPathModel(TreeWalk tw, String basePath, RevCommit commit) {
5fe7df 1191         String name;
JM 1192         long size = 0;
46f33f 1193         
a125cf 1194         if (StringUtils.isEmpty(basePath)) {
JM 1195             name = tw.getPathString();
5fe7df 1196         } else {
a125cf 1197             name = tw.getPathString().substring(basePath.length() + 1);
5fe7df 1198         }
eb870f 1199         ObjectId objectId = tw.getObjectId(0);
46f33f 1200         FilestoreModel filestoreItem = null;
PM 1201         
5fe7df 1202         try {
eb870f 1203             if (!tw.isSubtree() && (tw.getFileMode(0) != FileMode.GITLINK)) {
46f33f 1204
eb870f 1205                 size = tw.getObjectReader().getObjectSize(objectId, Constants.OBJ_BLOB);
46f33f 1206
PM 1207                 if (isPossibleFilestoreItem(size)) {
1208                     filestoreItem = getFilestoreItem(tw.getObjectReader().open(objectId));
1209                 }
5fe7df 1210             }
JM 1211         } catch (Throwable t) {
a2709d 1212             error(t, null, "failed to retrieve blob size for " + tw.getPathString());
5fe7df 1213         }
46f33f 1214         return new PathModel(name, tw.getPathString(), filestoreItem, size, tw.getFileMode(0).getBits(),
eb870f 1215                 objectId.getName(), commit.getName());
46f33f 1216     }
PM 1217     
1218     public static boolean isPossibleFilestoreItem(long size) {
1219         return (   (size >= com.gitblit.Constants.LEN_FILESTORE_META_MIN) 
1220                 && (size <= com.gitblit.Constants.LEN_FILESTORE_META_MAX));
1221     }
1222     
1223     /**
1224      * 
1225      * @return Representative FilestoreModel if valid, otherwise null
1226      */
1227     public static FilestoreModel getFilestoreItem(ObjectLoader obj){
1228         try {
1229             final byte[] blob = obj.getCachedBytes(com.gitblit.Constants.LEN_FILESTORE_META_MAX);
1230             final String meta = new String(blob, "UTF-8");
1231         
1232             return FilestoreModel.fromMetaString(meta);
1233
1234         } catch (LargeObjectException e) {
1235             //Intentionally failing silent
1236         } catch (Exception e) {
1237             error(e, null, "failed to retrieve filestoreItem " + obj.toString());
1238         }
1239         
1240         return null;
5fe7df 1241     }
a9a2ff 1242
MC 1243     /**
1244      * Returns a path model by path string
1245      *
1246      * @param repo
1247      * @param path
1248      * @param filter
1249      * @param commit
1250      * @return a path model of the specified object
1251      */
1252     private static PathModel getPathModel(Repository repo, String path, String filter, RevCommit commit)
1253             throws IOException {
1254
1255         long size = 0;
46f33f 1256         FilestoreModel filestoreItem = null;
a9a2ff 1257         TreeWalk tw = TreeWalk.forPath(repo, path, commit.getTree());
MC 1258         String pathString = path;
1259
46f33f 1260         if (!tw.isSubtree() && (tw.getFileMode(0) != FileMode.GITLINK)) {
a9a2ff 1261
46f33f 1262             pathString = PathUtils.getLastPathComponent(pathString);
PM 1263             
1264             size = tw.getObjectReader().getObjectSize(tw.getObjectId(0), Constants.OBJ_BLOB);
1265             
1266             if (isPossibleFilestoreItem(size)) {
1267                 filestoreItem = getFilestoreItem(tw.getObjectReader().open(tw.getObjectId(0)));
1268             }
1269         } else if (tw.isSubtree()) {
a9a2ff 1270
46f33f 1271             // do not display dirs that are behind in the path
PM 1272             if (!Strings.isNullOrEmpty(filter)) {
1273                 pathString = path.replaceFirst(filter + "/", "");
a9a2ff 1274             }
MC 1275
46f33f 1276             // remove the last slash from path in displayed link
PM 1277             if (pathString != null && pathString.charAt(pathString.length()-1) == '/') {
1278                 pathString = pathString.substring(0, pathString.length()-1);
1279             }
1280         }
a9a2ff 1281
46f33f 1282         return new PathModel(pathString, tw.getPathString(), filestoreItem, size, tw.getFileMode(0).getBits(),
PM 1283                 tw.getObjectId(0).getName(), commit.getName());
a9a2ff 1284
MC 1285     }
1286
5fe7df 1287
ed21d2 1288     /**
JM 1289      * Returns a permissions representation of the mode bits.
699e71 1290      *
ed21d2 1291      * @param mode
JM 1292      * @return string representation of the mode bits
1293      */
5fe7df 1294     public static String getPermissionsFromMode(int mode) {
JM 1295         if (FileMode.TREE.equals(mode)) {
1296             return "drwxr-xr-x";
1297         } else if (FileMode.REGULAR_FILE.equals(mode)) {
1298             return "-rw-r--r--";
1299         } else if (FileMode.EXECUTABLE_FILE.equals(mode)) {
1300             return "-rwxr-xr-x";
1301         } else if (FileMode.SYMLINK.equals(mode)) {
1302             return "symlink";
1303         } else if (FileMode.GITLINK.equals(mode)) {
eb870f 1304             return "submodule";
5fe7df 1305         }
a125cf 1306         return "missing";
5fe7df 1307     }
JM 1308
ed21d2 1309     /**
2f1c77 1310      * Returns a list of commits since the minimum date starting from the
JM 1311      * specified object id.
699e71 1312      *
2f1c77 1313      * @param repository
JM 1314      * @param objectId
1315      *            if unspecified, HEAD is assumed.
1316      * @param minimumDate
1317      * @return list of commits
1318      */
1319     public static List<RevCommit> getRevLog(Repository repository, String objectId, Date minimumDate) {
1320         List<RevCommit> list = new ArrayList<RevCommit>();
1321         if (!hasCommits(repository)) {
1322             return list;
1323         }
1324         try {
1325             // resolve branch
1326             ObjectId branchObject;
1327             if (StringUtils.isEmpty(objectId)) {
1328                 branchObject = getDefaultBranch(repository);
1329             } else {
1330                 branchObject = repository.resolve(objectId);
1331             }
1332
1333             RevWalk rw = new RevWalk(repository);
1334             rw.markStart(rw.parseCommit(branchObject));
1335             rw.setRevFilter(CommitTimeRevFilter.after(minimumDate));
1336             Iterable<RevCommit> revlog = rw;
1337             for (RevCommit rev : revlog) {
1338                 list.add(rev);
1339             }
1340             rw.dispose();
1341         } catch (Throwable t) {
1342             error(t, repository, "{0} failed to get {1} revlog for minimum date {2}", objectId,
1343                     minimumDate);
1344         }
1345         return list;
1346     }
1347
1348     /**
ed21d2 1349      * Returns a list of commits starting from HEAD and working backwards.
699e71 1350      *
ed21d2 1351      * @param repository
JM 1352      * @param maxCount
1353      *            if < 0, all commits for the repository are returned.
1354      * @return list of commits
1355      */
1356     public static List<RevCommit> getRevLog(Repository repository, int maxCount) {
a2709d 1357         return getRevLog(repository, null, 0, maxCount);
ef5c58 1358     }
JM 1359
ed21d2 1360     /**
JM 1361      * Returns a list of commits starting from the specified objectId using an
1362      * offset and maxCount for paging. This is similar to LIMIT n OFFSET p in
1363      * SQL. If the repository does not exist or is empty, an empty list is
1364      * returned.
699e71 1365      *
ed21d2 1366      * @param repository
JM 1367      * @param objectId
1368      *            if unspecified, HEAD is assumed.
1369      * @param offset
1370      * @param maxCount
1371      *            if < 0, all commits are returned.
1372      * @return a paged list of commits
1373      */
1374     public static List<RevCommit> getRevLog(Repository repository, String objectId, int offset,
2a7306 1375             int maxCount) {
ed21d2 1376         return getRevLog(repository, objectId, null, offset, maxCount);
JM 1377     }
1378
1379     /**
1380      * Returns a list of commits for the repository or a path within the
1381      * repository. Caller may specify ending revision with objectId. Caller may
1382      * specify offset and maxCount to achieve pagination of results. If the
1383      * repository does not exist or is empty, an empty list is returned.
699e71 1384      *
ed21d2 1385      * @param repository
JM 1386      * @param objectId
1387      *            if unspecified, HEAD is assumed.
1388      * @param path
1389      *            if unspecified, commits for repository are returned. If
1390      *            specified, commits for the path are returned.
1391      * @param offset
1392      * @param maxCount
1393      *            if < 0, all commits are returned.
1394      * @return a paged list of commits
1395      */
1396     public static List<RevCommit> getRevLog(Repository repository, String objectId, String path,
1397             int offset, int maxCount) {
5fe7df 1398         List<RevCommit> list = new ArrayList<RevCommit>();
85c2e6 1399         if (maxCount == 0) {
JM 1400             return list;
1401         }
ed21d2 1402         if (!hasCommits(repository)) {
bc9d4a 1403             return list;
JM 1404         }
5fe7df 1405         try {
a2709d 1406             // resolve branch
80d532 1407             ObjectId startRange = null;
JM 1408             ObjectId endRange;
a125cf 1409             if (StringUtils.isEmpty(objectId)) {
80d532 1410                 endRange = getDefaultBranch(repository);
a2709d 1411             } else {
80d532 1412                 if( objectId.contains("..") ) {
JM 1413                     // range expression
1414                     String[] parts = objectId.split("\\.\\.");
1415                     startRange = repository.resolve(parts[0]);
1416                     endRange = repository.resolve(parts[1]);
1417                 } else {
1418                     // objectid
1419                     endRange= repository.resolve(objectId);
1420                 }
ef5c58 1421             }
80d532 1422             if (endRange == null) {
4fea45 1423                 return list;
JM 1424             }
a2709d 1425
ed21d2 1426             RevWalk rw = new RevWalk(repository);
80d532 1427             rw.markStart(rw.parseCommit(endRange));
JM 1428             if (startRange != null) {
699e71 1429                 rw.markUninteresting(rw.parseCommit(startRange));
80d532 1430             }
f602a2 1431             if (!StringUtils.isEmpty(path)) {
2a7306 1432                 TreeFilter filter = AndTreeFilter.create(
JM 1433                         PathFilterGroup.createFromStrings(Collections.singleton(path)),
1434                         TreeFilter.ANY_DIFF);
a125cf 1435                 rw.setTreeFilter(filter);
f602a2 1436             }
a125cf 1437             Iterable<RevCommit> revlog = rw;
ef5c58 1438             if (offset > 0) {
JM 1439                 int count = 0;
1440                 for (RevCommit rev : revlog) {
1441                     count++;
1442                     if (count > offset) {
1443                         list.add(rev);
1444                         if (maxCount > 0 && list.size() == maxCount) {
1445                             break;
1446                         }
1447                     }
1448                 }
1449             } else {
1450                 for (RevCommit rev : revlog) {
1451                     list.add(rev);
1452                     if (maxCount > 0 && list.size() == maxCount) {
1453                         break;
1454                     }
5fe7df 1455                 }
JM 1456             }
a125cf 1457             rw.dispose();
5fe7df 1458         } catch (Throwable t) {
a2709d 1459             error(t, repository, "{0} failed to get {1} revlog for path {2}", objectId, path);
5fe7df 1460         }
JM 1461         return list;
1462     }
1463
88598b 1464     /**
f47590 1465      * Returns a list of commits for the repository within the range specified
JM 1466      * by startRangeId and endRangeId. If the repository does not exist or is
1467      * empty, an empty list is returned.
699e71 1468      *
f47590 1469      * @param repository
JM 1470      * @param startRangeId
1471      *            the first commit (not included in results)
1472      * @param endRangeId
1473      *            the end commit (included in results)
1474      * @return a list of commits
1475      */
1476     public static List<RevCommit> getRevLog(Repository repository, String startRangeId,
1477             String endRangeId) {
1478         List<RevCommit> list = new ArrayList<RevCommit>();
1479         if (!hasCommits(repository)) {
1480             return list;
1481         }
1482         try {
1483             ObjectId endRange = repository.resolve(endRangeId);
1484             ObjectId startRange = repository.resolve(startRangeId);
1485
1486             RevWalk rw = new RevWalk(repository);
1487             rw.markStart(rw.parseCommit(endRange));
5386a2 1488             if (startRange.equals(ObjectId.zeroId())) {
JM 1489                 // maybe this is a tag or an orphan branch
1490                 list.add(rw.parseCommit(endRange));
1491                 rw.dispose();
1492                 return list;
1493             } else {
1494                 rw.markUninteresting(rw.parseCommit(startRange));
1495             }
f47590 1496
JM 1497             Iterable<RevCommit> revlog = rw;
1498             for (RevCommit rev : revlog) {
1499                 list.add(rev);
1500             }
1501             rw.dispose();
1502         } catch (Throwable t) {
1503             error(t, repository, "{0} failed to get revlog for {1}..{2}", startRangeId, endRangeId);
1504         }
1505         return list;
1506     }
1507
1508     /**
ed21d2 1509      * Search the commit history for a case-insensitive match to the value.
JM 1510      * Search results require a specified SearchType of AUTHOR, COMMITTER, or
1511      * COMMIT. Results may be paginated using offset and maxCount. If the
1512      * repository does not exist or is empty, an empty list is returned.
699e71 1513      *
ed21d2 1514      * @param repository
JM 1515      * @param objectId
1516      *            if unspecified, HEAD is assumed.
1517      * @param value
1518      * @param type
1519      *            AUTHOR, COMMITTER, COMMIT
1520      * @param offset
1521      * @param maxCount
1522      *            if < 0, all matches are returned
1523      * @return matching list of commits
1524      */
1525     public static List<RevCommit> searchRevlogs(Repository repository, String objectId,
33d8d8 1526             String value, final com.gitblit.Constants.SearchType type, int offset, int maxCount) {
98ce17 1527         List<RevCommit> list = new ArrayList<RevCommit>();
36491b 1528         if (StringUtils.isEmpty(value)) {
JM 1529             return list;
1530         }
85c2e6 1531         if (maxCount == 0) {
JM 1532             return list;
1533         }
ed21d2 1534         if (!hasCommits(repository)) {
bc9d4a 1535             return list;
JM 1536         }
36491b 1537         final String lcValue = value.toLowerCase();
98ce17 1538         try {
a2709d 1539             // resolve branch
JM 1540             ObjectId branchObject;
a125cf 1541             if (StringUtils.isEmpty(objectId)) {
a2709d 1542                 branchObject = getDefaultBranch(repository);
JM 1543             } else {
1544                 branchObject = repository.resolve(objectId);
98ce17 1545             }
a2709d 1546
ed21d2 1547             RevWalk rw = new RevWalk(repository);
a125cf 1548             rw.setRevFilter(new RevFilter() {
98ce17 1549
JM 1550                 @Override
1551                 public RevFilter clone() {
ed21d2 1552                     // FindBugs complains about this method name.
JM 1553                     // This is part of JGit design and unrelated to Cloneable.
98ce17 1554                     return this;
JM 1555                 }
1556
1557                 @Override
2a7306 1558                 public boolean include(RevWalk walker, RevCommit commit) throws StopWalkException,
JM 1559                         MissingObjectException, IncorrectObjectTypeException, IOException {
a125cf 1560                     boolean include = false;
98ce17 1561                     switch (type) {
JM 1562                     case AUTHOR:
a125cf 1563                         include = (commit.getAuthorIdent().getName().toLowerCase().indexOf(lcValue) > -1)
2a7306 1564                                 || (commit.getAuthorIdent().getEmailAddress().toLowerCase()
JM 1565                                         .indexOf(lcValue) > -1);
a125cf 1566                         break;
98ce17 1567                     case COMMITTER:
a125cf 1568                         include = (commit.getCommitterIdent().getName().toLowerCase()
JM 1569                                 .indexOf(lcValue) > -1)
2a7306 1570                                 || (commit.getCommitterIdent().getEmailAddress().toLowerCase()
JM 1571                                         .indexOf(lcValue) > -1);
a125cf 1572                         break;
98ce17 1573                     case COMMIT:
a125cf 1574                         include = commit.getFullMessage().toLowerCase().indexOf(lcValue) > -1;
JM 1575                         break;
98ce17 1576                     }
a125cf 1577                     return include;
98ce17 1578                 }
JM 1579
1580             });
a2709d 1581             rw.markStart(rw.parseCommit(branchObject));
a125cf 1582             Iterable<RevCommit> revlog = rw;
98ce17 1583             if (offset > 0) {
JM 1584                 int count = 0;
1585                 for (RevCommit rev : revlog) {
1586                     count++;
1587                     if (count > offset) {
1588                         list.add(rev);
1589                         if (maxCount > 0 && list.size() == maxCount) {
1590                             break;
1591                         }
1592                     }
1593                 }
1594             } else {
1595                 for (RevCommit rev : revlog) {
1596                     list.add(rev);
1597                     if (maxCount > 0 && list.size() == maxCount) {
1598                         break;
1599                     }
1600                 }
1601             }
a125cf 1602             rw.dispose();
98ce17 1603         } catch (Throwable t) {
a2709d 1604             error(t, repository, "{0} failed to {1} search revlogs for {2}", type.name(), value);
98ce17 1605         }
JM 1606         return list;
a2709d 1607     }
JM 1608
1609     /**
1610      * Returns the default branch to use for a repository. Normally returns
1611      * whatever branch HEAD points to, but if HEAD points to nothing it returns
1612      * the most recently updated branch.
699e71 1613      *
a2709d 1614      * @param repository
JM 1615      * @return the objectid of a branch
1616      * @throws Exception
1617      */
1618     public static ObjectId getDefaultBranch(Repository repository) throws Exception {
1619         ObjectId object = repository.resolve(Constants.HEAD);
1620         if (object == null) {
1621             // no HEAD
1622             // perhaps non-standard repository, try local branches
1623             List<RefModel> branchModels = getLocalBranches(repository, true, -1);
1624             if (branchModels.size() > 0) {
1625                 // use most recently updated branch
1626                 RefModel branch = null;
1627                 Date lastDate = new Date(0);
1628                 for (RefModel branchModel : branchModels) {
1629                     if (branchModel.getDate().after(lastDate)) {
1630                         branch = branchModel;
1631                         lastDate = branch.getDate();
1632                     }
1633                 }
1634                 object = branch.getReferencedObjectId();
1635             }
1636         }
1637         return object;
98ce17 1638     }
JM 1639
ed21d2 1640     /**
2cdb73 1641      * Returns the target of the symbolic HEAD reference for a repository.
PLM 1642      * Normally returns a branch reference name, but when HEAD is detached,
1643      * the commit is matched against the known tags. The most recent matching
1644      * tag ref name will be returned if it references the HEAD commit. If
1645      * no match is found, the SHA1 is returned.
912938 1646      *
PLM 1647      * @param repository
90b8d7 1648      * @return the ref name or the SHA1 for a detached HEAD
912938 1649      */
90b8d7 1650     public static String getHEADRef(Repository repository) {
2cdb73 1651         String target = null;
912938 1652         try {
2cdb73 1653             target = repository.getFullBranch();
912938 1654         } catch (Throwable t) {
2cdb73 1655             error(t, repository, "{0} failed to get symbolic HEAD target");
912938 1656         }
2cdb73 1657         return target;
912938 1658     }
699e71 1659
912938 1660     /**
90b8d7 1661      * Sets the symbolic ref HEAD to the specified target ref. The
JM 1662      * HEAD will be detached if the target ref is not a branch.
912938 1663      *
PLM 1664      * @param repository
90b8d7 1665      * @param targetRef
JM 1666      * @return true if successful
912938 1667      */
90b8d7 1668     public static boolean setHEADtoRef(Repository repository, String targetRef) {
912938 1669         try {
90b8d7 1670              // detach HEAD if target ref is not a branch
JM 1671             boolean detach = !targetRef.startsWith(Constants.R_HEADS);
30f9d2 1672             RefUpdate.Result result;
PLM 1673             RefUpdate head = repository.updateRef(Constants.HEAD, detach);
1674             if (detach) { // Tag
90b8d7 1675                 RevCommit commit = getCommit(repository, targetRef);
30f9d2 1676                 head.setNewObjectId(commit.getId());
PLM 1677                 result = head.forceUpdate();
1678             } else {
90b8d7 1679                 result = head.link(targetRef);
30f9d2 1680             }
PLM 1681             switch (result) {
1682             case NEW:
1683             case FORCED:
1684             case NO_CHANGE:
1685             case FAST_FORWARD:
699e71 1686                 return true;
30f9d2 1687             default:
90b8d7 1688                 LOGGER.error(MessageFormat.format("{0} HEAD update to {1} returned result {2}",
JM 1689                         repository.getDirectory().getAbsolutePath(), targetRef, result));
30f9d2 1690             }
PLM 1691         } catch (Throwable t) {
90b8d7 1692             error(t, repository, "{0} failed to set HEAD to {1}", targetRef);
912938 1693         }
90b8d7 1694         return false;
912938 1695     }
699e71 1696
2cdb73 1697     /**
f96d32 1698      * Sets the local branch ref to point to the specified commit id.
JM 1699      *
1700      * @param repository
1701      * @param branch
1702      * @param commitId
1703      * @return true if successful
1704      */
1705     public static boolean setBranchRef(Repository repository, String branch, String commitId) {
1706         String branchName = branch;
5404d4 1707         if (!branchName.startsWith(Constants.R_REFS)) {
f96d32 1708             branchName = Constants.R_HEADS + branch;
JM 1709         }
1710
1711         try {
1712             RefUpdate refUpdate = repository.updateRef(branchName, false);
1713             refUpdate.setNewObjectId(ObjectId.fromString(commitId));
1714             RefUpdate.Result result = refUpdate.forceUpdate();
1715
1716             switch (result) {
1717             case NEW:
1718             case FORCED:
1719             case NO_CHANGE:
1720             case FAST_FORWARD:
699e71 1721                 return true;
f96d32 1722             default:
JM 1723                 LOGGER.error(MessageFormat.format("{0} {1} update to {2} returned result {3}",
1724                         repository.getDirectory().getAbsolutePath(), branchName, commitId, result));
1725             }
1726         } catch (Throwable t) {
1727             error(t, repository, "{0} failed to set {1} to {2}", branchName, commitId);
1728         }
1729         return false;
1730     }
699e71 1731
f96d32 1732     /**
JM 1733      * Deletes the specified branch ref.
699e71 1734      *
f96d32 1735      * @param repository
JM 1736      * @param branch
1737      * @return true if successful
1738      */
1739     public static boolean deleteBranchRef(Repository repository, String branch) {
1740         String branchName = branch;
1741         if (!branchName.startsWith(Constants.R_HEADS)) {
1742             branchName = Constants.R_HEADS + branch;
1743         }
1744
1745         try {
1746             RefUpdate refUpdate = repository.updateRef(branchName, false);
1747             refUpdate.setForceUpdate(true);
1748             RefUpdate.Result result = refUpdate.delete();
1749             switch (result) {
1750             case NEW:
1751             case FORCED:
1752             case NO_CHANGE:
1753             case FAST_FORWARD:
699e71 1754                 return true;
f96d32 1755             default:
JM 1756                 LOGGER.error(MessageFormat.format("{0} failed to delete to {1} returned result {2}",
1757                         repository.getDirectory().getAbsolutePath(), branchName, result));
1758             }
1759         } catch (Throwable t) {
1760             error(t, repository, "{0} failed to delete {1}", branchName);
1761         }
1762         return false;
1763     }
699e71 1764
f96d32 1765     /**
90b8d7 1766      * Get the full branch and tag ref names for any potential HEAD targets.
2cdb73 1767      *
PLM 1768      * @param repository
1769      * @return a list of ref names
1770      */
1771     public static List<String> getAvailableHeadTargets(Repository repository) {
1772         List<String> targets = new ArrayList<String>();
90b8d7 1773         for (RefModel branchModel : JGitUtils.getLocalBranches(repository, true, -1)) {
JM 1774             targets.add(branchModel.getName());
2cdb73 1775         }
90b8d7 1776
JM 1777         for (RefModel tagModel : JGitUtils.getTags(repository, true, -1)) {
1778             targets.add(tagModel.getName());
2cdb73 1779         }
PLM 1780         return targets;
1781     }
912938 1782
PLM 1783     /**
ed21d2 1784      * Returns all refs grouped by their associated object id.
699e71 1785      *
ed21d2 1786      * @param repository
JM 1787      * @return all refs grouped by their referenced object id
1788      */
1789     public static Map<ObjectId, List<RefModel>> getAllRefs(Repository repository) {
1e1b85 1790         return getAllRefs(repository, true);
JM 1791     }
699e71 1792
1e1b85 1793     /**
JM 1794      * Returns all refs grouped by their associated object id.
699e71 1795      *
1e1b85 1796      * @param repository
JM 1797      * @param includeRemoteRefs
1798      * @return all refs grouped by their referenced object id
1799      */
1800     public static Map<ObjectId, List<RefModel>> getAllRefs(Repository repository, boolean includeRemoteRefs) {
ed21d2 1801         List<RefModel> list = getRefs(repository, org.eclipse.jgit.lib.RefDatabase.ALL, true, -1);
JM 1802         Map<ObjectId, List<RefModel>> refs = new HashMap<ObjectId, List<RefModel>>();
1803         for (RefModel ref : list) {
1e1b85 1804             if (!includeRemoteRefs && ref.getName().startsWith(Constants.R_REMOTES)) {
JM 1805                 continue;
1806             }
ed21d2 1807             ObjectId objectid = ref.getReferencedObjectId();
JM 1808             if (!refs.containsKey(objectid)) {
1809                 refs.put(objectid, new ArrayList<RefModel>());
1810             }
1811             refs.get(objectid).add(ref);
1812         }
1813         return refs;
5fe7df 1814     }
JM 1815
ed21d2 1816     /**
JM 1817      * Returns the list of tags in the repository. If repository does not exist
1818      * or is empty, an empty list is returned.
699e71 1819      *
ed21d2 1820      * @param repository
JM 1821      * @param fullName
1822      *            if true, /refs/tags/yadayadayada is returned. If false,
1823      *            yadayadayada is returned.
1824      * @param maxCount
1825      *            if < 0, all tags are returned
1826      * @return list of tags
1827      */
1828     public static List<RefModel> getTags(Repository repository, boolean fullName, int maxCount) {
1829         return getRefs(repository, Constants.R_TAGS, fullName, maxCount);
232890 1830     }
JM 1831
ed21d2 1832     /**
f76fee 1833      * Returns the list of tags in the repository. If repository does not exist
GS 1834      * or is empty, an empty list is returned.
1835      *
1836      * @param repository
1837      * @param fullName
1838      *            if true, /refs/tags/yadayadayada is returned. If false,
1839      *            yadayadayada is returned.
1840      * @param maxCount
1841      *            if < 0, all tags are returned
1842      * @param offset
1843      *            if maxCount provided sets the starting point of the records to return
1844      * @return list of tags
1845      */
1846     public static List<RefModel> getTags(Repository repository, boolean fullName, int maxCount, int offset) {
1847         return getRefs(repository, Constants.R_TAGS, fullName, maxCount, offset);
1848     }
1849
1850     /**
ed21d2 1851      * Returns the list of local branches in the repository. If repository does
JM 1852      * not exist or is empty, an empty list is returned.
699e71 1853      *
ed21d2 1854      * @param repository
JM 1855      * @param fullName
1856      *            if true, /refs/heads/yadayadayada is returned. If false,
1857      *            yadayadayada is returned.
1858      * @param maxCount
1859      *            if < 0, all local branches are returned
1860      * @return list of local branches
1861      */
1862     public static List<RefModel> getLocalBranches(Repository repository, boolean fullName,
1863             int maxCount) {
1864         return getRefs(repository, Constants.R_HEADS, fullName, maxCount);
5fe7df 1865     }
JM 1866
ed21d2 1867     /**
JM 1868      * Returns the list of remote branches in the repository. If repository does
1869      * not exist or is empty, an empty list is returned.
699e71 1870      *
ed21d2 1871      * @param repository
JM 1872      * @param fullName
1873      *            if true, /refs/remotes/yadayadayada is returned. If false,
1874      *            yadayadayada is returned.
1875      * @param maxCount
1876      *            if < 0, all remote branches are returned
1877      * @return list of remote branches
1878      */
1879     public static List<RefModel> getRemoteBranches(Repository repository, boolean fullName,
1880             int maxCount) {
1881         return getRefs(repository, Constants.R_REMOTES, fullName, maxCount);
a125cf 1882     }
JM 1883
ed21d2 1884     /**
JM 1885      * Returns the list of note branches. If repository does not exist or is
1886      * empty, an empty list is returned.
699e71 1887      *
ed21d2 1888      * @param repository
JM 1889      * @param fullName
1890      *            if true, /refs/notes/yadayadayada is returned. If false,
1891      *            yadayadayada is returned.
1892      * @param maxCount
1893      *            if < 0, all note branches are returned
1894      * @return list of note branches
1895      */
1896     public static List<RefModel> getNoteBranches(Repository repository, boolean fullName,
1897             int maxCount) {
1898         return getRefs(repository, Constants.R_NOTES, fullName, maxCount);
1899     }
699e71 1900
f8bb95 1901     /**
699e71 1902      * Returns the list of refs in the specified base ref. If repository does
f8bb95 1903      * not exist or is empty, an empty list is returned.
699e71 1904      *
f8bb95 1905      * @param repository
JM 1906      * @param fullName
1907      *            if true, /refs/yadayadayada is returned. If false,
1908      *            yadayadayada is returned.
1909      * @return list of refs
1910      */
1911     public static List<RefModel> getRefs(Repository repository, String baseRef) {
1912         return getRefs(repository, baseRef, true, -1);
1913     }
ed21d2 1914
JM 1915     /**
1916      * Returns a list of references in the repository matching "refs". If the
1917      * repository is null or empty, an empty list is returned.
699e71 1918      *
ed21d2 1919      * @param repository
JM 1920      * @param refs
1921      *            if unspecified, all refs are returned
1922      * @param fullName
1923      *            if true, /refs/something/yadayadayada is returned. If false,
1924      *            yadayadayada is returned.
1925      * @param maxCount
1926      *            if < 0, all references are returned
1927      * @return list of references
1928      */
1929     private static List<RefModel> getRefs(Repository repository, String refs, boolean fullName,
1930             int maxCount) {
f76fee 1931         return getRefs(repository, refs, fullName, maxCount, 0);
GS 1932     }
1933
1934     /**
1935      * Returns a list of references in the repository matching "refs". If the
1936      * repository is null or empty, an empty list is returned.
1937      *
1938      * @param repository
1939      * @param refs
1940      *            if unspecified, all refs are returned
1941      * @param fullName
1942      *            if true, /refs/something/yadayadayada is returned. If false,
1943      *            yadayadayada is returned.
1944      * @param maxCount
1945      *            if < 0, all references are returned
1946      * @param offset
1947      *            if maxCount provided sets the starting point of the records to return
1948      * @return list of references
1949      */
1950     private static List<RefModel> getRefs(Repository repository, String refs, boolean fullName,
1951             int maxCount, int offset) {
5fe7df 1952         List<RefModel> list = new ArrayList<RefModel>();
85c2e6 1953         if (maxCount == 0) {
JM 1954             return list;
1955         }
ed21d2 1956         if (!hasCommits(repository)) {
JM 1957             return list;
1958         }
5fe7df 1959         try {
ed21d2 1960             Map<String, Ref> map = repository.getRefDatabase().getRefs(refs);
JM 1961             RevWalk rw = new RevWalk(repository);
2a7306 1962             for (Entry<String, Ref> entry : map.entrySet()) {
JM 1963                 Ref ref = entry.getValue();
4ab184 1964                 RevObject object = rw.parseAny(ref.getObjectId());
5cc4f2 1965                 String name = entry.getKey();
JM 1966                 if (fullName && !StringUtils.isEmpty(refs)) {
1967                     name = refs + name;
1968                 }
1969                 list.add(new RefModel(name, ref, object));
5fe7df 1970             }
4ab184 1971             rw.dispose();
5fe7df 1972             Collections.sort(list);
JM 1973             Collections.reverse(list);
1974             if (maxCount > 0 && list.size() > maxCount) {
f76fee 1975                 if (offset < 0) {
GS 1976                     offset = 0;
1977                 }
1978                 int endIndex = offset + maxCount;
1979                 if (endIndex > list.size()) {
1980                     endIndex = list.size();
1981                 }
1982                 list = new ArrayList<RefModel>(list.subList(offset, endIndex));
5fe7df 1983             }
JM 1984         } catch (IOException e) {
a2709d 1985             error(e, repository, "{0} failed to retrieve {1}", refs);
5fe7df 1986         }
JM 1987         return list;
1988     }
1989
ed21d2 1990     /**
11924d 1991      * Returns a RefModel for the gh-pages branch in the repository. If the
JM 1992      * branch can not be found, null is returned.
699e71 1993      *
11924d 1994      * @param repository
JM 1995      * @return a refmodel for the gh-pages branch or null
1996      */
1997     public static RefModel getPagesBranch(Repository repository) {
cc1cd7 1998         return getBranch(repository, "gh-pages");
JM 1999     }
2000
2001     /**
2002      * Returns a RefModel for a specific branch name in the repository. If the
2003      * branch can not be found, null is returned.
699e71 2004      *
cc1cd7 2005      * @param repository
JM 2006      * @return a refmodel for the branch or null
2007      */
2008     public static RefModel getBranch(Repository repository, String name) {
2009         RefModel branch = null;
11924d 2010         try {
cc1cd7 2011             // search for the branch in local heads
11924d 2012             for (RefModel ref : JGitUtils.getLocalBranches(repository, false, -1)) {
8744a1 2013                 if (ref.reference.getName().endsWith(name)) {
cc1cd7 2014                     branch = ref;
11924d 2015                     break;
JM 2016                 }
2017             }
2018
cc1cd7 2019             // search for the branch in remote heads
JM 2020             if (branch == null) {
11924d 2021                 for (RefModel ref : JGitUtils.getRemoteBranches(repository, false, -1)) {
8744a1 2022                     if (ref.reference.getName().endsWith(name)) {
cc1cd7 2023                         branch = ref;
11924d 2024                         break;
JM 2025                     }
2026                 }
2027             }
2028         } catch (Throwable t) {
cc1cd7 2029             LOGGER.error(MessageFormat.format("Failed to find {0} branch!", name), t);
11924d 2030         }
cc1cd7 2031         return branch;
11924d 2032     }
699e71 2033
eb870f 2034     /**
JM 2035      * Returns the list of submodules for this repository.
699e71 2036      *
eb870f 2037      * @param repository
JM 2038      * @param commit
2039      * @return list of submodules
2040      */
2041     public static List<SubmoduleModel> getSubmodules(Repository repository, String commitId) {
2042         RevCommit commit = getCommit(repository, commitId);
2043         return getSubmodules(repository, commit.getTree());
2044     }
699e71 2045
eb870f 2046     /**
JM 2047      * Returns the list of submodules for this repository.
699e71 2048      *
eb870f 2049      * @param repository
JM 2050      * @param commit
2051      * @return list of submodules
2052      */
2053     public static List<SubmoduleModel> getSubmodules(Repository repository, RevTree tree) {
2054         List<SubmoduleModel> list = new ArrayList<SubmoduleModel>();
e4e682 2055         byte [] blob = getByteContent(repository, tree, ".gitmodules", false);
eb870f 2056         if (blob == null) {
JM 2057             return list;
2058         }
2059         try {
2060             BlobBasedConfig config = new BlobBasedConfig(repository.getConfig(), blob);
2061             for (String module : config.getSubsections("submodule")) {
2062                 String path = config.getString("submodule", module, "path");
2063                 String url = config.getString("submodule", module, "url");
2064                 list.add(new SubmoduleModel(module, path, url));
2065             }
2066         } catch (ConfigInvalidException e) {
2067             LOGGER.error("Failed to load .gitmodules file for " + repository.getDirectory(), e);
2068         }
2069         return list;
2070     }
699e71 2071
eb870f 2072     /**
JM 2073      * Returns the submodule definition for the specified path at the specified
2074      * commit.  If no module is defined for the path, null is returned.
699e71 2075      *
eb870f 2076      * @param repository
JM 2077      * @param commit
2078      * @param path
2079      * @return a submodule definition or null if there is no submodule
2080      */
2081     public static SubmoduleModel getSubmoduleModel(Repository repository, String commitId, String path) {
2082         for (SubmoduleModel model : getSubmodules(repository, commitId)) {
2083             if (model.path.equals(path)) {
2084                 return model;
2085             }
2086         }
2087         return null;
2088     }
699e71 2089
657a65 2090     public static String getSubmoduleCommitId(Repository repository, String path, RevCommit commit) {
JM 2091         String commitId = null;
2092         RevWalk rw = new RevWalk(repository);
2093         TreeWalk tw = new TreeWalk(repository);
2094         tw.setFilter(PathFilterGroup.createFromStrings(Collections.singleton(path)));
2095         try {
2096             tw.reset(commit.getTree());
2097             while (tw.next()) {
2098                 if (tw.isSubtree() && !path.equals(tw.getPathString())) {
2099                     tw.enterSubtree();
2100                     continue;
2101                 }
2102                 if (FileMode.GITLINK == tw.getFileMode(0)) {
2103                     commitId = tw.getObjectId(0).getName();
2104                     break;
2105                 }
2106             }
2107         } catch (Throwable t) {
2108             error(t, repository, "{0} can't find {1} in commit {2}", path, commit.name());
2109         } finally {
2110             rw.dispose();
a1cee6 2111             tw.close();
657a65 2112         }
JM 2113         return commitId;
2114     }
11924d 2115
JM 2116     /**
ed21d2 2117      * Returns the list of notes entered about the commit from the refs/notes
JM 2118      * namespace. If the repository does not exist or is empty, an empty list is
2119      * returned.
699e71 2120      *
ed21d2 2121      * @param repository
JM 2122      * @param commit
2123      * @return list of notes
2124      */
a125cf 2125     public static List<GitNote> getNotesOnCommit(Repository repository, RevCommit commit) {
JM 2126         List<GitNote> list = new ArrayList<GitNote>();
ed21d2 2127         if (!hasCommits(repository)) {
JM 2128             return list;
2129         }
2130         List<RefModel> noteBranches = getNoteBranches(repository, true, -1);
2131         for (RefModel notesRef : noteBranches) {
4ab184 2132             RevTree notesTree = JGitUtils.getCommit(repository, notesRef.getName()).getTree();
9357e9 2133             // flat notes list
JM 2134             String notePath = commit.getName();
2135             String text = getStringContent(repository, notesTree, notePath);
2136             if (!StringUtils.isEmpty(text)) {
2137                 List<RevCommit> history = getRevLog(repository, notesRef.getName(), notePath, 0, -1);
2138                 RefModel noteRef = new RefModel(notesRef.displayName, null, history.get(history
2139                         .size() - 1));
2140                 GitNote gitNote = new GitNote(noteRef, text);
2141                 list.add(gitNote);
2142                 continue;
2143             }
699e71 2144
9357e9 2145             // folder structure
a125cf 2146             StringBuilder sb = new StringBuilder(commit.getName());
JM 2147             sb.insert(2, '/');
9357e9 2148             notePath = sb.toString();
JM 2149             text = getStringContent(repository, notesTree, notePath);
a125cf 2150             if (!StringUtils.isEmpty(text)) {
4ab184 2151                 List<RevCommit> history = getRevLog(repository, notesRef.getName(), notePath, 0, -1);
JM 2152                 RefModel noteRef = new RefModel(notesRef.displayName, null, history.get(history
2153                         .size() - 1));
2154                 GitNote gitNote = new GitNote(noteRef, text);
a125cf 2155                 list.add(gitNote);
5fe7df 2156             }
JM 2157         }
a125cf 2158         return list;
5fe7df 2159     }
0f47b2 2160
99f359 2161     /**
S 2162      * this method creates an incremental revision number as a tag according to
0f47b2 2163      * the amount of already existing tags, which start with a defined prefix.
699e71 2164      *
99f359 2165      * @param repository
S 2166      * @param objectId
0f47b2 2167      * @param tagger
JM 2168      * @param prefix
2169      * @param intPattern
2170      * @param message
99f359 2171      * @return true if operation was successful, otherwise false
S 2172      */
0f47b2 2173     public static boolean createIncrementalRevisionTag(Repository repository,
JM 2174             String objectId, PersonIdent tagger, String prefix, String intPattern, String message) {
99f359 2175         boolean result = false;
S 2176         Iterator<Entry<String, Ref>> iterator = repository.getTags().entrySet().iterator();
0f47b2 2177         long lastRev = 0;
99f359 2178         while (iterator.hasNext()) {
S 2179             Entry<String, Ref> entry = iterator.next();
0f47b2 2180             if (entry.getKey().startsWith(prefix)) {
JM 2181                 try {
2182                     long val = Long.parseLong(entry.getKey().substring(prefix.length()));
2183                     if (val > lastRev) {
2184                         lastRev = val;
2185                     }
2186                 } catch (Exception e) {
2187                     // this tag is NOT an incremental revision tag
2188                 }
99f359 2189             }
S 2190         }
0f47b2 2191         DecimalFormat df = new DecimalFormat(intPattern);
JM 2192         result = createTag(repository, objectId, tagger, prefix + df.format((lastRev + 1)), message);
99f359 2193         return result;
S 2194     }
5fe7df 2195
ed21d2 2196     /**
99f359 2197      * creates a tag in a repository
699e71 2198      *
99f359 2199      * @param repository
S 2200      * @param objectId, the ref the tag points towards
0f47b2 2201      * @param tagger, the person tagging the object
JM 2202      * @param tag, the string label
2203      * @param message, the string message
99f359 2204      * @return boolean, true if operation was successful, otherwise false
S 2205      */
0f47b2 2206     public static boolean createTag(Repository repository, String objectId, PersonIdent tagger, String tag, String message) {
699e71 2207         try {
99f359 2208             Git gitClient = Git.open(repository.getDirectory());
S 2209             TagCommand tagCommand = gitClient.tag();
0f47b2 2210             tagCommand.setTagger(tagger);
JM 2211             tagCommand.setMessage(message);
99f359 2212             if (objectId != null) {
S 2213                 RevObject revObj = getCommit(repository, objectId);
2214                 tagCommand.setObjectId(revObj);
2215             }
2216             tagCommand.setName(tag);
699e71 2217             Ref call = tagCommand.call();
99f359 2218             return call != null ? true : false;
S 2219         } catch (Exception e) {
0f47b2 2220             error(e, repository, "Failed to create tag {1} in repository {0}", objectId, tag);
99f359 2221         }
S 2222         return false;
2223     }
699e71 2224
99f359 2225     /**
7b0895 2226      * Create an orphaned branch in a repository.
699e71 2227      *
a2709d 2228      * @param repository
7b0895 2229      * @param branchName
JM 2230      * @param author
2231      *            if unspecified, Gitblit will be the author of this new branch
2232      * @return true if successful
a2709d 2233      */
7b0895 2234     public static boolean createOrphanBranch(Repository repository, String branchName,
JM 2235             PersonIdent author) {
2236         boolean success = false;
2237         String message = "Created branch " + branchName;
2238         if (author == null) {
2239             author = new PersonIdent("Gitblit", "gitblit@localhost");
2240         }
2241         try {
2242             ObjectInserter odi = repository.newObjectInserter();
2243             try {
2244                 // Create a blob object to insert into a tree
2245                 ObjectId blobId = odi.insert(Constants.OBJ_BLOB,
2246                         message.getBytes(Constants.CHARACTER_ENCODING));
2247
2248                 // Create a tree object to reference from a commit
2249                 TreeFormatter tree = new TreeFormatter();
69a559 2250                 tree.append(".branch", FileMode.REGULAR_FILE, blobId);
7b0895 2251                 ObjectId treeId = odi.insert(tree);
JM 2252
2253                 // Create a commit object
2254                 CommitBuilder commit = new CommitBuilder();
2255                 commit.setAuthor(author);
2256                 commit.setCommitter(author);
2257                 commit.setEncoding(Constants.CHARACTER_ENCODING);
2258                 commit.setMessage(message);
2259                 commit.setTreeId(treeId);
2260
2261                 // Insert the commit into the repository
2262                 ObjectId commitId = odi.insert(commit);
2263                 odi.flush();
2264
2265                 RevWalk revWalk = new RevWalk(repository);
2266                 try {
2267                     RevCommit revCommit = revWalk.parseCommit(commitId);
2268                     if (!branchName.startsWith("refs/")) {
2269                         branchName = "refs/heads/" + branchName;
2270                     }
2271                     RefUpdate ru = repository.updateRef(branchName);
2272                     ru.setNewObjectId(commitId);
2273                     ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false);
2274                     Result rc = ru.forceUpdate();
2275                     switch (rc) {
2276                     case NEW:
2277                     case FORCED:
2278                     case FAST_FORWARD:
2279                         success = true;
2280                         break;
2281                     default:
2282                         success = false;
2283                     }
2284                 } finally {
a1cee6 2285                     revWalk.close();
7b0895 2286                 }
JM 2287             } finally {
a1cee6 2288                 odi.close();
7b0895 2289             }
JM 2290         } catch (Throwable t) {
2291             error(t, repository, "Failed to create orphan branch {1} in repository {0}", branchName);
9197d3 2292         }
a125cf 2293         return success;
9197d3 2294     }
699e71 2295
e4e682 2296     /**
JM 2297      * Reads the sparkleshare id, if present, from the repository.
699e71 2298      *
e4e682 2299      * @param repository
JM 2300      * @return an id or null
2301      */
2302     public static String getSparkleshareId(Repository repository) {
2303         byte[] content = getByteContent(repository, null, ".sparkleshare", false);
2304         if (content == null) {
2305             return null;
2306         }
2307         return StringUtils.decodeString(content);
2308     }
c44dd0 2309
JM 2310     /**
2311      * Automatic repair of (some) invalid refspecs.  These are the result of a
2312      * bug in JGit cloning where a double forward-slash was injected.  :(
2313      *
2314      * @param repository
2315      * @return true, if the refspecs were repaired
2316      */
2317     public static boolean repairFetchSpecs(Repository repository) {
2318         StoredConfig rc = repository.getConfig();
2319
2320         // auto-repair broken fetch ref specs
2321         for (String name : rc.getSubsections("remote")) {
2322             int invalidSpecs = 0;
2323             int repairedSpecs = 0;
2324             List<String> specs = new ArrayList<String>();
2325             for (String spec : rc.getStringList("remote", name, "fetch")) {
2326                 try {
2327                     RefSpec rs = new RefSpec(spec);
2328                     // valid spec
2329                     specs.add(spec);
2330                 } catch (IllegalArgumentException e) {
2331                     // invalid spec
2332                     invalidSpecs++;
2333                     if (spec.contains("//")) {
2334                         // auto-repair this known spec bug
2335                         spec = spec.replace("//", "/");
2336                         specs.add(spec);
2337                         repairedSpecs++;
2338                     }
2339                 }
2340             }
2341
2342             if (invalidSpecs == repairedSpecs && repairedSpecs > 0) {
2343                 // the fetch specs were automatically repaired
2344                 rc.setStringList("remote", name, "fetch", specs);
2345                 try {
2346                     rc.save();
2347                     rc.load();
2348                     LOGGER.debug("repaired {} invalid fetch refspecs for {}", repairedSpecs, repository.getDirectory());
2349                     return true;
2350                 } catch (Exception e) {
2351                     LOGGER.error(null, e);
2352                 }
2353             } else if (invalidSpecs > 0) {
2354                 LOGGER.error("mirror executor found {} invalid fetch refspecs for {}", invalidSpecs, repository.getDirectory());
2355             }
2356         }
2357         return false;
2358     }
a9a2ff 2359
MC 2360     /**
2361      * Returns true if the commit identified by commitId is an ancestor or the
2362      * the commit identified by tipId.
2363      *
2364      * @param repository
2365      * @param commitId
2366      * @param tipId
2367      * @return true if there is the commit is an ancestor of the tip
2368      */
2369     public static boolean isMergedInto(Repository repository, String commitId, String tipId) {
2370         try {
2371             return isMergedInto(repository, repository.resolve(commitId), repository.resolve(tipId));
2372         } catch (Exception e) {
2373             LOGGER.error("Failed to determine isMergedInto", e);
2374         }
2375         return false;
2376     }
2377
2378     /**
2379      * Returns true if the commit identified by commitId is an ancestor or the
2380      * the commit identified by tipId.
2381      *
2382      * @param repository
2383      * @param commitId
2384      * @param tipId
2385      * @return true if there is the commit is an ancestor of the tip
2386      */
2387     public static boolean isMergedInto(Repository repository, ObjectId commitId, ObjectId tipCommitId) {
2388         // traverse the revlog looking for a commit chain between the endpoints
2389         RevWalk rw = new RevWalk(repository);
2390         try {
2391             // must re-lookup RevCommits to workaround undocumented RevWalk bug
2392             RevCommit tip = rw.lookupCommit(tipCommitId);
2393             RevCommit commit = rw.lookupCommit(commitId);
2394             return rw.isMergedInto(commit, tip);
2395         } catch (Exception e) {
2396             LOGGER.error("Failed to determine isMergedInto", e);
2397         } finally {
2398             rw.dispose();
2399         }
2400         return false;
2401     }
2402
2403     /**
2404      * Returns the merge base of two commits or null if there is no common
2405      * ancestry.
2406      *
2407      * @param repository
2408      * @param commitIdA
2409      * @param commitIdB
2410      * @return the commit id of the merge base or null if there is no common base
2411      */
2412     public static String getMergeBase(Repository repository, ObjectId commitIdA, ObjectId commitIdB) {
2413         RevWalk rw = new RevWalk(repository);
2414         try {
2415             RevCommit a = rw.lookupCommit(commitIdA);
2416             RevCommit b = rw.lookupCommit(commitIdB);
2417
2418             rw.setRevFilter(RevFilter.MERGE_BASE);
2419             rw.markStart(a);
2420             rw.markStart(b);
2421             RevCommit mergeBase = rw.next();
2422             if (mergeBase == null) {
2423                 return null;
2424             }
2425             return mergeBase.getName();
2426         } catch (Exception e) {
2427             LOGGER.error("Failed to determine merge base", e);
2428         } finally {
2429             rw.dispose();
2430         }
2431         return null;
2432     }
2433
2434     public static enum MergeStatus {
2435         MISSING_INTEGRATION_BRANCH, MISSING_SRC_BRANCH, NOT_MERGEABLE, FAILED, ALREADY_MERGED, MERGEABLE, MERGED;
2436     }
2437
2438     /**
2439      * Determines if we can cleanly merge one branch into another.  Returns true
2440      * if we can merge without conflict, otherwise returns false.
2441      *
2442      * @param repository
2443      * @param src
2444      * @param toBranch
2445      * @return true if we can merge without conflict
2446      */
2447     public static MergeStatus canMerge(Repository repository, String src, String toBranch) {
2448         RevWalk revWalk = null;
2449         try {
4a2fb1 2450             revWalk = new RevWalk(repository);
JM 2451             ObjectId branchId = repository.resolve(toBranch);
2452             if (branchId == null) {
2453                 return MergeStatus.MISSING_INTEGRATION_BRANCH;
2454             }
2455             ObjectId srcId = repository.resolve(src);
2456             if (srcId == null) {
2457                 return MergeStatus.MISSING_SRC_BRANCH;
2458             }
a9a2ff 2459             RevCommit branchTip = revWalk.lookupCommit(branchId);
MC 2460             RevCommit srcTip = revWalk.lookupCommit(srcId);
2461             if (revWalk.isMergedInto(srcTip, branchTip)) {
2462                 // already merged
2463                 return MergeStatus.ALREADY_MERGED;
2464             } else if (revWalk.isMergedInto(branchTip, srcTip)) {
2465                 // fast-forward
2466                 return MergeStatus.MERGEABLE;
2467             }
2468             RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
2469             boolean canMerge = merger.merge(branchTip, srcTip);
2470             if (canMerge) {
2471                 return MergeStatus.MERGEABLE;
2472             }
4a2fb1 2473         } catch (NullPointerException e) {
JM 2474             LOGGER.error("Failed to determine canMerge", e);
a9a2ff 2475         } catch (IOException e) {
MC 2476             LOGGER.error("Failed to determine canMerge", e);
87170e 2477         } finally {
a9a2ff 2478             if (revWalk != null) {
a1cee6 2479                 revWalk.close();
a9a2ff 2480             }
MC 2481         }
2482         return MergeStatus.NOT_MERGEABLE;
2483     }
2484
2485
2486     public static class MergeResult {
2487         public final MergeStatus status;
2488         public final String sha;
2489
2490         MergeResult(MergeStatus status, String sha) {
2491             this.status = status;
2492             this.sha = sha;
2493         }
2494     }
2495
2496     /**
2497      * Tries to merge a commit into a branch.  If there are conflicts, the merge
2498      * will fail.
2499      *
2500      * @param repository
2501      * @param src
2502      * @param toBranch
2503      * @param committer
2504      * @param message
2505      * @return the merge result
2506      */
2507     public static MergeResult merge(Repository repository, String src, String toBranch,
2508             PersonIdent committer, String message) {
2509
2510         if (!toBranch.startsWith(Constants.R_REFS)) {
2511             // branch ref doesn't start with ref, assume this is a branch head
2512             toBranch = Constants.R_HEADS + toBranch;
2513         }
2514
2515         RevWalk revWalk = null;
2516         try {
2517             revWalk = new RevWalk(repository);
2518             RevCommit branchTip = revWalk.lookupCommit(repository.resolve(toBranch));
2519             RevCommit srcTip = revWalk.lookupCommit(repository.resolve(src));
2520             if (revWalk.isMergedInto(srcTip, branchTip)) {
2521                 // already merged
2522                 return new MergeResult(MergeStatus.ALREADY_MERGED, null);
2523             }
2524             RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
2525             boolean merged = merger.merge(branchTip, srcTip);
2526             if (merged) {
2527                 // create a merge commit and a reference to track the merge commit
2528                 ObjectId treeId = merger.getResultTreeId();
2529                 ObjectInserter odi = repository.newObjectInserter();
2530                 try {
2531                     // Create a commit object
2532                     CommitBuilder commitBuilder = new CommitBuilder();
2533                     commitBuilder.setCommitter(committer);
2534                     commitBuilder.setAuthor(committer);
2535                     commitBuilder.setEncoding(Constants.CHARSET);
2536                     if (StringUtils.isEmpty(message)) {
2537                         message = MessageFormat.format("merge {0} into {1}", srcTip.getName(), branchTip.getName());
2538                     }
2539                     commitBuilder.setMessage(message);
2540                     commitBuilder.setParentIds(branchTip.getId(), srcTip.getId());
2541                     commitBuilder.setTreeId(treeId);
2542
2543                     // Insert the merge commit into the repository
2544                     ObjectId mergeCommitId = odi.insert(commitBuilder);
2545                     odi.flush();
2546
2547                     // set the merge ref to the merge commit
2548                     RevCommit mergeCommit = revWalk.parseCommit(mergeCommitId);
2549                     RefUpdate mergeRefUpdate = repository.updateRef(toBranch);
2550                     mergeRefUpdate.setNewObjectId(mergeCommitId);
2551                     mergeRefUpdate.setRefLogMessage("commit: " + mergeCommit.getShortMessage(), false);
2552                     RefUpdate.Result rc = mergeRefUpdate.update();
2553                     switch (rc) {
2554                     case FAST_FORWARD:
2555                         // successful, clean merge
d2ab7a 2556                         break;
a9a2ff 2557                     default:
MC 2558                         throw new GitBlitException(MessageFormat.format("Unexpected result \"{0}\" when merging commit {1} into {2} in {3}",
2559                                 rc.name(), srcTip.getName(), branchTip.getName(), repository.getDirectory()));
2560                     }
2561
2562                     // return the merge commit id
2563                     return new MergeResult(MergeStatus.MERGED, mergeCommitId.getName());
2564                 } finally {
a1cee6 2565                     odi.close();
a9a2ff 2566                 }
MC 2567             }
2568         } catch (IOException e) {
2569             LOGGER.error("Failed to merge", e);
87170e 2570         } finally {
a9a2ff 2571             if (revWalk != null) {
a1cee6 2572                 revWalk.close();
a9a2ff 2573             }
MC 2574         }
2575         return new MergeResult(MergeStatus.FAILED, null);
2576     }
46f33f 2577     
PM 2578     
2579     /**
2580      * Returns the LFS URL for the given oid 
2581      * Currently assumes that the Gitblit Filestore is used 
2582      *
2583      * @param baseURL
2584      * @param repository name
2585      * @param oid of lfs item
2586      * @return the lfs item URL
2587      */
2588     public static String getLfsRepositoryUrl(String baseURL, String repositoryName, String oid) {
2589         
2590         if (baseURL.length() > 0 && baseURL.charAt(baseURL.length() - 1) == '/') {
2591             baseURL = baseURL.substring(0, baseURL.length() - 1);
2592         }
2593         
2594         return baseURL + com.gitblit.Constants.R_PATH 
2595                        + repositoryName + "/" 
2596                        + com.gitblit.Constants.R_LFS 
2597                        + "objects/" + oid;
2598         
2599     }
5fe7df 2600 }