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