Paul Martin
2016-03-28 33f3580f8f4499ac9cd4812872c94fcdd81a5829
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         String branchName = branch;
1749         if (!branchName.startsWith(Constants.R_HEADS)) {
1750             branchName = Constants.R_HEADS + branch;
1751         }
1752
1753         try {
1754             RefUpdate refUpdate = repository.updateRef(branchName, false);
1755             refUpdate.setForceUpdate(true);
1756             RefUpdate.Result result = refUpdate.delete();
1757             switch (result) {
1758             case NEW:
1759             case FORCED:
1760             case NO_CHANGE:
1761             case FAST_FORWARD:
699e71 1762                 return true;
f96d32 1763             default:
JM 1764                 LOGGER.error(MessageFormat.format("{0} failed to delete to {1} returned result {2}",
1765                         repository.getDirectory().getAbsolutePath(), branchName, result));
1766             }
1767         } catch (Throwable t) {
1768             error(t, repository, "{0} failed to delete {1}", branchName);
1769         }
1770         return false;
1771     }
699e71 1772
f96d32 1773     /**
90b8d7 1774      * Get the full branch and tag ref names for any potential HEAD targets.
2cdb73 1775      *
PLM 1776      * @param repository
1777      * @return a list of ref names
1778      */
1779     public static List<String> getAvailableHeadTargets(Repository repository) {
1780         List<String> targets = new ArrayList<String>();
90b8d7 1781         for (RefModel branchModel : JGitUtils.getLocalBranches(repository, true, -1)) {
JM 1782             targets.add(branchModel.getName());
2cdb73 1783         }
90b8d7 1784
JM 1785         for (RefModel tagModel : JGitUtils.getTags(repository, true, -1)) {
1786             targets.add(tagModel.getName());
2cdb73 1787         }
PLM 1788         return targets;
1789     }
912938 1790
PLM 1791     /**
ed21d2 1792      * Returns all refs grouped by their associated object id.
699e71 1793      *
ed21d2 1794      * @param repository
JM 1795      * @return all refs grouped by their referenced object id
1796      */
1797     public static Map<ObjectId, List<RefModel>> getAllRefs(Repository repository) {
1e1b85 1798         return getAllRefs(repository, true);
JM 1799     }
699e71 1800
1e1b85 1801     /**
JM 1802      * Returns all refs grouped by their associated object id.
699e71 1803      *
1e1b85 1804      * @param repository
JM 1805      * @param includeRemoteRefs
1806      * @return all refs grouped by their referenced object id
1807      */
1808     public static Map<ObjectId, List<RefModel>> getAllRefs(Repository repository, boolean includeRemoteRefs) {
ed21d2 1809         List<RefModel> list = getRefs(repository, org.eclipse.jgit.lib.RefDatabase.ALL, true, -1);
JM 1810         Map<ObjectId, List<RefModel>> refs = new HashMap<ObjectId, List<RefModel>>();
1811         for (RefModel ref : list) {
1e1b85 1812             if (!includeRemoteRefs && ref.getName().startsWith(Constants.R_REMOTES)) {
JM 1813                 continue;
1814             }
ed21d2 1815             ObjectId objectid = ref.getReferencedObjectId();
JM 1816             if (!refs.containsKey(objectid)) {
1817                 refs.put(objectid, new ArrayList<RefModel>());
1818             }
1819             refs.get(objectid).add(ref);
1820         }
1821         return refs;
5fe7df 1822     }
JM 1823
ed21d2 1824     /**
JM 1825      * Returns the list of tags in the repository. If repository does not exist
1826      * or is empty, an empty list is returned.
699e71 1827      *
ed21d2 1828      * @param repository
JM 1829      * @param fullName
1830      *            if true, /refs/tags/yadayadayada is returned. If false,
1831      *            yadayadayada is returned.
1832      * @param maxCount
1833      *            if < 0, all tags are returned
1834      * @return list of tags
1835      */
1836     public static List<RefModel> getTags(Repository repository, boolean fullName, int maxCount) {
1837         return getRefs(repository, Constants.R_TAGS, fullName, maxCount);
232890 1838     }
JM 1839
ed21d2 1840     /**
f76fee 1841      * Returns the list of tags in the repository. If repository does not exist
GS 1842      * or is empty, an empty list is returned.
1843      *
1844      * @param repository
1845      * @param fullName
1846      *            if true, /refs/tags/yadayadayada is returned. If false,
1847      *            yadayadayada is returned.
1848      * @param maxCount
1849      *            if < 0, all tags are returned
1850      * @param offset
1851      *            if maxCount provided sets the starting point of the records to return
1852      * @return list of tags
1853      */
1854     public static List<RefModel> getTags(Repository repository, boolean fullName, int maxCount, int offset) {
1855         return getRefs(repository, Constants.R_TAGS, fullName, maxCount, offset);
1856     }
1857
1858     /**
ed21d2 1859      * Returns the list of local branches in the repository. If repository does
JM 1860      * not exist or is empty, an empty list is returned.
699e71 1861      *
ed21d2 1862      * @param repository
JM 1863      * @param fullName
1864      *            if true, /refs/heads/yadayadayada is returned. If false,
1865      *            yadayadayada is returned.
1866      * @param maxCount
1867      *            if < 0, all local branches are returned
1868      * @return list of local branches
1869      */
1870     public static List<RefModel> getLocalBranches(Repository repository, boolean fullName,
1871             int maxCount) {
1872         return getRefs(repository, Constants.R_HEADS, fullName, maxCount);
5fe7df 1873     }
JM 1874
ed21d2 1875     /**
JM 1876      * Returns the list of remote branches in the repository. If repository does
1877      * not exist or is empty, an empty list is returned.
699e71 1878      *
ed21d2 1879      * @param repository
JM 1880      * @param fullName
1881      *            if true, /refs/remotes/yadayadayada is returned. If false,
1882      *            yadayadayada is returned.
1883      * @param maxCount
1884      *            if < 0, all remote branches are returned
1885      * @return list of remote branches
1886      */
1887     public static List<RefModel> getRemoteBranches(Repository repository, boolean fullName,
1888             int maxCount) {
1889         return getRefs(repository, Constants.R_REMOTES, fullName, maxCount);
a125cf 1890     }
JM 1891
ed21d2 1892     /**
JM 1893      * Returns the list of note branches. If repository does not exist or is
1894      * empty, an empty list is returned.
699e71 1895      *
ed21d2 1896      * @param repository
JM 1897      * @param fullName
1898      *            if true, /refs/notes/yadayadayada is returned. If false,
1899      *            yadayadayada is returned.
1900      * @param maxCount
1901      *            if < 0, all note branches are returned
1902      * @return list of note branches
1903      */
1904     public static List<RefModel> getNoteBranches(Repository repository, boolean fullName,
1905             int maxCount) {
1906         return getRefs(repository, Constants.R_NOTES, fullName, maxCount);
1907     }
699e71 1908
f8bb95 1909     /**
699e71 1910      * Returns the list of refs in the specified base ref. If repository does
f8bb95 1911      * not exist or is empty, an empty list is returned.
699e71 1912      *
f8bb95 1913      * @param repository
JM 1914      * @param fullName
1915      *            if true, /refs/yadayadayada is returned. If false,
1916      *            yadayadayada is returned.
1917      * @return list of refs
1918      */
1919     public static List<RefModel> getRefs(Repository repository, String baseRef) {
1920         return getRefs(repository, baseRef, true, -1);
1921     }
ed21d2 1922
JM 1923     /**
1924      * Returns a list of references in the repository matching "refs". If the
1925      * repository is null or empty, an empty list is returned.
699e71 1926      *
ed21d2 1927      * @param repository
JM 1928      * @param refs
1929      *            if unspecified, all refs are returned
1930      * @param fullName
1931      *            if true, /refs/something/yadayadayada is returned. If false,
1932      *            yadayadayada is returned.
1933      * @param maxCount
1934      *            if < 0, all references are returned
1935      * @return list of references
1936      */
1937     private static List<RefModel> getRefs(Repository repository, String refs, boolean fullName,
1938             int maxCount) {
f76fee 1939         return getRefs(repository, refs, fullName, maxCount, 0);
GS 1940     }
1941
1942     /**
1943      * Returns a list of references in the repository matching "refs". If the
1944      * repository is null or empty, an empty list is returned.
1945      *
1946      * @param repository
1947      * @param refs
1948      *            if unspecified, all refs are returned
1949      * @param fullName
1950      *            if true, /refs/something/yadayadayada is returned. If false,
1951      *            yadayadayada is returned.
1952      * @param maxCount
1953      *            if < 0, all references are returned
1954      * @param offset
1955      *            if maxCount provided sets the starting point of the records to return
1956      * @return list of references
1957      */
1958     private static List<RefModel> getRefs(Repository repository, String refs, boolean fullName,
1959             int maxCount, int offset) {
5fe7df 1960         List<RefModel> list = new ArrayList<RefModel>();
85c2e6 1961         if (maxCount == 0) {
JM 1962             return list;
1963         }
ed21d2 1964         if (!hasCommits(repository)) {
JM 1965             return list;
1966         }
5fe7df 1967         try {
ed21d2 1968             Map<String, Ref> map = repository.getRefDatabase().getRefs(refs);
JM 1969             RevWalk rw = new RevWalk(repository);
2a7306 1970             for (Entry<String, Ref> entry : map.entrySet()) {
JM 1971                 Ref ref = entry.getValue();
4ab184 1972                 RevObject object = rw.parseAny(ref.getObjectId());
5cc4f2 1973                 String name = entry.getKey();
JM 1974                 if (fullName && !StringUtils.isEmpty(refs)) {
1975                     name = refs + name;
1976                 }
1977                 list.add(new RefModel(name, ref, object));
5fe7df 1978             }
4ab184 1979             rw.dispose();
5fe7df 1980             Collections.sort(list);
JM 1981             Collections.reverse(list);
1982             if (maxCount > 0 && list.size() > maxCount) {
f76fee 1983                 if (offset < 0) {
GS 1984                     offset = 0;
1985                 }
1986                 int endIndex = offset + maxCount;
1987                 if (endIndex > list.size()) {
1988                     endIndex = list.size();
1989                 }
1990                 list = new ArrayList<RefModel>(list.subList(offset, endIndex));
5fe7df 1991             }
JM 1992         } catch (IOException e) {
a2709d 1993             error(e, repository, "{0} failed to retrieve {1}", refs);
5fe7df 1994         }
JM 1995         return list;
1996     }
1997
ed21d2 1998     /**
11924d 1999      * Returns a RefModel for the gh-pages branch in the repository. If the
JM 2000      * branch can not be found, null is returned.
699e71 2001      *
11924d 2002      * @param repository
JM 2003      * @return a refmodel for the gh-pages branch or null
2004      */
2005     public static RefModel getPagesBranch(Repository repository) {
cc1cd7 2006         return getBranch(repository, "gh-pages");
JM 2007     }
2008
2009     /**
2010      * Returns a RefModel for a specific branch name in the repository. If the
2011      * branch can not be found, null is returned.
699e71 2012      *
cc1cd7 2013      * @param repository
JM 2014      * @return a refmodel for the branch or null
2015      */
2016     public static RefModel getBranch(Repository repository, String name) {
2017         RefModel branch = null;
11924d 2018         try {
cc1cd7 2019             // search for the branch in local heads
11924d 2020             for (RefModel ref : JGitUtils.getLocalBranches(repository, false, -1)) {
8744a1 2021                 if (ref.reference.getName().endsWith(name)) {
cc1cd7 2022                     branch = ref;
11924d 2023                     break;
JM 2024                 }
2025             }
2026
cc1cd7 2027             // search for the branch in remote heads
JM 2028             if (branch == null) {
11924d 2029                 for (RefModel ref : JGitUtils.getRemoteBranches(repository, false, -1)) {
8744a1 2030                     if (ref.reference.getName().endsWith(name)) {
cc1cd7 2031                         branch = ref;
11924d 2032                         break;
JM 2033                     }
2034                 }
2035             }
2036         } catch (Throwable t) {
cc1cd7 2037             LOGGER.error(MessageFormat.format("Failed to find {0} branch!", name), t);
11924d 2038         }
cc1cd7 2039         return branch;
11924d 2040     }
699e71 2041
eb870f 2042     /**
JM 2043      * Returns the list of submodules for this repository.
699e71 2044      *
eb870f 2045      * @param repository
JM 2046      * @param commit
2047      * @return list of submodules
2048      */
2049     public static List<SubmoduleModel> getSubmodules(Repository repository, String commitId) {
2050         RevCommit commit = getCommit(repository, commitId);
2051         return getSubmodules(repository, commit.getTree());
2052     }
699e71 2053
eb870f 2054     /**
JM 2055      * Returns the list of submodules for this repository.
699e71 2056      *
eb870f 2057      * @param repository
JM 2058      * @param commit
2059      * @return list of submodules
2060      */
2061     public static List<SubmoduleModel> getSubmodules(Repository repository, RevTree tree) {
2062         List<SubmoduleModel> list = new ArrayList<SubmoduleModel>();
e4e682 2063         byte [] blob = getByteContent(repository, tree, ".gitmodules", false);
eb870f 2064         if (blob == null) {
JM 2065             return list;
2066         }
2067         try {
2068             BlobBasedConfig config = new BlobBasedConfig(repository.getConfig(), blob);
2069             for (String module : config.getSubsections("submodule")) {
2070                 String path = config.getString("submodule", module, "path");
2071                 String url = config.getString("submodule", module, "url");
2072                 list.add(new SubmoduleModel(module, path, url));
2073             }
2074         } catch (ConfigInvalidException e) {
2075             LOGGER.error("Failed to load .gitmodules file for " + repository.getDirectory(), e);
2076         }
2077         return list;
2078     }
699e71 2079
eb870f 2080     /**
JM 2081      * Returns the submodule definition for the specified path at the specified
2082      * commit.  If no module is defined for the path, null is returned.
699e71 2083      *
eb870f 2084      * @param repository
JM 2085      * @param commit
2086      * @param path
2087      * @return a submodule definition or null if there is no submodule
2088      */
2089     public static SubmoduleModel getSubmoduleModel(Repository repository, String commitId, String path) {
2090         for (SubmoduleModel model : getSubmodules(repository, commitId)) {
2091             if (model.path.equals(path)) {
2092                 return model;
2093             }
2094         }
2095         return null;
2096     }
699e71 2097
657a65 2098     public static String getSubmoduleCommitId(Repository repository, String path, RevCommit commit) {
JM 2099         String commitId = null;
2100         RevWalk rw = new RevWalk(repository);
2101         TreeWalk tw = new TreeWalk(repository);
2102         tw.setFilter(PathFilterGroup.createFromStrings(Collections.singleton(path)));
2103         try {
2104             tw.reset(commit.getTree());
2105             while (tw.next()) {
2106                 if (tw.isSubtree() && !path.equals(tw.getPathString())) {
2107                     tw.enterSubtree();
2108                     continue;
2109                 }
2110                 if (FileMode.GITLINK == tw.getFileMode(0)) {
2111                     commitId = tw.getObjectId(0).getName();
2112                     break;
2113                 }
2114             }
2115         } catch (Throwable t) {
2116             error(t, repository, "{0} can't find {1} in commit {2}", path, commit.name());
2117         } finally {
2118             rw.dispose();
a1cee6 2119             tw.close();
657a65 2120         }
JM 2121         return commitId;
2122     }
11924d 2123
JM 2124     /**
ed21d2 2125      * Returns the list of notes entered about the commit from the refs/notes
JM 2126      * namespace. If the repository does not exist or is empty, an empty list is
2127      * returned.
699e71 2128      *
ed21d2 2129      * @param repository
JM 2130      * @param commit
2131      * @return list of notes
2132      */
a125cf 2133     public static List<GitNote> getNotesOnCommit(Repository repository, RevCommit commit) {
JM 2134         List<GitNote> list = new ArrayList<GitNote>();
ed21d2 2135         if (!hasCommits(repository)) {
JM 2136             return list;
2137         }
2138         List<RefModel> noteBranches = getNoteBranches(repository, true, -1);
2139         for (RefModel notesRef : noteBranches) {
4ab184 2140             RevTree notesTree = JGitUtils.getCommit(repository, notesRef.getName()).getTree();
9357e9 2141             // flat notes list
JM 2142             String notePath = commit.getName();
2143             String text = getStringContent(repository, notesTree, notePath);
2144             if (!StringUtils.isEmpty(text)) {
2145                 List<RevCommit> history = getRevLog(repository, notesRef.getName(), notePath, 0, -1);
2146                 RefModel noteRef = new RefModel(notesRef.displayName, null, history.get(history
2147                         .size() - 1));
2148                 GitNote gitNote = new GitNote(noteRef, text);
2149                 list.add(gitNote);
2150                 continue;
2151             }
699e71 2152
9357e9 2153             // folder structure
a125cf 2154             StringBuilder sb = new StringBuilder(commit.getName());
JM 2155             sb.insert(2, '/');
9357e9 2156             notePath = sb.toString();
JM 2157             text = getStringContent(repository, notesTree, notePath);
a125cf 2158             if (!StringUtils.isEmpty(text)) {
4ab184 2159                 List<RevCommit> history = getRevLog(repository, notesRef.getName(), notePath, 0, -1);
JM 2160                 RefModel noteRef = new RefModel(notesRef.displayName, null, history.get(history
2161                         .size() - 1));
2162                 GitNote gitNote = new GitNote(noteRef, text);
a125cf 2163                 list.add(gitNote);
5fe7df 2164             }
JM 2165         }
a125cf 2166         return list;
5fe7df 2167     }
0f47b2 2168
99f359 2169     /**
S 2170      * this method creates an incremental revision number as a tag according to
0f47b2 2171      * the amount of already existing tags, which start with a defined prefix.
699e71 2172      *
99f359 2173      * @param repository
S 2174      * @param objectId
0f47b2 2175      * @param tagger
JM 2176      * @param prefix
2177      * @param intPattern
2178      * @param message
99f359 2179      * @return true if operation was successful, otherwise false
S 2180      */
0f47b2 2181     public static boolean createIncrementalRevisionTag(Repository repository,
JM 2182             String objectId, PersonIdent tagger, String prefix, String intPattern, String message) {
99f359 2183         boolean result = false;
S 2184         Iterator<Entry<String, Ref>> iterator = repository.getTags().entrySet().iterator();
0f47b2 2185         long lastRev = 0;
99f359 2186         while (iterator.hasNext()) {
S 2187             Entry<String, Ref> entry = iterator.next();
0f47b2 2188             if (entry.getKey().startsWith(prefix)) {
JM 2189                 try {
2190                     long val = Long.parseLong(entry.getKey().substring(prefix.length()));
2191                     if (val > lastRev) {
2192                         lastRev = val;
2193                     }
2194                 } catch (Exception e) {
2195                     // this tag is NOT an incremental revision tag
2196                 }
99f359 2197             }
S 2198         }
0f47b2 2199         DecimalFormat df = new DecimalFormat(intPattern);
JM 2200         result = createTag(repository, objectId, tagger, prefix + df.format((lastRev + 1)), message);
99f359 2201         return result;
S 2202     }
5fe7df 2203
ed21d2 2204     /**
99f359 2205      * creates a tag in a repository
699e71 2206      *
99f359 2207      * @param repository
S 2208      * @param objectId, the ref the tag points towards
0f47b2 2209      * @param tagger, the person tagging the object
JM 2210      * @param tag, the string label
2211      * @param message, the string message
99f359 2212      * @return boolean, true if operation was successful, otherwise false
S 2213      */
0f47b2 2214     public static boolean createTag(Repository repository, String objectId, PersonIdent tagger, String tag, String message) {
699e71 2215         try {
99f359 2216             Git gitClient = Git.open(repository.getDirectory());
S 2217             TagCommand tagCommand = gitClient.tag();
0f47b2 2218             tagCommand.setTagger(tagger);
JM 2219             tagCommand.setMessage(message);
99f359 2220             if (objectId != null) {
S 2221                 RevObject revObj = getCommit(repository, objectId);
2222                 tagCommand.setObjectId(revObj);
2223             }
2224             tagCommand.setName(tag);
699e71 2225             Ref call = tagCommand.call();
99f359 2226             return call != null ? true : false;
S 2227         } catch (Exception e) {
0f47b2 2228             error(e, repository, "Failed to create tag {1} in repository {0}", objectId, tag);
99f359 2229         }
S 2230         return false;
2231     }
699e71 2232
99f359 2233     /**
7b0895 2234      * Create an orphaned branch in a repository.
699e71 2235      *
a2709d 2236      * @param repository
7b0895 2237      * @param branchName
JM 2238      * @param author
2239      *            if unspecified, Gitblit will be the author of this new branch
2240      * @return true if successful
a2709d 2241      */
7b0895 2242     public static boolean createOrphanBranch(Repository repository, String branchName,
JM 2243             PersonIdent author) {
2244         boolean success = false;
2245         String message = "Created branch " + branchName;
2246         if (author == null) {
2247             author = new PersonIdent("Gitblit", "gitblit@localhost");
2248         }
2249         try {
2250             ObjectInserter odi = repository.newObjectInserter();
2251             try {
2252                 // Create a blob object to insert into a tree
2253                 ObjectId blobId = odi.insert(Constants.OBJ_BLOB,
2254                         message.getBytes(Constants.CHARACTER_ENCODING));
2255
2256                 // Create a tree object to reference from a commit
2257                 TreeFormatter tree = new TreeFormatter();
69a559 2258                 tree.append(".branch", FileMode.REGULAR_FILE, blobId);
7b0895 2259                 ObjectId treeId = odi.insert(tree);
JM 2260
2261                 // Create a commit object
2262                 CommitBuilder commit = new CommitBuilder();
2263                 commit.setAuthor(author);
2264                 commit.setCommitter(author);
2265                 commit.setEncoding(Constants.CHARACTER_ENCODING);
2266                 commit.setMessage(message);
2267                 commit.setTreeId(treeId);
2268
2269                 // Insert the commit into the repository
2270                 ObjectId commitId = odi.insert(commit);
2271                 odi.flush();
2272
2273                 RevWalk revWalk = new RevWalk(repository);
2274                 try {
2275                     RevCommit revCommit = revWalk.parseCommit(commitId);
2276                     if (!branchName.startsWith("refs/")) {
2277                         branchName = "refs/heads/" + branchName;
2278                     }
2279                     RefUpdate ru = repository.updateRef(branchName);
2280                     ru.setNewObjectId(commitId);
2281                     ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false);
2282                     Result rc = ru.forceUpdate();
2283                     switch (rc) {
2284                     case NEW:
2285                     case FORCED:
2286                     case FAST_FORWARD:
2287                         success = true;
2288                         break;
2289                     default:
2290                         success = false;
2291                     }
2292                 } finally {
a1cee6 2293                     revWalk.close();
7b0895 2294                 }
JM 2295             } finally {
a1cee6 2296                 odi.close();
7b0895 2297             }
JM 2298         } catch (Throwable t) {
2299             error(t, repository, "Failed to create orphan branch {1} in repository {0}", branchName);
9197d3 2300         }
a125cf 2301         return success;
9197d3 2302     }
699e71 2303
e4e682 2304     /**
JM 2305      * Reads the sparkleshare id, if present, from the repository.
699e71 2306      *
e4e682 2307      * @param repository
JM 2308      * @return an id or null
2309      */
2310     public static String getSparkleshareId(Repository repository) {
2311         byte[] content = getByteContent(repository, null, ".sparkleshare", false);
2312         if (content == null) {
2313             return null;
2314         }
2315         return StringUtils.decodeString(content);
2316     }
c44dd0 2317
JM 2318     /**
2319      * Automatic repair of (some) invalid refspecs.  These are the result of a
2320      * bug in JGit cloning where a double forward-slash was injected.  :(
2321      *
2322      * @param repository
2323      * @return true, if the refspecs were repaired
2324      */
2325     public static boolean repairFetchSpecs(Repository repository) {
2326         StoredConfig rc = repository.getConfig();
2327
2328         // auto-repair broken fetch ref specs
2329         for (String name : rc.getSubsections("remote")) {
2330             int invalidSpecs = 0;
2331             int repairedSpecs = 0;
2332             List<String> specs = new ArrayList<String>();
2333             for (String spec : rc.getStringList("remote", name, "fetch")) {
2334                 try {
2335                     RefSpec rs = new RefSpec(spec);
2336                     // valid spec
2337                     specs.add(spec);
2338                 } catch (IllegalArgumentException e) {
2339                     // invalid spec
2340                     invalidSpecs++;
2341                     if (spec.contains("//")) {
2342                         // auto-repair this known spec bug
2343                         spec = spec.replace("//", "/");
2344                         specs.add(spec);
2345                         repairedSpecs++;
2346                     }
2347                 }
2348             }
2349
2350             if (invalidSpecs == repairedSpecs && repairedSpecs > 0) {
2351                 // the fetch specs were automatically repaired
2352                 rc.setStringList("remote", name, "fetch", specs);
2353                 try {
2354                     rc.save();
2355                     rc.load();
2356                     LOGGER.debug("repaired {} invalid fetch refspecs for {}", repairedSpecs, repository.getDirectory());
2357                     return true;
2358                 } catch (Exception e) {
2359                     LOGGER.error(null, e);
2360                 }
2361             } else if (invalidSpecs > 0) {
2362                 LOGGER.error("mirror executor found {} invalid fetch refspecs for {}", invalidSpecs, repository.getDirectory());
2363             }
2364         }
2365         return false;
2366     }
a9a2ff 2367
MC 2368     /**
2369      * Returns true if the commit identified by commitId is an ancestor or the
2370      * the commit identified by tipId.
2371      *
2372      * @param repository
2373      * @param commitId
2374      * @param tipId
2375      * @return true if there is the commit is an ancestor of the tip
2376      */
2377     public static boolean isMergedInto(Repository repository, String commitId, String tipId) {
2378         try {
2379             return isMergedInto(repository, repository.resolve(commitId), repository.resolve(tipId));
2380         } catch (Exception e) {
2381             LOGGER.error("Failed to determine isMergedInto", e);
2382         }
2383         return false;
2384     }
2385
2386     /**
2387      * Returns true if the commit identified by commitId is an ancestor or the
2388      * the commit identified by tipId.
2389      *
2390      * @param repository
2391      * @param commitId
2392      * @param tipId
2393      * @return true if there is the commit is an ancestor of the tip
2394      */
2395     public static boolean isMergedInto(Repository repository, ObjectId commitId, ObjectId tipCommitId) {
2396         // traverse the revlog looking for a commit chain between the endpoints
2397         RevWalk rw = new RevWalk(repository);
2398         try {
2399             // must re-lookup RevCommits to workaround undocumented RevWalk bug
2400             RevCommit tip = rw.lookupCommit(tipCommitId);
2401             RevCommit commit = rw.lookupCommit(commitId);
2402             return rw.isMergedInto(commit, tip);
2403         } catch (Exception e) {
2404             LOGGER.error("Failed to determine isMergedInto", e);
2405         } finally {
2406             rw.dispose();
2407         }
2408         return false;
2409     }
2410
2411     /**
2412      * Returns the merge base of two commits or null if there is no common
2413      * ancestry.
2414      *
2415      * @param repository
2416      * @param commitIdA
2417      * @param commitIdB
2418      * @return the commit id of the merge base or null if there is no common base
2419      */
2420     public static String getMergeBase(Repository repository, ObjectId commitIdA, ObjectId commitIdB) {
2421         RevWalk rw = new RevWalk(repository);
2422         try {
2423             RevCommit a = rw.lookupCommit(commitIdA);
2424             RevCommit b = rw.lookupCommit(commitIdB);
2425
2426             rw.setRevFilter(RevFilter.MERGE_BASE);
2427             rw.markStart(a);
2428             rw.markStart(b);
2429             RevCommit mergeBase = rw.next();
2430             if (mergeBase == null) {
2431                 return null;
2432             }
2433             return mergeBase.getName();
2434         } catch (Exception e) {
2435             LOGGER.error("Failed to determine merge base", e);
2436         } finally {
2437             rw.dispose();
2438         }
2439         return null;
2440     }
2441
2442     public static enum MergeStatus {
2443         MISSING_INTEGRATION_BRANCH, MISSING_SRC_BRANCH, NOT_MERGEABLE, FAILED, ALREADY_MERGED, MERGEABLE, MERGED;
2444     }
2445
2446     /**
2447      * Determines if we can cleanly merge one branch into another.  Returns true
2448      * if we can merge without conflict, otherwise returns false.
2449      *
2450      * @param repository
2451      * @param src
2452      * @param toBranch
2453      * @return true if we can merge without conflict
2454      */
2455     public static MergeStatus canMerge(Repository repository, String src, String toBranch) {
2456         RevWalk revWalk = null;
2457         try {
4a2fb1 2458             revWalk = new RevWalk(repository);
JM 2459             ObjectId branchId = repository.resolve(toBranch);
2460             if (branchId == null) {
2461                 return MergeStatus.MISSING_INTEGRATION_BRANCH;
2462             }
2463             ObjectId srcId = repository.resolve(src);
2464             if (srcId == null) {
2465                 return MergeStatus.MISSING_SRC_BRANCH;
2466             }
a9a2ff 2467             RevCommit branchTip = revWalk.lookupCommit(branchId);
MC 2468             RevCommit srcTip = revWalk.lookupCommit(srcId);
2469             if (revWalk.isMergedInto(srcTip, branchTip)) {
2470                 // already merged
2471                 return MergeStatus.ALREADY_MERGED;
2472             } else if (revWalk.isMergedInto(branchTip, srcTip)) {
2473                 // fast-forward
2474                 return MergeStatus.MERGEABLE;
2475             }
2476             RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
2477             boolean canMerge = merger.merge(branchTip, srcTip);
2478             if (canMerge) {
2479                 return MergeStatus.MERGEABLE;
2480             }
4a2fb1 2481         } catch (NullPointerException e) {
JM 2482             LOGGER.error("Failed to determine canMerge", e);
a9a2ff 2483         } catch (IOException e) {
MC 2484             LOGGER.error("Failed to determine canMerge", e);
87170e 2485         } finally {
a9a2ff 2486             if (revWalk != null) {
a1cee6 2487                 revWalk.close();
a9a2ff 2488             }
MC 2489         }
2490         return MergeStatus.NOT_MERGEABLE;
2491     }
2492
2493
2494     public static class MergeResult {
2495         public final MergeStatus status;
2496         public final String sha;
2497
2498         MergeResult(MergeStatus status, String sha) {
2499             this.status = status;
2500             this.sha = sha;
2501         }
2502     }
2503
2504     /**
2505      * Tries to merge a commit into a branch.  If there are conflicts, the merge
2506      * will fail.
2507      *
2508      * @param repository
2509      * @param src
2510      * @param toBranch
2511      * @param committer
2512      * @param message
2513      * @return the merge result
2514      */
2515     public static MergeResult merge(Repository repository, String src, String toBranch,
2516             PersonIdent committer, String message) {
2517
2518         if (!toBranch.startsWith(Constants.R_REFS)) {
2519             // branch ref doesn't start with ref, assume this is a branch head
2520             toBranch = Constants.R_HEADS + toBranch;
2521         }
2522
2523         RevWalk revWalk = null;
2524         try {
2525             revWalk = new RevWalk(repository);
2526             RevCommit branchTip = revWalk.lookupCommit(repository.resolve(toBranch));
2527             RevCommit srcTip = revWalk.lookupCommit(repository.resolve(src));
2528             if (revWalk.isMergedInto(srcTip, branchTip)) {
2529                 // already merged
2530                 return new MergeResult(MergeStatus.ALREADY_MERGED, null);
2531             }
2532             RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
2533             boolean merged = merger.merge(branchTip, srcTip);
2534             if (merged) {
2535                 // create a merge commit and a reference to track the merge commit
2536                 ObjectId treeId = merger.getResultTreeId();
2537                 ObjectInserter odi = repository.newObjectInserter();
2538                 try {
2539                     // Create a commit object
2540                     CommitBuilder commitBuilder = new CommitBuilder();
2541                     commitBuilder.setCommitter(committer);
2542                     commitBuilder.setAuthor(committer);
2543                     commitBuilder.setEncoding(Constants.CHARSET);
2544                     if (StringUtils.isEmpty(message)) {
2545                         message = MessageFormat.format("merge {0} into {1}", srcTip.getName(), branchTip.getName());
2546                     }
2547                     commitBuilder.setMessage(message);
2548                     commitBuilder.setParentIds(branchTip.getId(), srcTip.getId());
2549                     commitBuilder.setTreeId(treeId);
2550
2551                     // Insert the merge commit into the repository
2552                     ObjectId mergeCommitId = odi.insert(commitBuilder);
2553                     odi.flush();
2554
2555                     // set the merge ref to the merge commit
2556                     RevCommit mergeCommit = revWalk.parseCommit(mergeCommitId);
2557                     RefUpdate mergeRefUpdate = repository.updateRef(toBranch);
2558                     mergeRefUpdate.setNewObjectId(mergeCommitId);
2559                     mergeRefUpdate.setRefLogMessage("commit: " + mergeCommit.getShortMessage(), false);
2560                     RefUpdate.Result rc = mergeRefUpdate.update();
2561                     switch (rc) {
2562                     case FAST_FORWARD:
2563                         // successful, clean merge
d2ab7a 2564                         break;
a9a2ff 2565                     default:
MC 2566                         throw new GitBlitException(MessageFormat.format("Unexpected result \"{0}\" when merging commit {1} into {2} in {3}",
2567                                 rc.name(), srcTip.getName(), branchTip.getName(), repository.getDirectory()));
2568                     }
2569
2570                     // return the merge commit id
2571                     return new MergeResult(MergeStatus.MERGED, mergeCommitId.getName());
2572                 } finally {
a1cee6 2573                     odi.close();
a9a2ff 2574                 }
MC 2575             }
2576         } catch (IOException e) {
2577             LOGGER.error("Failed to merge", e);
87170e 2578         } finally {
a9a2ff 2579             if (revWalk != null) {
a1cee6 2580                 revWalk.close();
a9a2ff 2581             }
MC 2582         }
2583         return new MergeResult(MergeStatus.FAILED, null);
2584     }
46f33f 2585     
PM 2586     
2587     /**
2588      * Returns the LFS URL for the given oid 
2589      * Currently assumes that the Gitblit Filestore is used 
2590      *
2591      * @param baseURL
2592      * @param repository name
2593      * @param oid of lfs item
2594      * @return the lfs item URL
2595      */
2596     public static String getLfsRepositoryUrl(String baseURL, String repositoryName, String oid) {
2597         
2598         if (baseURL.length() > 0 && baseURL.charAt(baseURL.length() - 1) == '/') {
2599             baseURL = baseURL.substring(0, baseURL.length() - 1);
2600         }
2601         
2602         return baseURL + com.gitblit.Constants.R_PATH 
2603                        + repositoryName + "/" 
2604                        + com.gitblit.Constants.R_LFS 
2605                        + "objects/" + oid;
2606         
2607     }
275496 2608     
PM 2609     /**
2610      * Returns all tree entries that do not match the ignore paths.
2611      *
2612      * @param db
2613      * @param ignorePaths
2614      * @param dcBuilder
2615      * @throws IOException
2616      */
2617     public static List<DirCacheEntry> getTreeEntries(Repository db, String branch, Collection<String> ignorePaths) throws IOException {
2618         List<DirCacheEntry> list = new ArrayList<DirCacheEntry>();
2619         TreeWalk tw = null;
2620         try {
2621             ObjectId treeId = db.resolve(branch + "^{tree}");
2622             if (treeId == null) {
2623                 // branch does not exist yet
2624                 return list;
2625             }
2626             tw = new TreeWalk(db);
2627             int hIdx = tw.addTree(treeId);
2628             tw.setRecursive(true);
2629
2630             while (tw.next()) {
2631                 String path = tw.getPathString();
2632                 CanonicalTreeParser hTree = null;
2633                 if (hIdx != -1) {
2634                     hTree = tw.getTree(hIdx, CanonicalTreeParser.class);
2635                 }
2636                 if (!ignorePaths.contains(path)) {
2637                     // add all other tree entries
2638                     if (hTree != null) {
2639                         final DirCacheEntry entry = new DirCacheEntry(path);
2640                         entry.setObjectId(hTree.getEntryObjectId());
2641                         entry.setFileMode(hTree.getEntryFileMode());
2642                         list.add(entry);
2643                     }
2644                 }
2645             }
2646         } finally {
2647             if (tw != null) {
2648                 tw.close();
2649             }
2650         }
2651         return list;
2652     }
2653     
2654     public static boolean commitIndex(Repository db, String branch, DirCache index,
2655                                       ObjectId parentId, boolean forceCommit,
2656                                       String author, String authorEmail, String message) throws IOException, ConcurrentRefUpdateException {
2657         boolean success = false;
2658
2659         ObjectId headId = db.resolve(branch + "^{commit}");
2660         ObjectId baseId = parentId;
2661         if (baseId == null || headId == null) { return false; }
2662         
2663         ObjectInserter odi = db.newObjectInserter();
2664         try {
2665             // Create the in-memory index of the new/updated ticket
2666             ObjectId indexTreeId = index.writeTree(odi);
2667
2668             // Create a commit object
2669             PersonIdent ident = new PersonIdent(author, authorEmail);
2670             
2671             if (forceCommit == false) {
2672                 ThreeWayMerger merger = MergeStrategy.RECURSIVE.newMerger(db, true);
2673                 merger.setObjectInserter(odi);
2674                 merger.setBase(baseId);
2675                 boolean mergeSuccess = merger.merge(indexTreeId, headId);
2676                 
2677                 if (mergeSuccess) {
2678                     indexTreeId = merger.getResultTreeId();
2679                  } else {
2680                     //Manual merge required
2681                     return false; 
2682                  }
2683             }
2684             
2685             CommitBuilder commit = new CommitBuilder();
2686             commit.setAuthor(ident);
2687             commit.setCommitter(ident);
2688             commit.setEncoding(com.gitblit.Constants.ENCODING);
2689             commit.setMessage(message);
2690             commit.setParentId(headId);
2691             commit.setTreeId(indexTreeId);
2692
2693             // Insert the commit into the repository
2694             ObjectId commitId = odi.insert(commit);
2695             odi.flush();
2696
2697             RevWalk revWalk = new RevWalk(db);
2698             try {
2699                 RevCommit revCommit = revWalk.parseCommit(commitId);
2700                 RefUpdate ru = db.updateRef(branch);
2701                 ru.setForceUpdate(forceCommit);
2702                 ru.setNewObjectId(commitId);
2703                 ru.setExpectedOldObjectId(headId);
2704                 ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false);
2705                 Result rc = ru.update();
2706
2707                 switch (rc) {
2708                 case NEW:
2709                 case FORCED:
2710                 case FAST_FORWARD:
2711                     success = true;
2712                     break;
2713                 case REJECTED:
2714                 case LOCK_FAILURE:
2715                     throw new ConcurrentRefUpdateException(JGitText.get().couldNotLockHEAD,
2716                             ru.getRef(), rc);
2717                 default:
2718                     throw new JGitInternalException(MessageFormat.format(
2719                             JGitText.get().updatingRefFailed, branch, commitId.toString(),
2720                             rc));
2721                 }
2722             } finally {
2723                 revWalk.close();
2724             }
2725         } finally {
2726             odi.close();
2727         }
2728         return success;
2729     }
795ce2 2730     
PM 2731     /**
2732      * Returns true if the commit identified by commitId is at the tip of it's branch.
2733      *
2734      * @param repository
2735      * @param commitId
2736      * @return true if the given commit is the tip
2737      */
2738     public static boolean isTip(Repository repository, String commitId) {
2739         try {
2740             RefModel tip = getBranch(repository, commitId);
2741             return (tip != null);    
2742         } catch (Exception e) {
2743             LOGGER.error("Failed to determine isTip", e);
2744         }
2745         return false;
2746     }
275496 2747
5fe7df 2748 }