選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

RefLogUtils.java 25KB

Ticket tracker with patchset contributions A basic issue tracker styled as a hybrid of GitHub and BitBucket issues. You may attach commits to an existing ticket or you can push a single commit to create a *proposal* ticket. Tickets keep track of patchsets (one or more commits) and allow patchset rewriting (rebase, amend, squash) by detecing the non-fast-forward update and assigning a new patchset number to the new commits. Ticket tracker -------------- The ticket tracker stores tickets as an append-only journal of changes. The journals are deserialized and a ticket is built by applying the journal entries. Tickets are indexed using Apache Lucene and all queries and searches are executed against this Lucene index. There is one trade-off to this persistence design: user attributions are non-relational. What does that mean? Each journal entry stores the username of the author. If the username changes in the user service, the journal entry will not reflect that change because the values are hard-coded. Here are a few reasons/justifications for this design choice: 1. commit identifications (author, committer, tagger) are non-relational 2. maintains the KISS principle 3. your favorite text editor can still be your administration tool Persistence Choices ------------------- **FileTicketService**: stores journals on the filesystem **BranchTicketService**: stores journals on an orphan branch **RedisTicketService**: stores journals in a Redis key-value datastore It should be relatively straight-forward to develop other backends (MongoDB, etc) as long as the journal design is preserved. Pushing Commits --------------- Each push to a ticket is identified as a patchset revision. A patchset revision may add commits to the patchset (fast-forward) OR a patchset revision may rewrite history (rebase, squash, rebase+squash, or amend). Patchset authors should not be afraid to polish, revise, and rewrite their code before merging into the proposed branch. Gitblit will create one ref for each patchset. These refs are updated for fast-forward pushes or created for rewrites. They are formatted as `refs/tickets/{shard}/{id}/{patchset}`. The *shard* is the last two digits of the id. If the id < 10, prefix a 0. The *shard* is always two digits long. The shard's purpose is to ensure Gitblit doesn't exceed any filesystem directory limits for file creation. **Creating a Proposal Ticket** You may create a new change proposal ticket just by pushing a **single commit** to `refs/for/{branch}` where branch is the proposed integration branch OR `refs/for/new` or `refs/for/default` which both will use the default repository branch. git push origin HEAD:refs/for/new **Updating a Patchset** The safe way to update an existing patchset is to push to the patchset ref. git push origin HEAD:refs/heads/ticket/{id} This ensures you do not accidentally create a new patchset in the event that the patchset was updated after you last pulled. The not-so-safe way to update an existing patchset is to push using the magic ref. git push origin HEAD:refs/for/{id} This push ref will update an exisitng patchset OR create a new patchset if the update is non-fast-forward. **Rebasing, Squashing, Amending** Gitblit makes rebasing, squashing, and amending patchsets easy. Normally, pushing a non-fast-forward update would require rewind (RW+) repository permissions. Gitblit provides a magic ref which will allow ticket participants to rewrite a ticket patchset as long as the ticket is open. git push origin HEAD:refs/for/{id} Pushing changes to this ref allows the patchset authors to rebase, squash, or amend the patchset commits without requiring client-side use of the *--force* flag on push AND without requiring RW+ permission to the repository. Since each patchset is tracked with a ref it is easy to recover from accidental non-fast-forward updates. Features -------- - Ticket tracker with status changes and responsible assignments - Patchset revision scoring mechanism - Update/Rewrite patchset handling - Close-on-push detection - Server-side Merge button for simple merges - Comments with Markdown syntax support - Rich mail notifications - Voting - Mentions - Watch lists - Querying - Searches - Partial miletones support - Multiple backend options
10年前
Ticket tracker with patchset contributions A basic issue tracker styled as a hybrid of GitHub and BitBucket issues. You may attach commits to an existing ticket or you can push a single commit to create a *proposal* ticket. Tickets keep track of patchsets (one or more commits) and allow patchset rewriting (rebase, amend, squash) by detecing the non-fast-forward update and assigning a new patchset number to the new commits. Ticket tracker -------------- The ticket tracker stores tickets as an append-only journal of changes. The journals are deserialized and a ticket is built by applying the journal entries. Tickets are indexed using Apache Lucene and all queries and searches are executed against this Lucene index. There is one trade-off to this persistence design: user attributions are non-relational. What does that mean? Each journal entry stores the username of the author. If the username changes in the user service, the journal entry will not reflect that change because the values are hard-coded. Here are a few reasons/justifications for this design choice: 1. commit identifications (author, committer, tagger) are non-relational 2. maintains the KISS principle 3. your favorite text editor can still be your administration tool Persistence Choices ------------------- **FileTicketService**: stores journals on the filesystem **BranchTicketService**: stores journals on an orphan branch **RedisTicketService**: stores journals in a Redis key-value datastore It should be relatively straight-forward to develop other backends (MongoDB, etc) as long as the journal design is preserved. Pushing Commits --------------- Each push to a ticket is identified as a patchset revision. A patchset revision may add commits to the patchset (fast-forward) OR a patchset revision may rewrite history (rebase, squash, rebase+squash, or amend). Patchset authors should not be afraid to polish, revise, and rewrite their code before merging into the proposed branch. Gitblit will create one ref for each patchset. These refs are updated for fast-forward pushes or created for rewrites. They are formatted as `refs/tickets/{shard}/{id}/{patchset}`. The *shard* is the last two digits of the id. If the id < 10, prefix a 0. The *shard* is always two digits long. The shard's purpose is to ensure Gitblit doesn't exceed any filesystem directory limits for file creation. **Creating a Proposal Ticket** You may create a new change proposal ticket just by pushing a **single commit** to `refs/for/{branch}` where branch is the proposed integration branch OR `refs/for/new` or `refs/for/default` which both will use the default repository branch. git push origin HEAD:refs/for/new **Updating a Patchset** The safe way to update an existing patchset is to push to the patchset ref. git push origin HEAD:refs/heads/ticket/{id} This ensures you do not accidentally create a new patchset in the event that the patchset was updated after you last pulled. The not-so-safe way to update an existing patchset is to push using the magic ref. git push origin HEAD:refs/for/{id} This push ref will update an exisitng patchset OR create a new patchset if the update is non-fast-forward. **Rebasing, Squashing, Amending** Gitblit makes rebasing, squashing, and amending patchsets easy. Normally, pushing a non-fast-forward update would require rewind (RW+) repository permissions. Gitblit provides a magic ref which will allow ticket participants to rewrite a ticket patchset as long as the ticket is open. git push origin HEAD:refs/for/{id} Pushing changes to this ref allows the patchset authors to rebase, squash, or amend the patchset commits without requiring client-side use of the *--force* flag on push AND without requiring RW+ permission to the repository. Since each patchset is tracked with a ref it is easy to recover from accidental non-fast-forward updates. Features -------- - Ticket tracker with status changes and responsible assignments - Patchset revision scoring mechanism - Update/Rewrite patchset handling - Close-on-push detection - Server-side Merge button for simple merges - Comments with Markdown syntax support - Rich mail notifications - Voting - Mentions - Watch lists - Querying - Searches - Partial miletones support - Multiple backend options
10年前
Ticket tracker with patchset contributions A basic issue tracker styled as a hybrid of GitHub and BitBucket issues. You may attach commits to an existing ticket or you can push a single commit to create a *proposal* ticket. Tickets keep track of patchsets (one or more commits) and allow patchset rewriting (rebase, amend, squash) by detecing the non-fast-forward update and assigning a new patchset number to the new commits. Ticket tracker -------------- The ticket tracker stores tickets as an append-only journal of changes. The journals are deserialized and a ticket is built by applying the journal entries. Tickets are indexed using Apache Lucene and all queries and searches are executed against this Lucene index. There is one trade-off to this persistence design: user attributions are non-relational. What does that mean? Each journal entry stores the username of the author. If the username changes in the user service, the journal entry will not reflect that change because the values are hard-coded. Here are a few reasons/justifications for this design choice: 1. commit identifications (author, committer, tagger) are non-relational 2. maintains the KISS principle 3. your favorite text editor can still be your administration tool Persistence Choices ------------------- **FileTicketService**: stores journals on the filesystem **BranchTicketService**: stores journals on an orphan branch **RedisTicketService**: stores journals in a Redis key-value datastore It should be relatively straight-forward to develop other backends (MongoDB, etc) as long as the journal design is preserved. Pushing Commits --------------- Each push to a ticket is identified as a patchset revision. A patchset revision may add commits to the patchset (fast-forward) OR a patchset revision may rewrite history (rebase, squash, rebase+squash, or amend). Patchset authors should not be afraid to polish, revise, and rewrite their code before merging into the proposed branch. Gitblit will create one ref for each patchset. These refs are updated for fast-forward pushes or created for rewrites. They are formatted as `refs/tickets/{shard}/{id}/{patchset}`. The *shard* is the last two digits of the id. If the id < 10, prefix a 0. The *shard* is always two digits long. The shard's purpose is to ensure Gitblit doesn't exceed any filesystem directory limits for file creation. **Creating a Proposal Ticket** You may create a new change proposal ticket just by pushing a **single commit** to `refs/for/{branch}` where branch is the proposed integration branch OR `refs/for/new` or `refs/for/default` which both will use the default repository branch. git push origin HEAD:refs/for/new **Updating a Patchset** The safe way to update an existing patchset is to push to the patchset ref. git push origin HEAD:refs/heads/ticket/{id} This ensures you do not accidentally create a new patchset in the event that the patchset was updated after you last pulled. The not-so-safe way to update an existing patchset is to push using the magic ref. git push origin HEAD:refs/for/{id} This push ref will update an exisitng patchset OR create a new patchset if the update is non-fast-forward. **Rebasing, Squashing, Amending** Gitblit makes rebasing, squashing, and amending patchsets easy. Normally, pushing a non-fast-forward update would require rewind (RW+) repository permissions. Gitblit provides a magic ref which will allow ticket participants to rewrite a ticket patchset as long as the ticket is open. git push origin HEAD:refs/for/{id} Pushing changes to this ref allows the patchset authors to rebase, squash, or amend the patchset commits without requiring client-side use of the *--force* flag on push AND without requiring RW+ permission to the repository. Since each patchset is tracked with a ref it is easy to recover from accidental non-fast-forward updates. Features -------- - Ticket tracker with status changes and responsible assignments - Patchset revision scoring mechanism - Update/Rewrite patchset handling - Close-on-push detection - Server-side Merge button for simple merges - Comments with Markdown syntax support - Rich mail notifications - Voting - Mentions - Watch lists - Querying - Searches - Partial miletones support - Multiple backend options
10年前
Ticket tracker with patchset contributions A basic issue tracker styled as a hybrid of GitHub and BitBucket issues. You may attach commits to an existing ticket or you can push a single commit to create a *proposal* ticket. Tickets keep track of patchsets (one or more commits) and allow patchset rewriting (rebase, amend, squash) by detecing the non-fast-forward update and assigning a new patchset number to the new commits. Ticket tracker -------------- The ticket tracker stores tickets as an append-only journal of changes. The journals are deserialized and a ticket is built by applying the journal entries. Tickets are indexed using Apache Lucene and all queries and searches are executed against this Lucene index. There is one trade-off to this persistence design: user attributions are non-relational. What does that mean? Each journal entry stores the username of the author. If the username changes in the user service, the journal entry will not reflect that change because the values are hard-coded. Here are a few reasons/justifications for this design choice: 1. commit identifications (author, committer, tagger) are non-relational 2. maintains the KISS principle 3. your favorite text editor can still be your administration tool Persistence Choices ------------------- **FileTicketService**: stores journals on the filesystem **BranchTicketService**: stores journals on an orphan branch **RedisTicketService**: stores journals in a Redis key-value datastore It should be relatively straight-forward to develop other backends (MongoDB, etc) as long as the journal design is preserved. Pushing Commits --------------- Each push to a ticket is identified as a patchset revision. A patchset revision may add commits to the patchset (fast-forward) OR a patchset revision may rewrite history (rebase, squash, rebase+squash, or amend). Patchset authors should not be afraid to polish, revise, and rewrite their code before merging into the proposed branch. Gitblit will create one ref for each patchset. These refs are updated for fast-forward pushes or created for rewrites. They are formatted as `refs/tickets/{shard}/{id}/{patchset}`. The *shard* is the last two digits of the id. If the id < 10, prefix a 0. The *shard* is always two digits long. The shard's purpose is to ensure Gitblit doesn't exceed any filesystem directory limits for file creation. **Creating a Proposal Ticket** You may create a new change proposal ticket just by pushing a **single commit** to `refs/for/{branch}` where branch is the proposed integration branch OR `refs/for/new` or `refs/for/default` which both will use the default repository branch. git push origin HEAD:refs/for/new **Updating a Patchset** The safe way to update an existing patchset is to push to the patchset ref. git push origin HEAD:refs/heads/ticket/{id} This ensures you do not accidentally create a new patchset in the event that the patchset was updated after you last pulled. The not-so-safe way to update an existing patchset is to push using the magic ref. git push origin HEAD:refs/for/{id} This push ref will update an exisitng patchset OR create a new patchset if the update is non-fast-forward. **Rebasing, Squashing, Amending** Gitblit makes rebasing, squashing, and amending patchsets easy. Normally, pushing a non-fast-forward update would require rewind (RW+) repository permissions. Gitblit provides a magic ref which will allow ticket participants to rewrite a ticket patchset as long as the ticket is open. git push origin HEAD:refs/for/{id} Pushing changes to this ref allows the patchset authors to rebase, squash, or amend the patchset commits without requiring client-side use of the *--force* flag on push AND without requiring RW+ permission to the repository. Since each patchset is tracked with a ref it is easy to recover from accidental non-fast-forward updates. Features -------- - Ticket tracker with status changes and responsible assignments - Patchset revision scoring mechanism - Update/Rewrite patchset handling - Close-on-push detection - Server-side Merge button for simple merges - Comments with Markdown syntax support - Rich mail notifications - Voting - Mentions - Watch lists - Querying - Searches - Partial miletones support - Multiple backend options
10年前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750
  1. /*
  2. * Copyright 2013 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. */
  16. package com.gitblit.utils;
  17. import java.io.IOException;
  18. import java.text.DateFormat;
  19. import java.text.MessageFormat;
  20. import java.text.SimpleDateFormat;
  21. import java.util.ArrayList;
  22. import java.util.Arrays;
  23. import java.util.Collection;
  24. import java.util.Collections;
  25. import java.util.Date;
  26. import java.util.HashMap;
  27. import java.util.List;
  28. import java.util.Map;
  29. import java.util.Set;
  30. import java.util.TimeZone;
  31. import java.util.TreeSet;
  32. import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
  33. import org.eclipse.jgit.api.errors.JGitInternalException;
  34. import org.eclipse.jgit.dircache.DirCache;
  35. import org.eclipse.jgit.dircache.DirCacheBuilder;
  36. import org.eclipse.jgit.dircache.DirCacheEntry;
  37. import org.eclipse.jgit.internal.JGitText;
  38. import org.eclipse.jgit.lib.CommitBuilder;
  39. import org.eclipse.jgit.lib.FileMode;
  40. import org.eclipse.jgit.lib.ObjectId;
  41. import org.eclipse.jgit.lib.ObjectInserter;
  42. import org.eclipse.jgit.lib.PersonIdent;
  43. import org.eclipse.jgit.lib.Ref;
  44. import org.eclipse.jgit.lib.RefRename;
  45. import org.eclipse.jgit.lib.RefUpdate;
  46. import org.eclipse.jgit.lib.RefUpdate.Result;
  47. import org.eclipse.jgit.lib.Repository;
  48. import org.eclipse.jgit.revwalk.RevCommit;
  49. import org.eclipse.jgit.revwalk.RevWalk;
  50. import org.eclipse.jgit.transport.ReceiveCommand;
  51. import org.eclipse.jgit.treewalk.CanonicalTreeParser;
  52. import org.eclipse.jgit.treewalk.TreeWalk;
  53. import org.slf4j.Logger;
  54. import org.slf4j.LoggerFactory;
  55. import com.gitblit.Constants;
  56. import com.gitblit.models.DailyLogEntry;
  57. import com.gitblit.models.PathModel.PathChangeModel;
  58. import com.gitblit.models.RefLogEntry;
  59. import com.gitblit.models.RefModel;
  60. import com.gitblit.models.RepositoryCommit;
  61. import com.gitblit.models.UserModel;
  62. /**
  63. * Utility class for maintaining a reflog within a git repository on an
  64. * orphan branch.
  65. *
  66. * @author James Moger
  67. *
  68. */
  69. public class RefLogUtils {
  70. private static final String GB_REFLOG = "refs/gitblit/reflog";
  71. private static final Logger LOGGER = LoggerFactory.getLogger(RefLogUtils.class);
  72. /**
  73. * Log an error message and exception.
  74. *
  75. * @param t
  76. * @param repository
  77. * if repository is not null it MUST be the {0} parameter in the
  78. * pattern.
  79. * @param pattern
  80. * @param objects
  81. */
  82. private static void error(Throwable t, Repository repository, String pattern, Object... objects) {
  83. List<Object> parameters = new ArrayList<Object>();
  84. if (objects != null && objects.length > 0) {
  85. for (Object o : objects) {
  86. parameters.add(o);
  87. }
  88. }
  89. if (repository != null) {
  90. parameters.add(0, repository.getDirectory().getAbsolutePath());
  91. }
  92. LOGGER.error(MessageFormat.format(pattern, parameters.toArray()), t);
  93. }
  94. /**
  95. * Returns true if the repository has a reflog branch.
  96. *
  97. * @param repository
  98. * @return true if the repository has a reflog branch
  99. */
  100. public static boolean hasRefLogBranch(Repository repository) {
  101. try {
  102. return repository.getRef(GB_REFLOG) != null;
  103. } catch(Exception e) {
  104. LOGGER.error("failed to determine hasRefLogBranch", e);
  105. }
  106. return false;
  107. }
  108. /**
  109. * Returns a RefModel for the reflog branch in the repository. If the
  110. * branch can not be found, null is returned.
  111. *
  112. * @param repository
  113. * @return a refmodel for the reflog branch or null
  114. */
  115. public static RefModel getRefLogBranch(Repository repository) {
  116. List<RefModel> refs = JGitUtils.getRefs(repository, com.gitblit.Constants.R_GITBLIT);
  117. RefModel pushLog = null;
  118. final String GB_PUSHES = "refs/gitblit/pushes";
  119. for (RefModel ref : refs) {
  120. if (ref.reference.getName().equals(GB_REFLOG)) {
  121. return ref;
  122. } else if (ref.reference.getName().equals(GB_PUSHES)) {
  123. pushLog = ref;
  124. }
  125. }
  126. if (pushLog != null) {
  127. // rename refs/gitblit/pushes to refs/gitblit/reflog
  128. RefRename cmd;
  129. try {
  130. cmd = repository.renameRef(GB_PUSHES, GB_REFLOG);
  131. cmd.setRefLogIdent(new PersonIdent("Gitblit", "gitblit@localhost"));
  132. cmd.setRefLogMessage("renamed " + GB_PUSHES + " => " + GB_REFLOG);
  133. Result res = cmd.rename();
  134. switch (res) {
  135. case RENAMED:
  136. return getRefLogBranch(repository);
  137. default:
  138. LOGGER.error("failed to rename " + GB_PUSHES + " => " + GB_REFLOG + " (" + res.name() + ")");
  139. }
  140. } catch (IOException e) {
  141. LOGGER.error("failed to rename pushlog", e);
  142. }
  143. }
  144. return null;
  145. }
  146. private static UserModel newUserModelFrom(PersonIdent ident) {
  147. String name = ident.getName();
  148. String username;
  149. String displayname;
  150. if (name.indexOf('/') > -1) {
  151. int slash = name.indexOf('/');
  152. displayname = name.substring(0, slash);
  153. username = name.substring(slash + 1);
  154. } else {
  155. displayname = name;
  156. username = ident.getEmailAddress();
  157. }
  158. UserModel user = new UserModel(username);
  159. user.displayName = displayname;
  160. user.emailAddress = ident.getEmailAddress();
  161. return user;
  162. }
  163. /**
  164. * Logs a ref deletion.
  165. *
  166. * @param user
  167. * @param repository
  168. * @param ref
  169. * @return true, if the update was successful
  170. */
  171. public static boolean deleteRef(UserModel user, Repository repository, Ref ref) {
  172. try {
  173. if (ref == null) {
  174. return false;
  175. }
  176. RefModel reflogBranch = getRefLogBranch(repository);
  177. if (reflogBranch == null) {
  178. return false;
  179. }
  180. List<RevCommit> log = JGitUtils.getRevLog(repository, reflogBranch.getName(), ref.getName(), 0, 1);
  181. if (log.isEmpty()) {
  182. // this ref is not in the reflog branch
  183. return false;
  184. }
  185. ReceiveCommand cmd = new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), ref.getName());
  186. return updateRefLog(user, repository, Arrays.asList(cmd));
  187. } catch (Throwable t) {
  188. error(t, repository, "Failed to commit reflog entry to {0}");
  189. }
  190. return false;
  191. }
  192. /**
  193. * Updates the reflog with the received commands.
  194. *
  195. * @param user
  196. * @param repository
  197. * @param commands
  198. * @return true, if the update was successful
  199. */
  200. public static boolean updateRefLog(UserModel user, Repository repository,
  201. Collection<ReceiveCommand> commands) {
  202. // only track branches and tags
  203. List<ReceiveCommand> filteredCommands = new ArrayList<ReceiveCommand>();
  204. for (ReceiveCommand cmd : commands) {
  205. if (!cmd.getRefName().startsWith(Constants.R_HEADS)
  206. && !cmd.getRefName().startsWith(Constants.R_TAGS)) {
  207. continue;
  208. }
  209. filteredCommands.add(cmd);
  210. }
  211. if (filteredCommands.isEmpty()) {
  212. // nothing to log
  213. return true;
  214. }
  215. RefModel reflogBranch = getRefLogBranch(repository);
  216. if (reflogBranch == null) {
  217. JGitUtils.createOrphanBranch(repository, GB_REFLOG, null);
  218. }
  219. boolean success = false;
  220. String message = "push";
  221. try {
  222. ObjectId headId = repository.resolve(GB_REFLOG + "^{commit}");
  223. ObjectInserter odi = repository.newObjectInserter();
  224. try {
  225. // Create the in-memory index of the push log entry
  226. DirCache index = createIndex(repository, headId, commands);
  227. ObjectId indexTreeId = index.writeTree(odi);
  228. PersonIdent ident;
  229. if (UserModel.ANONYMOUS.equals(user)) {
  230. // anonymous push
  231. ident = new PersonIdent(user.username + "/" + user.username, user.username);
  232. } else {
  233. // construct real pushing account
  234. ident = new PersonIdent(MessageFormat.format("{0}/{1}", user.getDisplayName(), user.username),
  235. user.emailAddress == null ? user.username : user.emailAddress);
  236. }
  237. // Create a commit object
  238. CommitBuilder commit = new CommitBuilder();
  239. commit.setAuthor(ident);
  240. commit.setCommitter(ident);
  241. commit.setEncoding(Constants.ENCODING);
  242. commit.setMessage(message);
  243. commit.setParentId(headId);
  244. commit.setTreeId(indexTreeId);
  245. // Insert the commit into the repository
  246. ObjectId commitId = odi.insert(commit);
  247. odi.flush();
  248. RevWalk revWalk = new RevWalk(repository);
  249. try {
  250. RevCommit revCommit = revWalk.parseCommit(commitId);
  251. RefUpdate ru = repository.updateRef(GB_REFLOG);
  252. ru.setNewObjectId(commitId);
  253. ru.setExpectedOldObjectId(headId);
  254. ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false);
  255. Result rc = ru.forceUpdate();
  256. switch (rc) {
  257. case NEW:
  258. case FORCED:
  259. case FAST_FORWARD:
  260. success = true;
  261. break;
  262. case REJECTED:
  263. case LOCK_FAILURE:
  264. throw new ConcurrentRefUpdateException(JGitText.get().couldNotLockHEAD,
  265. ru.getRef(), rc);
  266. default:
  267. throw new JGitInternalException(MessageFormat.format(
  268. JGitText.get().updatingRefFailed, GB_REFLOG, commitId.toString(),
  269. rc));
  270. }
  271. } finally {
  272. revWalk.release();
  273. }
  274. } finally {
  275. odi.release();
  276. }
  277. } catch (Throwable t) {
  278. error(t, repository, "Failed to commit reflog entry to {0}");
  279. }
  280. return success;
  281. }
  282. /**
  283. * Creates an in-memory index of the push log entry.
  284. *
  285. * @param repo
  286. * @param headId
  287. * @param commands
  288. * @return an in-memory index
  289. * @throws IOException
  290. */
  291. private static DirCache createIndex(Repository repo, ObjectId headId,
  292. Collection<ReceiveCommand> commands) throws IOException {
  293. DirCache inCoreIndex = DirCache.newInCore();
  294. DirCacheBuilder dcBuilder = inCoreIndex.builder();
  295. ObjectInserter inserter = repo.newObjectInserter();
  296. long now = System.currentTimeMillis();
  297. Set<String> ignorePaths = new TreeSet<String>();
  298. try {
  299. // add receive commands to the temporary index
  300. for (ReceiveCommand command : commands) {
  301. // use the ref names as the path names
  302. String path = command.getRefName();
  303. ignorePaths.add(path);
  304. StringBuilder change = new StringBuilder();
  305. change.append(command.getType().name()).append(' ');
  306. switch (command.getType()) {
  307. case CREATE:
  308. change.append(ObjectId.zeroId().getName());
  309. change.append(' ');
  310. change.append(command.getNewId().getName());
  311. break;
  312. case UPDATE:
  313. case UPDATE_NONFASTFORWARD:
  314. change.append(command.getOldId().getName());
  315. change.append(' ');
  316. change.append(command.getNewId().getName());
  317. break;
  318. case DELETE:
  319. change = null;
  320. break;
  321. }
  322. if (change == null) {
  323. // ref deleted
  324. continue;
  325. }
  326. String content = change.toString();
  327. // create an index entry for this attachment
  328. final DirCacheEntry dcEntry = new DirCacheEntry(path);
  329. dcEntry.setLength(content.length());
  330. dcEntry.setLastModified(now);
  331. dcEntry.setFileMode(FileMode.REGULAR_FILE);
  332. // insert object
  333. dcEntry.setObjectId(inserter.insert(org.eclipse.jgit.lib.Constants.OBJ_BLOB, content.getBytes("UTF-8")));
  334. // add to temporary in-core index
  335. dcBuilder.add(dcEntry);
  336. }
  337. // Traverse HEAD to add all other paths
  338. TreeWalk treeWalk = new TreeWalk(repo);
  339. int hIdx = -1;
  340. if (headId != null)
  341. hIdx = treeWalk.addTree(new RevWalk(repo).parseTree(headId));
  342. treeWalk.setRecursive(true);
  343. while (treeWalk.next()) {
  344. String path = treeWalk.getPathString();
  345. CanonicalTreeParser hTree = null;
  346. if (hIdx != -1)
  347. hTree = treeWalk.getTree(hIdx, CanonicalTreeParser.class);
  348. if (!ignorePaths.contains(path)) {
  349. // add entries from HEAD for all other paths
  350. if (hTree != null) {
  351. // create a new DirCacheEntry with data retrieved from
  352. // HEAD
  353. final DirCacheEntry dcEntry = new DirCacheEntry(path);
  354. dcEntry.setObjectId(hTree.getEntryObjectId());
  355. dcEntry.setFileMode(hTree.getEntryFileMode());
  356. // add to temporary in-core index
  357. dcBuilder.add(dcEntry);
  358. }
  359. }
  360. }
  361. // release the treewalk
  362. treeWalk.release();
  363. // finish temporary in-core index used for this commit
  364. dcBuilder.finish();
  365. } finally {
  366. inserter.release();
  367. }
  368. return inCoreIndex;
  369. }
  370. public static List<RefLogEntry> getRefLog(String repositoryName, Repository repository) {
  371. return getRefLog(repositoryName, repository, null, 0, -1);
  372. }
  373. public static List<RefLogEntry> getRefLog(String repositoryName, Repository repository, int maxCount) {
  374. return getRefLog(repositoryName, repository, null, 0, maxCount);
  375. }
  376. public static List<RefLogEntry> getRefLog(String repositoryName, Repository repository, int offset, int maxCount) {
  377. return getRefLog(repositoryName, repository, null, offset, maxCount);
  378. }
  379. public static List<RefLogEntry> getRefLog(String repositoryName, Repository repository, Date minimumDate) {
  380. return getRefLog(repositoryName, repository, minimumDate, 0, -1);
  381. }
  382. /**
  383. * Returns the list of reflog entries as they were recorded by Gitblit.
  384. * Each RefLogEntry may represent multiple ref updates.
  385. *
  386. * @param repositoryName
  387. * @param repository
  388. * @param minimumDate
  389. * @param offset
  390. * @param maxCount
  391. * if < 0, all pushes are returned.
  392. * @return a list of push log entries
  393. */
  394. public static List<RefLogEntry> getRefLog(String repositoryName, Repository repository,
  395. Date minimumDate, int offset, int maxCount) {
  396. List<RefLogEntry> list = new ArrayList<RefLogEntry>();
  397. RefModel ref = getRefLogBranch(repository);
  398. if (ref == null) {
  399. return list;
  400. }
  401. if (maxCount == 0) {
  402. return list;
  403. }
  404. Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(repository);
  405. List<RevCommit> pushes;
  406. if (minimumDate == null) {
  407. pushes = JGitUtils.getRevLog(repository, GB_REFLOG, offset, maxCount);
  408. } else {
  409. pushes = JGitUtils.getRevLog(repository, GB_REFLOG, minimumDate);
  410. }
  411. for (RevCommit push : pushes) {
  412. if (push.getAuthorIdent().getName().equalsIgnoreCase("gitblit")) {
  413. // skip gitblit/internal commits
  414. continue;
  415. }
  416. UserModel user = newUserModelFrom(push.getAuthorIdent());
  417. Date date = push.getAuthorIdent().getWhen();
  418. RefLogEntry log = new RefLogEntry(repositoryName, date, user);
  419. // only report HEADS and TAGS for now
  420. List<PathChangeModel> changedRefs = new ArrayList<PathChangeModel>();
  421. for (PathChangeModel refChange : JGitUtils.getFilesInCommit(repository, push)) {
  422. if (refChange.path.startsWith(Constants.R_HEADS)
  423. || refChange.path.startsWith(Constants.R_TAGS)) {
  424. changedRefs.add(refChange);
  425. }
  426. }
  427. if (changedRefs.isEmpty()) {
  428. // skip empty commits
  429. continue;
  430. }
  431. list.add(log);
  432. for (PathChangeModel change : changedRefs) {
  433. switch (change.changeType) {
  434. case DELETE:
  435. log.updateRef(change.path, ReceiveCommand.Type.DELETE);
  436. break;
  437. case ADD:
  438. log.updateRef(change.path, ReceiveCommand.Type.CREATE);
  439. default:
  440. String content = JGitUtils.getStringContent(repository, push.getTree(), change.path);
  441. String [] fields = content.split(" ");
  442. String oldId = fields[1];
  443. String newId = fields[2];
  444. log.updateRef(change.path, ReceiveCommand.Type.valueOf(fields[0]), oldId, newId);
  445. if (ObjectId.zeroId().getName().equals(newId)) {
  446. // ref deletion
  447. continue;
  448. }
  449. try {
  450. List<RevCommit> pushedCommits = JGitUtils.getRevLog(repository, oldId, newId);
  451. for (RevCommit pushedCommit : pushedCommits) {
  452. RepositoryCommit repoCommit = log.addCommit(change.path, pushedCommit);
  453. if (repoCommit != null) {
  454. repoCommit.setRefs(allRefs.get(pushedCommit.getId()));
  455. }
  456. }
  457. } catch (Exception e) {
  458. }
  459. }
  460. }
  461. }
  462. Collections.sort(list);
  463. return list;
  464. }
  465. /**
  466. * Returns the list of pushes separated by ref (e.g. each ref has it's own
  467. * PushLogEntry object).
  468. *
  469. * @param repositoryName
  470. * @param repository
  471. * @param maxCount
  472. * @return a list of push log entries separated by ref
  473. */
  474. public static List<RefLogEntry> getLogByRef(String repositoryName, Repository repository, int maxCount) {
  475. return getLogByRef(repositoryName, repository, 0, maxCount);
  476. }
  477. /**
  478. * Returns the list of pushes separated by ref (e.g. each ref has it's own
  479. * PushLogEntry object).
  480. *
  481. * @param repositoryName
  482. * @param repository
  483. * @param offset
  484. * @param maxCount
  485. * @return a list of push log entries separated by ref
  486. */
  487. public static List<RefLogEntry> getLogByRef(String repositoryName, Repository repository, int offset,
  488. int maxCount) {
  489. // break the push log into ref push logs and then merge them back into a list
  490. Map<String, List<RefLogEntry>> refMap = new HashMap<String, List<RefLogEntry>>();
  491. List<RefLogEntry> refLog = getRefLog(repositoryName, repository, offset, maxCount);
  492. for (RefLogEntry entry : refLog) {
  493. for (String ref : entry.getChangedRefs()) {
  494. if (!refMap.containsKey(ref)) {
  495. refMap.put(ref, new ArrayList<RefLogEntry>());
  496. }
  497. // construct new ref-specific ref change entry
  498. RefLogEntry refChange;
  499. if (entry instanceof DailyLogEntry) {
  500. // simulated push log from commits grouped by date
  501. refChange = new DailyLogEntry(entry.repository, entry.date);
  502. } else {
  503. // real push log entry
  504. refChange = new RefLogEntry(entry.repository, entry.date, entry.user);
  505. }
  506. refChange.updateRef(ref, entry.getChangeType(ref), entry.getOldId(ref), entry.getNewId(ref));
  507. refChange.addCommits(entry.getCommits(ref));
  508. refMap.get(ref).add(refChange);
  509. }
  510. }
  511. // merge individual ref changes into master list
  512. List<RefLogEntry> mergedRefLog = new ArrayList<RefLogEntry>();
  513. for (List<RefLogEntry> refPush : refMap.values()) {
  514. mergedRefLog.addAll(refPush);
  515. }
  516. // sort ref log
  517. Collections.sort(mergedRefLog);
  518. return mergedRefLog;
  519. }
  520. /**
  521. * Returns the list of ref changes separated by ref (e.g. each ref has it's own
  522. * RefLogEntry object).
  523. *
  524. * @param repositoryName
  525. * @param repository
  526. * @param minimumDate
  527. * @return a list of ref log entries separated by ref
  528. */
  529. public static List<RefLogEntry> getLogByRef(String repositoryName, Repository repository, Date minimumDate) {
  530. // break the push log into ref push logs and then merge them back into a list
  531. Map<String, List<RefLogEntry>> refMap = new HashMap<String, List<RefLogEntry>>();
  532. List<RefLogEntry> pushes = getRefLog(repositoryName, repository, minimumDate);
  533. for (RefLogEntry push : pushes) {
  534. for (String ref : push.getChangedRefs()) {
  535. if (!refMap.containsKey(ref)) {
  536. refMap.put(ref, new ArrayList<RefLogEntry>());
  537. }
  538. // construct new ref-specific push log entry
  539. RefLogEntry refPush = new RefLogEntry(push.repository, push.date, push.user);
  540. refPush.updateRef(ref, push.getChangeType(ref), push.getOldId(ref), push.getNewId(ref));
  541. refPush.addCommits(push.getCommits(ref));
  542. refMap.get(ref).add(refPush);
  543. }
  544. }
  545. // merge individual ref pushes into master list
  546. List<RefLogEntry> refPushLog = new ArrayList<RefLogEntry>();
  547. for (List<RefLogEntry> refPush : refMap.values()) {
  548. refPushLog.addAll(refPush);
  549. }
  550. // sort ref push log
  551. Collections.sort(refPushLog);
  552. return refPushLog;
  553. }
  554. /**
  555. * Returns a commit log grouped by day.
  556. *
  557. * @param repositoryName
  558. * @param repository
  559. * @param minimumDate
  560. * @param offset
  561. * @param maxCount
  562. * if < 0, all pushes are returned.
  563. * @param the timezone to use when aggregating commits by date
  564. * @return a list of grouped commit log entries
  565. */
  566. public static List<DailyLogEntry> getDailyLog(String repositoryName, Repository repository,
  567. Date minimumDate, int offset, int maxCount,
  568. TimeZone timezone) {
  569. DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
  570. df.setTimeZone(timezone);
  571. Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(repository);
  572. Map<String, DailyLogEntry> tags = new HashMap<String, DailyLogEntry>();
  573. Map<String, DailyLogEntry> pulls = new HashMap<String, DailyLogEntry>();
  574. Map<String, DailyLogEntry> dailydigests = new HashMap<String, DailyLogEntry>();
  575. String linearParent = null;
  576. for (RefModel local : JGitUtils.getLocalBranches(repository, true, -1)) {
  577. if (!local.getDate().after(minimumDate)) {
  578. // branch not recently updated
  579. continue;
  580. }
  581. String branch = local.getName();
  582. List<RepositoryCommit> commits = CommitCache.instance().getCommits(repositoryName, repository, branch, minimumDate);
  583. linearParent = null;
  584. for (RepositoryCommit commit : commits) {
  585. if (linearParent != null) {
  586. if (!commit.getName().equals(linearParent)) {
  587. // only follow linear branch commits
  588. continue;
  589. }
  590. }
  591. Date date = commit.getCommitDate();
  592. String dateStr = df.format(date);
  593. if (!dailydigests.containsKey(dateStr)) {
  594. dailydigests.put(dateStr, new DailyLogEntry(repositoryName, date));
  595. }
  596. DailyLogEntry digest = dailydigests.get(dateStr);
  597. if (commit.getParentCount() == 0) {
  598. linearParent = null;
  599. digest.updateRef(branch, ReceiveCommand.Type.CREATE);
  600. } else {
  601. linearParent = commit.getParents()[0].getId().getName();
  602. digest.updateRef(branch, ReceiveCommand.Type.UPDATE, linearParent, commit.getName());
  603. }
  604. RepositoryCommit repoCommit = digest.addCommit(commit);
  605. if (repoCommit != null) {
  606. List<RefModel> matchedRefs = allRefs.get(commit.getId());
  607. repoCommit.setRefs(matchedRefs);
  608. if (!ArrayUtils.isEmpty(matchedRefs)) {
  609. for (RefModel ref : matchedRefs) {
  610. if (ref.getName().startsWith(Constants.R_TAGS)) {
  611. // treat tags as special events in the log
  612. if (!tags.containsKey(dateStr)) {
  613. UserModel tagUser = newUserModelFrom(ref.getAuthorIdent());
  614. Date tagDate = commit.getAuthorIdent().getWhen();
  615. tags.put(dateStr, new DailyLogEntry(repositoryName, tagDate, tagUser));
  616. }
  617. RefLogEntry tagEntry = tags.get(dateStr);
  618. tagEntry.updateRef(ref.getName(), ReceiveCommand.Type.CREATE);
  619. RepositoryCommit rc = repoCommit.clone(ref.getName());
  620. tagEntry.addCommit(rc);
  621. } else if (ref.getName().startsWith(Constants.R_PULL)) {
  622. // treat pull requests as special events in the log
  623. if (!pulls.containsKey(dateStr)) {
  624. UserModel commitUser = newUserModelFrom(ref.getAuthorIdent());
  625. Date commitDate = commit.getAuthorIdent().getWhen();
  626. pulls.put(dateStr, new DailyLogEntry(repositoryName, commitDate, commitUser));
  627. }
  628. RefLogEntry pullEntry = pulls.get(dateStr);
  629. pullEntry.updateRef(ref.getName(), ReceiveCommand.Type.CREATE);
  630. RepositoryCommit rc = repoCommit.clone(ref.getName());
  631. pullEntry.addCommit(rc);
  632. }
  633. }
  634. }
  635. }
  636. }
  637. }
  638. List<DailyLogEntry> list = new ArrayList<DailyLogEntry>(dailydigests.values());
  639. list.addAll(tags.values());
  640. //list.addAll(pulls.values());
  641. Collections.sort(list);
  642. return list;
  643. }
  644. /**
  645. * Returns the list of commits separated by ref (e.g. each ref has it's own
  646. * PushLogEntry object for each day).
  647. *
  648. * @param repositoryName
  649. * @param repository
  650. * @param minimumDate
  651. * @param the timezone to use when aggregating commits by date
  652. * @return a list of push log entries separated by ref and date
  653. */
  654. public static List<DailyLogEntry> getDailyLogByRef(String repositoryName, Repository repository,
  655. Date minimumDate, TimeZone timezone) {
  656. // break the push log into ref push logs and then merge them back into a list
  657. Map<String, List<DailyLogEntry>> refMap = new HashMap<String, List<DailyLogEntry>>();
  658. List<DailyLogEntry> pushes = getDailyLog(repositoryName, repository, minimumDate, 0, -1, timezone);
  659. for (DailyLogEntry push : pushes) {
  660. for (String ref : push.getChangedRefs()) {
  661. if (!refMap.containsKey(ref)) {
  662. refMap.put(ref, new ArrayList<DailyLogEntry>());
  663. }
  664. // construct new ref-specific push log entry
  665. DailyLogEntry refPush = new DailyLogEntry(push.repository, push.date, push.user);
  666. refPush.updateRef(ref, push.getChangeType(ref), push.getOldId(ref), push.getNewId(ref));
  667. refPush.addCommits(push.getCommits(ref));
  668. refMap.get(ref).add(refPush);
  669. }
  670. }
  671. // merge individual ref pushes into master list
  672. List<DailyLogEntry> refPushLog = new ArrayList<DailyLogEntry>();
  673. for (List<DailyLogEntry> refPush : refMap.values()) {
  674. for (DailyLogEntry entry : refPush) {
  675. if (entry.getCommitCount() > 0) {
  676. refPushLog.add(entry);
  677. }
  678. }
  679. }
  680. // sort ref push log
  681. Collections.sort(refPushLog);
  682. return refPushLog;
  683. }
  684. }