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