PageRenderTime 53ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 1ms

/jgit-flow-core/src/main/java/com/atlassian/jgitflow/core/util/GitHelper.java

https://bitbucket.org/lukejackson/jgit-flow
Java | 614 lines | 440 code | 75 blank | 99 comment | 55 complexity | f6e58e6940107246f88b75b5dc74aeb1 MD5 | raw file
Possible License(s): Apache-2.0
  1. package com.atlassian.jgitflow.core.util;
  2. import java.io.IOException;
  3. import java.util.*;
  4. import com.atlassian.jgitflow.core.JGitFlowConstants;
  5. import com.atlassian.jgitflow.core.JGitFlowReporter;
  6. import com.atlassian.jgitflow.core.exception.JGitFlowGitAPIException;
  7. import com.atlassian.jgitflow.core.exception.JGitFlowIOException;
  8. import com.atlassian.jgitflow.core.exception.LocalBranchMissingException;
  9. import org.eclipse.jgit.api.Git;
  10. import org.eclipse.jgit.api.ListBranchCommand;
  11. import org.eclipse.jgit.api.errors.GitAPIException;
  12. import org.eclipse.jgit.lib.*;
  13. import org.eclipse.jgit.revwalk.RevCommit;
  14. import org.eclipse.jgit.revwalk.RevWalk;
  15. import org.eclipse.jgit.revwalk.filter.RevFilter;
  16. import org.eclipse.jgit.treewalk.FileTreeIterator;
  17. import org.eclipse.jgit.util.StringUtils;
  18. import static com.atlassian.jgitflow.core.util.Preconditions.checkNotNull;
  19. /**
  20. * A helper class for common Git operations
  21. */
  22. public class GitHelper
  23. {
  24. /**
  25. * Checks to see if one branch is merged into another
  26. *
  27. * @param git The git instance to use
  28. * @param commitString The name of the commit we're testing
  29. * @param baseBranch The name of the base branch to look for the merge
  30. * @return if the contents of branchName has been merged into baseName
  31. * @throws com.atlassian.jgitflow.core.exception.LocalBranchMissingException
  32. * @throws com.atlassian.jgitflow.core.exception.JGitFlowIOException
  33. * @throws com.atlassian.jgitflow.core.exception.JGitFlowGitAPIException
  34. */
  35. public static boolean isMergedInto(Git git, String commitString, String baseBranch) throws LocalBranchMissingException, JGitFlowIOException, JGitFlowGitAPIException
  36. {
  37. RevCommit branchCommit = getCommitForString(git, commitString);
  38. return isMergedInto(git, branchCommit, baseBranch);
  39. }
  40. /**
  41. * Gets a commit for a given string with no body
  42. *
  43. * @param git The git instance to use
  44. * @param commitId The name of the commit to find
  45. * @return The commit
  46. * @throws com.atlassian.jgitflow.core.exception.JGitFlowIOException
  47. */
  48. public static RevCommit getCommitForString(Git git, String commitId) throws JGitFlowIOException, LocalBranchMissingException
  49. {
  50. RevWalk walk = null;
  51. try
  52. {
  53. ObjectId commit = git.getRepository().resolve(commitId);
  54. if (null == commit)
  55. {
  56. throw new LocalBranchMissingException("commit " + commitId + " does not exist");
  57. }
  58. walk = new RevWalk(git.getRepository());
  59. walk.setRetainBody(true);
  60. return walk.parseCommit(commit);
  61. }
  62. catch (IOException e)
  63. {
  64. throw new JGitFlowIOException(e);
  65. }
  66. finally
  67. {
  68. if (null != walk)
  69. {
  70. walk.release();
  71. }
  72. }
  73. }
  74. /**
  75. * Checks to see if a specific commit is merged into a branch
  76. *
  77. * @param git The git instance to use
  78. * @param commit The commit to test
  79. * @param branchName The name of the base branch to look for the merge
  80. * @return if the contents of commit has been merged into baseName
  81. * @throws com.atlassian.jgitflow.core.exception.JGitFlowGitAPIException
  82. * @throws com.atlassian.jgitflow.core.exception.JGitFlowIOException
  83. */
  84. public static boolean isMergedInto(Git git, RevCommit commit, String branchName) throws JGitFlowGitAPIException, JGitFlowIOException
  85. {
  86. Repository repo = git.getRepository();
  87. try
  88. {
  89. ObjectId base = repo.resolve(branchName);
  90. if (null == base)
  91. {
  92. return false;
  93. }
  94. Iterable<RevCommit> baseCommits = git.log().add(base).call();
  95. boolean merged = false;
  96. for (RevCommit entry : baseCommits)
  97. {
  98. if (entry.getId().equals(commit))
  99. {
  100. merged = true;
  101. break;
  102. }
  103. if (entry.getParentCount() > 1 && Arrays.asList(entry.getParents()).contains(commit))
  104. {
  105. merged = true;
  106. break;
  107. }
  108. }
  109. return merged;
  110. }
  111. catch (GitAPIException e)
  112. {
  113. throw new JGitFlowGitAPIException(e);
  114. }
  115. catch (IOException e)
  116. {
  117. throw new JGitFlowIOException(e);
  118. }
  119. }
  120. /**
  121. * Gets the latest commit for a branch
  122. *
  123. * @param git The git instance to use
  124. * @param branchName The name of the branch to find the commit on
  125. * @return The latest commit for the branch
  126. * @throws com.atlassian.jgitflow.core.exception.JGitFlowIOException
  127. */
  128. public static RevCommit getLatestCommit(Git git, String branchName) throws JGitFlowIOException
  129. {
  130. RevWalk walk = null;
  131. try
  132. {
  133. ObjectId branch = git.getRepository().resolve(branchName);
  134. walk = new RevWalk(git.getRepository());
  135. walk.setRetainBody(true);
  136. return walk.parseCommit(branch);
  137. }
  138. catch (IOException e)
  139. {
  140. throw new JGitFlowIOException(e);
  141. }
  142. finally
  143. {
  144. if (null != walk)
  145. {
  146. walk.release();
  147. }
  148. }
  149. }
  150. /**
  151. * Checks to see if a local branch with the given name exists
  152. *
  153. * @param git The git instance to use
  154. * @param branchName The name of the branch to look for
  155. * @return if the branch exists or not
  156. * @throws com.atlassian.jgitflow.core.exception.JGitFlowGitAPIException
  157. */
  158. public static boolean localBranchExists(Git git, String branchName) throws JGitFlowGitAPIException
  159. {
  160. boolean exists = false;
  161. if (StringUtils.isEmptyOrNull(branchName))
  162. {
  163. return exists;
  164. }
  165. try
  166. {
  167. List<Ref> refs = git.branchList().setListMode(null).call();
  168. for (Ref ref : refs)
  169. {
  170. String simpleName = ref.getName().substring(ref.getName().indexOf(Constants.R_HEADS) + Constants.R_HEADS.length());
  171. if (simpleName.equals(branchName))
  172. {
  173. exists = true;
  174. break;
  175. }
  176. }
  177. return exists;
  178. }
  179. catch (GitAPIException e)
  180. {
  181. throw new JGitFlowGitAPIException(e);
  182. }
  183. }
  184. /**
  185. * Checks to see if a remote branch with the given name exists
  186. *
  187. * @param git The git instance to use
  188. * @param branch The name of the branch to look for
  189. * @return if the branch exists or not
  190. * @throws com.atlassian.jgitflow.core.exception.JGitFlowGitAPIException
  191. */
  192. public static boolean remoteBranchExists(Git git, final String branch, JGitFlowReporter reporter) throws JGitFlowGitAPIException
  193. {
  194. reporter.debugMethod(getName(), "remoteBranchExists");
  195. reporter.debugText(getName(), "checking for branch: " + branch);
  196. boolean exists = false;
  197. if (StringUtils.isEmptyOrNull(branch))
  198. {
  199. return exists;
  200. }
  201. try
  202. {
  203. List<Ref> refs = git.branchList().setListMode(ListBranchCommand.ListMode.REMOTE).call();
  204. reporter.debugText(getName(), "got " + refs.size() + " remote refs");
  205. for (Ref ref : refs)
  206. {
  207. reporter.debugText(getName(), "ref name: " + ref.getName());
  208. //if we're not coming from origin, just ignore
  209. if (!ref.getName().contains(JGitFlowConstants.R_REMOTE_ORIGIN))
  210. {
  211. continue;
  212. }
  213. String simpleName = ref.getName().substring(ref.getName().indexOf(JGitFlowConstants.R_REMOTE_ORIGIN) + JGitFlowConstants.R_REMOTE_ORIGIN.length());
  214. reporter.debugText(getName(), "ref simple name: " + simpleName);
  215. reporter.debugText(getName(), "simple name equals branch? " + simpleName.equals(branch));
  216. if (simpleName.equals(branch))
  217. {
  218. exists = true;
  219. break;
  220. }
  221. }
  222. return exists;
  223. }
  224. catch (GitAPIException e)
  225. {
  226. throw new JGitFlowGitAPIException(e);
  227. }
  228. finally
  229. {
  230. reporter.endMethod();
  231. reporter.flush();
  232. }
  233. }
  234. public static boolean localBranchBehindRemote(Git git, final String branch, JGitFlowReporter reporter) throws JGitFlowIOException
  235. {
  236. final RevWalk walk = new RevWalk(git.getRepository());
  237. walk.setRetainBody(true);
  238. boolean behind = false;
  239. try
  240. {
  241. Ref remote = getRemoteBranch(git, branch);
  242. Ref local = getLocalBranch(git, branch);
  243. checkNotNull(remote);
  244. checkNotNull(local);
  245. ObjectId remoteId = git.getRepository().resolve(remote.getObjectId().getName());
  246. RevCommit remoteCommit = walk.parseCommit(remoteId);
  247. RevCommit localCommit = walk.parseCommit(local.getObjectId());
  248. if (!localCommit.equals(remoteCommit))
  249. {
  250. reporter.debugText(getName(), localCommit.getName() + " !equals " + remoteCommit.getName());
  251. behind = true;
  252. walk.setRevFilter(RevFilter.MERGE_BASE);
  253. walk.markStart(localCommit);
  254. walk.markStart(remoteCommit);
  255. RevCommit base = walk.next();
  256. reporter.debugText(getName(), "checking if remote is at our merge base");
  257. if (null != base)
  258. {
  259. walk.parseBody(base);
  260. //remote is behind
  261. if (remoteCommit.equals(base))
  262. {
  263. reporter.debugText(getName(), "remote equals merge base, branch is newer");
  264. behind = false;
  265. }
  266. }
  267. }
  268. }
  269. catch (IOException e)
  270. {
  271. reporter.errorText(getName(), e.getMessage());
  272. reporter.endMethod();
  273. reporter.flush();
  274. throw new JGitFlowIOException(e);
  275. }
  276. finally
  277. {
  278. walk.release();
  279. }
  280. return behind;
  281. }
  282. /**
  283. * Gets a reference to a remote branch with the given name
  284. *
  285. * @param git The git instance to use
  286. * @param branchName The name of the remote branch
  287. * @return A reference to the remote branch or null
  288. * @throws com.atlassian.jgitflow.core.exception.JGitFlowIOException
  289. */
  290. public static Ref getRemoteBranch(Git git, String branchName) throws JGitFlowIOException
  291. {
  292. try
  293. {
  294. final Map<String, Ref> refList = git.getRepository().getRefDatabase().getRefs(Constants.R_REMOTES);
  295. Ref remote = null;
  296. for (Map.Entry<String, Ref> entry : refList.entrySet())
  297. {
  298. int index = entry.getValue().getName().indexOf(JGitFlowConstants.R_REMOTE_ORIGIN);
  299. if (index < 0)
  300. {
  301. continue;
  302. }
  303. String simpleName = entry.getValue().getName().substring(index + JGitFlowConstants.R_REMOTE_ORIGIN.length());
  304. if (simpleName.equals(branchName))
  305. {
  306. remote = entry.getValue();
  307. break;
  308. }
  309. }
  310. return remote;
  311. }
  312. catch (IOException e)
  313. {
  314. throw new JGitFlowIOException(e);
  315. }
  316. }
  317. /**
  318. * Gets a reference to a local branch with the given name
  319. *
  320. * @param git The git instance to use
  321. * @param branchName The name of the remote branch
  322. * @return A reference to the local branch or null
  323. * @throws com.atlassian.jgitflow.core.exception.JGitFlowIOException
  324. */
  325. public static Ref getLocalBranch(Git git, String branchName) throws JGitFlowIOException
  326. {
  327. try
  328. {
  329. Ref ref2check = git.getRepository().getRef(branchName);
  330. Ref local = null;
  331. if (ref2check != null && ref2check.getName().startsWith(Constants.R_HEADS))
  332. {
  333. local = ref2check;
  334. }
  335. return local;
  336. }
  337. catch (IOException e)
  338. {
  339. throw new JGitFlowIOException(e);
  340. }
  341. }
  342. /**
  343. * Gets a list of branch references that begin with the given prefix
  344. *
  345. * @param git The git instance to use
  346. * @param prefix The prefix to test for
  347. * @return A list of branch references matching the given prefix
  348. * @throws com.atlassian.jgitflow.core.exception.JGitFlowGitAPIException
  349. */
  350. public static List<Ref> listBranchesWithPrefix(Git git, String prefix, JGitFlowReporter reporter) throws JGitFlowGitAPIException
  351. {
  352. List<Ref> branches = new ArrayList<Ref>();
  353. reporter.debugMethod(getName(), "listBranchesWithPrefix");
  354. try
  355. {
  356. List<Ref> refs = git.branchList().setListMode(ListBranchCommand.ListMode.ALL).call();
  357. for (Ref ref : refs)
  358. {
  359. String simpleName;
  360. String originPrefix = Constants.R_REMOTES + Constants.DEFAULT_REMOTE_NAME + "/";
  361. if (ref.getName().indexOf(Constants.R_HEADS) > -1)
  362. {
  363. simpleName = ref.getName().substring(ref.getName().indexOf(Constants.R_HEADS) + Constants.R_HEADS.length());
  364. }
  365. else if (ref.getName().indexOf(originPrefix) > -1)
  366. {
  367. simpleName = ref.getName().substring(ref.getName().indexOf(originPrefix) + originPrefix.length());
  368. }
  369. else
  370. {
  371. simpleName = "";
  372. }
  373. reporter.debugText(getName(), "simple name [" + simpleName + "] startsWith prefix [" + prefix + "] ? " + simpleName.startsWith(prefix));
  374. if (simpleName.startsWith(prefix))
  375. {
  376. branches.add(ref);
  377. }
  378. }
  379. return branches;
  380. }
  381. catch (GitAPIException e)
  382. {
  383. throw new JGitFlowGitAPIException(e);
  384. }
  385. finally
  386. {
  387. reporter.endMethod();
  388. }
  389. }
  390. /**
  391. * Tests to see if a working folder is clean. e.g. all changes have been committed.
  392. *
  393. * @param git The git instance to use
  394. * @param allowUntracked
  395. * @param reporter @return if the branch is clean or not
  396. * @throws com.atlassian.jgitflow.core.exception.JGitFlowIOException
  397. * @throws com.atlassian.jgitflow.core.exception.JGitFlowGitAPIException
  398. */
  399. public static CleanStatus workingTreeIsClean(Git git, boolean allowUntracked, JGitFlowReporter reporter) throws JGitFlowIOException, JGitFlowGitAPIException
  400. {
  401. reporter.debugMethod(getName(), "workingTreeIsClean");
  402. try
  403. {
  404. IndexDiff diffIndex = new IndexDiff(git.getRepository(), Constants.HEAD, new FileTreeIterator(git.getRepository()));
  405. if (diffIndex.diff())
  406. {
  407. int addedSize = diffIndex.getAdded().size();
  408. int assumedSize = diffIndex.getAssumeUnchanged().size();
  409. int changedSize = diffIndex.getChanged().size();
  410. int conflictSize = diffIndex.getConflicting().size();
  411. int ignoredSize = diffIndex.getIgnoredNotInIndex().size();
  412. int missingSize = diffIndex.getMissing().size();
  413. int modifiedSize = diffIndex.getModified().size();
  414. int removedSize = diffIndex.getRemoved().size();
  415. int untrackedSize = diffIndex.getUntracked().size();
  416. int untrackedFolderSize = diffIndex.getUntrackedFolders().size();
  417. boolean changed = false;
  418. boolean untracked = false;
  419. StringBuilder sb = new StringBuilder();
  420. reporter.debugText(getName(), "diffIndex.diff() returned diffs. working tree is dirty!");
  421. reporter.debugText(getName(), "added size: " + addedSize);
  422. reportDirtyDetails(getName(), "added", diffIndex.getAdded(), reporter);
  423. reporter.debugText(getName(), "assume unchanged size: " + assumedSize);
  424. reporter.debugText(getName(), "changed size: " + changedSize);
  425. reportDirtyDetails(getName(), "changed", diffIndex.getChanged(), reporter);
  426. reporter.debugText(getName(), "conflicting size: " + conflictSize);
  427. reportDirtyDetails(getName(), "conflicting", diffIndex.getConflicting(), reporter);
  428. reporter.debugText(getName(), "ignored not in index size: " + ignoredSize);
  429. reporter.debugText(getName(), "missing size: " + missingSize);
  430. reportDirtyDetails(getName(), "missing", diffIndex.getMissing(), reporter);
  431. reporter.debugText(getName(), "modified size: " + modifiedSize);
  432. reportDirtyDetails(getName(), "modified", diffIndex.getModified(), reporter);
  433. reporter.debugText(getName(), "removed size: " + removedSize);
  434. reportDirtyDetails(getName(), "removed", diffIndex.getRemoved(), reporter);
  435. reporter.debugText(getName(), "untracked size: " + untrackedSize);
  436. reportDirtyDetails(getName(), "untracked", diffIndex.getUntracked(), reporter);
  437. reporter.debugText(getName(), "untracked folders size: " + untrackedFolderSize);
  438. reportDirtyDetails(getName(), "untracked folders", diffIndex.getUntrackedFolders(), reporter);
  439. reporter.endMethod();
  440. if (addedSize > 0 || changedSize > 0 || conflictSize > 0 || missingSize > 0 || modifiedSize > 0 || removedSize > 0)
  441. {
  442. changed = true;
  443. sb.append("Working tree has uncommitted changes");
  444. }
  445. if (!allowUntracked && (untrackedSize > 0 || untrackedFolderSize > 0))
  446. {
  447. if (ignoredSize > 0)
  448. {
  449. Set<String> ignores = diffIndex.getIgnoredNotInIndex();
  450. if (untrackedSize > 0)
  451. {
  452. Set<String> utFiles = diffIndex.getUntracked();
  453. utFiles.removeAll(ignores);
  454. untrackedSize = utFiles.size();
  455. }
  456. if (untrackedFolderSize > 0)
  457. {
  458. Set<String> utFolders = diffIndex.getUntrackedFolders();
  459. utFolders.removeAll(ignores);
  460. untrackedFolderSize = utFolders.size();
  461. }
  462. }
  463. if (untrackedSize > 0 || untrackedFolderSize > 0)
  464. {
  465. untracked = true;
  466. }
  467. if (!changed)
  468. {
  469. sb.append("Working tree has untracked files");
  470. }
  471. else
  472. {
  473. sb.append(" and untracked files");
  474. }
  475. }
  476. return new CleanStatus(untracked, changed, sb.toString());
  477. }
  478. reporter.debugText(getName(), "working tree is clean");
  479. reporter.endMethod();
  480. return new CleanStatus(false, false, "Working tree is clean");
  481. }
  482. catch (IOException e)
  483. {
  484. reporter.errorText(getName(), e.getMessage());
  485. reporter.endMethod();
  486. reporter.flush();
  487. throw new JGitFlowIOException(e);
  488. }
  489. }
  490. private static void reportDirtyDetails(String cmdName, String reason, Set<String> files, JGitFlowReporter reporter)
  491. {
  492. if (files.size() > 0)
  493. {
  494. reporter.debugText(cmdName, reason + " details: ");
  495. for (String file : files)
  496. {
  497. reporter.debugText(cmdName, " -- " + reason + ": " + file);
  498. }
  499. }
  500. }
  501. /**
  502. * Tests to see if a tag exists with the given name
  503. *
  504. * @param git The git instance to use
  505. * @param tagName The name of the tag to test for
  506. * @return if the tag exists or not
  507. * @throws com.atlassian.jgitflow.core.exception.JGitFlowGitAPIException
  508. */
  509. public static boolean tagExists(Git git, final String tagName) throws JGitFlowGitAPIException
  510. {
  511. boolean exists = false;
  512. if (StringUtils.isEmptyOrNull(tagName))
  513. {
  514. return exists;
  515. }
  516. try
  517. {
  518. List<Ref> refs = git.tagList().call();
  519. for (Ref ref : refs)
  520. {
  521. String simpleName = ref.getName().substring(ref.getName().indexOf(Constants.R_TAGS) + Constants.R_TAGS.length());
  522. if (simpleName.equals(tagName))
  523. {
  524. exists = true;
  525. break;
  526. }
  527. }
  528. return exists;
  529. }
  530. catch (GitAPIException e)
  531. {
  532. throw new JGitFlowGitAPIException(e);
  533. }
  534. }
  535. private static String getName()
  536. {
  537. return GitHelper.class.getSimpleName();
  538. }
  539. }