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