You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ChangeIdUtil.java 8.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. /*
  2. * Copyright (C) 2010, Robin Rosenberg and others
  3. *
  4. * This program and the accompanying materials are made available under the
  5. * terms of the Eclipse Distribution License v. 1.0 which is available at
  6. * https://www.eclipse.org/org/documents/edl-v10.php.
  7. *
  8. * SPDX-License-Identifier: BSD-3-Clause
  9. */
  10. package org.eclipse.jgit.util;
  11. import java.util.regex.Pattern;
  12. import org.eclipse.jgit.lib.Constants;
  13. import org.eclipse.jgit.lib.ObjectId;
  14. import org.eclipse.jgit.lib.ObjectInserter;
  15. import org.eclipse.jgit.lib.PersonIdent;
  16. /**
  17. * Utilities for creating and working with Change-Id's, like the one used by
  18. * Gerrit Code Review.
  19. * <p>
  20. * A Change-Id is a SHA-1 computed from the content of a commit, in a similar
  21. * fashion to how the commit id is computed. Unlike the commit id a Change-Id is
  22. * retained in the commit and subsequent revised commits in the footer of the
  23. * commit text.
  24. */
  25. public class ChangeIdUtil {
  26. static final String CHANGE_ID = "Change-Id:"; //$NON-NLS-1$
  27. // package-private so the unit test can test this part only
  28. @SuppressWarnings("nls")
  29. static String clean(String msg) {
  30. return msg.//
  31. replaceAll("(?i)(?m)^Signed-off-by:.*$\n?", "").// //$NON-NLS-1$
  32. replaceAll("(?m)^#.*$\n?", "").// //$NON-NLS-1$
  33. replaceAll("(?m)\n\n\n+", "\\\n").// //$NON-NLS-1$
  34. replaceAll("\\n*$", "").// //$NON-NLS-1$
  35. replaceAll("(?s)\ndiff --git.*", "").// //$NON-NLS-1$
  36. trim();
  37. }
  38. /**
  39. * Compute a Change-Id.
  40. *
  41. * @param treeId
  42. * The id of the tree that would be committed
  43. * @param firstParentId
  44. * parent id of previous commit or null
  45. * @param author
  46. * the {@link org.eclipse.jgit.lib.PersonIdent} for the presumed
  47. * author and time
  48. * @param committer
  49. * the {@link org.eclipse.jgit.lib.PersonIdent} for the presumed
  50. * committer and time
  51. * @param message
  52. * The commit message
  53. * @return the change id SHA1 string (without the 'I') or null if the
  54. * message is not complete enough
  55. */
  56. public static ObjectId computeChangeId(final ObjectId treeId,
  57. final ObjectId firstParentId, final PersonIdent author,
  58. final PersonIdent committer, final String message) {
  59. String cleanMessage = clean(message);
  60. if (cleanMessage.length() == 0)
  61. return null;
  62. StringBuilder b = new StringBuilder();
  63. b.append("tree "); //$NON-NLS-1$
  64. b.append(ObjectId.toString(treeId));
  65. b.append("\n"); //$NON-NLS-1$
  66. if (firstParentId != null) {
  67. b.append("parent "); //$NON-NLS-1$
  68. b.append(ObjectId.toString(firstParentId));
  69. b.append("\n"); //$NON-NLS-1$
  70. }
  71. b.append("author "); //$NON-NLS-1$
  72. b.append(author.toExternalString());
  73. b.append("\n"); //$NON-NLS-1$
  74. b.append("committer "); //$NON-NLS-1$
  75. b.append(committer.toExternalString());
  76. b.append("\n\n"); //$NON-NLS-1$
  77. b.append(cleanMessage);
  78. try (ObjectInserter f = new ObjectInserter.Formatter()) {
  79. return f.idFor(Constants.OBJ_COMMIT, Constants.encode(b.toString()));
  80. }
  81. }
  82. private static final Pattern issuePattern = Pattern
  83. .compile("^(Bug|Issue)[a-zA-Z0-9-]*:.*$"); //$NON-NLS-1$
  84. private static final Pattern footerPattern = Pattern
  85. .compile("(^[a-zA-Z0-9-]+:(?!//).*$)"); //$NON-NLS-1$
  86. private static final Pattern changeIdPattern = Pattern
  87. .compile("(^" + CHANGE_ID + " *I[a-f0-9]{40}$)"); //$NON-NLS-1$ //$NON-NLS-2$
  88. private static final Pattern includeInFooterPattern = Pattern
  89. .compile("^[ \\[].*$"); //$NON-NLS-1$
  90. private static final Pattern trailingWhitespace = Pattern.compile("\\s+$"); //$NON-NLS-1$
  91. /**
  92. * Find the right place to insert a Change-Id and return it.
  93. * <p>
  94. * The Change-Id is inserted before the first footer line but after a Bug
  95. * line.
  96. *
  97. * @param message
  98. * a message.
  99. * @param changeId
  100. * a Change-Id.
  101. * @return a commit message with an inserted Change-Id line
  102. */
  103. public static String insertId(String message, ObjectId changeId) {
  104. return insertId(message, changeId, false);
  105. }
  106. /**
  107. * Find the right place to insert a Change-Id and return it.
  108. * <p>
  109. * If no Change-Id is found the Change-Id is inserted before the first
  110. * footer line but after a Bug line.
  111. *
  112. * If Change-Id is found and replaceExisting is set to false, the message is
  113. * unchanged.
  114. *
  115. * If Change-Id is found and replaceExisting is set to true, the Change-Id
  116. * is replaced with {@code changeId}.
  117. *
  118. * @param message
  119. * a message.
  120. * @param changeId
  121. * a Change-Id.
  122. * @param replaceExisting
  123. * a boolean.
  124. * @return a commit message with an inserted Change-Id line
  125. */
  126. public static String insertId(String message, ObjectId changeId,
  127. boolean replaceExisting) {
  128. int indexOfChangeId = indexOfChangeId(message, "\n"); //$NON-NLS-1$
  129. if (indexOfChangeId > 0) {
  130. if (!replaceExisting) {
  131. return message;
  132. }
  133. StringBuilder ret = new StringBuilder(
  134. message.substring(0, indexOfChangeId));
  135. ret.append(CHANGE_ID);
  136. ret.append(" I"); //$NON-NLS-1$
  137. ret.append(ObjectId.toString(changeId));
  138. int indexOfNextLineBreak = message.indexOf('\n',
  139. indexOfChangeId);
  140. if (indexOfNextLineBreak > 0)
  141. ret.append(message.substring(indexOfNextLineBreak));
  142. return ret.toString();
  143. }
  144. String[] lines = message.split("\n"); //$NON-NLS-1$
  145. int footerFirstLine = indexOfFirstFooterLine(lines);
  146. int insertAfter = footerFirstLine;
  147. for (int i = footerFirstLine; i < lines.length; ++i) {
  148. if (issuePattern.matcher(lines[i]).matches()) {
  149. insertAfter = i + 1;
  150. continue;
  151. }
  152. break;
  153. }
  154. StringBuilder ret = new StringBuilder();
  155. int i = 0;
  156. for (; i < insertAfter; ++i) {
  157. ret.append(lines[i]);
  158. ret.append("\n"); //$NON-NLS-1$
  159. }
  160. if (insertAfter == lines.length && insertAfter == footerFirstLine)
  161. ret.append("\n"); //$NON-NLS-1$
  162. ret.append(CHANGE_ID);
  163. ret.append(" I"); //$NON-NLS-1$
  164. ret.append(ObjectId.toString(changeId));
  165. ret.append("\n"); //$NON-NLS-1$
  166. for (; i < lines.length; ++i) {
  167. ret.append(lines[i]);
  168. ret.append("\n"); //$NON-NLS-1$
  169. }
  170. return ret.toString();
  171. }
  172. /**
  173. * Return the index in the String {@code message} where the Change-Id entry
  174. * in the footer begins. If there are more than one entries matching the
  175. * pattern, return the index of the last one in the last section. Because of
  176. * Bug: 400818 we release the constraint here that a footer must contain
  177. * only lines matching {@code footerPattern}.
  178. *
  179. * @param message
  180. * a message.
  181. * @param delimiter
  182. * the line delimiter, like "\n" or "\r\n", needed to find the
  183. * footer
  184. * @return the index of the ChangeId footer in the message, or -1 if no
  185. * ChangeId footer available
  186. */
  187. public static int indexOfChangeId(String message, String delimiter) {
  188. String[] lines = message.split(delimiter);
  189. if (lines.length == 0)
  190. return -1;
  191. int indexOfChangeIdLine = 0;
  192. boolean inFooter = false;
  193. for (int i = lines.length - 1; i >= 0; --i) {
  194. if (!inFooter && isEmptyLine(lines[i]))
  195. continue;
  196. inFooter = true;
  197. if (changeIdPattern.matcher(trimRight(lines[i])).matches()) {
  198. indexOfChangeIdLine = i;
  199. break;
  200. } else if (isEmptyLine(lines[i]) || i == 0)
  201. return -1;
  202. }
  203. int indexOfChangeIdLineinString = 0;
  204. for (int i = 0; i < indexOfChangeIdLine; ++i)
  205. indexOfChangeIdLineinString += lines[i].length()
  206. + delimiter.length();
  207. return indexOfChangeIdLineinString
  208. + lines[indexOfChangeIdLine].indexOf(CHANGE_ID);
  209. }
  210. private static boolean isEmptyLine(String line) {
  211. return line.trim().length() == 0;
  212. }
  213. private static String trimRight(String s) {
  214. return trailingWhitespace.matcher(s).replaceAll(""); //$NON-NLS-1$
  215. }
  216. /**
  217. * Find the index of the first line of the footer paragraph in an array of
  218. * the lines, or lines.length if no footer is available
  219. *
  220. * @param lines
  221. * the commit message split into lines and the line delimiters
  222. * stripped off
  223. * @return the index of the first line of the footer paragraph, or
  224. * lines.length if no footer is available
  225. */
  226. public static int indexOfFirstFooterLine(String[] lines) {
  227. int footerFirstLine = lines.length;
  228. for (int i = lines.length - 1; i > 1; --i) {
  229. if (footerPattern.matcher(lines[i]).matches()) {
  230. footerFirstLine = i;
  231. continue;
  232. }
  233. if (footerFirstLine != lines.length && lines[i].length() == 0)
  234. break;
  235. if (footerFirstLine != lines.length
  236. && includeInFooterPattern.matcher(lines[i]).matches()) {
  237. footerFirstLine = i + 1;
  238. continue;
  239. }
  240. footerFirstLine = lines.length;
  241. break;
  242. }
  243. return footerFirstLine;
  244. }
  245. }