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.

EditRepositoryPage.java 30KB

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 years ago
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 years ago
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 years ago
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 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808
  1. /*
  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. */
  16. package com.gitblit.wicket.pages;
  17. import java.text.MessageFormat;
  18. import java.util.ArrayList;
  19. import java.util.Arrays;
  20. import java.util.Collections;
  21. import java.util.HashSet;
  22. import java.util.Iterator;
  23. import java.util.LinkedHashMap;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.Set;
  27. import org.apache.wicket.PageParameters;
  28. import org.apache.wicket.ajax.AjaxRequestTarget;
  29. import org.apache.wicket.ajax.form.AjaxFormChoiceComponentUpdatingBehavior;
  30. import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
  31. import org.apache.wicket.behavior.SimpleAttributeModifier;
  32. import org.apache.wicket.extensions.markup.html.form.palette.Palette;
  33. import org.apache.wicket.markup.html.WebMarkupContainer;
  34. import org.apache.wicket.markup.html.basic.Label;
  35. import org.apache.wicket.markup.html.form.Button;
  36. import org.apache.wicket.markup.html.form.CheckBox;
  37. import org.apache.wicket.markup.html.form.ChoiceRenderer;
  38. import org.apache.wicket.markup.html.form.DropDownChoice;
  39. import org.apache.wicket.markup.html.form.Form;
  40. import org.apache.wicket.markup.html.form.IChoiceRenderer;
  41. import org.apache.wicket.markup.html.form.RadioChoice;
  42. import org.apache.wicket.markup.html.form.TextField;
  43. import org.apache.wicket.markup.html.link.Link;
  44. import org.apache.wicket.markup.html.list.ListItem;
  45. import org.apache.wicket.markup.html.list.ListView;
  46. import org.apache.wicket.model.CompoundPropertyModel;
  47. import org.apache.wicket.model.IModel;
  48. import org.apache.wicket.model.Model;
  49. import org.apache.wicket.model.util.CollectionModel;
  50. import org.apache.wicket.model.util.ListModel;
  51. import org.eclipse.jgit.lib.Repository;
  52. import com.gitblit.Constants;
  53. import com.gitblit.Constants.AccessRestrictionType;
  54. import com.gitblit.Constants.AuthorizationControl;
  55. import com.gitblit.Constants.CommitMessageRenderer;
  56. import com.gitblit.Constants.FederationStrategy;
  57. import com.gitblit.Constants.RegistrantType;
  58. import com.gitblit.GitBlitException;
  59. import com.gitblit.Keys;
  60. import com.gitblit.models.RegistrantAccessPermission;
  61. import com.gitblit.models.RepositoryModel;
  62. import com.gitblit.models.UserChoice;
  63. import com.gitblit.models.UserModel;
  64. import com.gitblit.utils.ArrayUtils;
  65. import com.gitblit.utils.StringUtils;
  66. import com.gitblit.wicket.GitBlitWebSession;
  67. import com.gitblit.wicket.StringChoiceRenderer;
  68. import com.gitblit.wicket.WicketUtils;
  69. import com.gitblit.wicket.panels.BasePanel.JavascriptEventConfirmation;
  70. import com.gitblit.wicket.panels.BulletListPanel;
  71. import com.gitblit.wicket.panels.RegistrantPermissionsPanel;
  72. public class EditRepositoryPage extends RootSubPage {
  73. private final boolean isCreate;
  74. private boolean isAdmin;
  75. RepositoryModel repositoryModel;
  76. private IModel<String> metricAuthorExclusions;
  77. private IModel<String> mailingLists;
  78. public EditRepositoryPage() {
  79. // create constructor
  80. super();
  81. isCreate = true;
  82. RepositoryModel model = new RepositoryModel();
  83. String restriction = app().settings().getString(Keys.git.defaultAccessRestriction, "PUSH");
  84. model.accessRestriction = AccessRestrictionType.fromName(restriction);
  85. String authorization = app().settings().getString(Keys.git.defaultAuthorizationControl, null);
  86. model.authorizationControl = AuthorizationControl.fromName(authorization);
  87. GitBlitWebSession session = GitBlitWebSession.get();
  88. UserModel user = session.getUser();
  89. if (user != null && user.canCreate() && !user.canAdmin()) {
  90. // personal create permissions, inject personal repository path
  91. model.name = user.getPersonalPath() + "/";
  92. model.projectPath = user.getPersonalPath();
  93. model.addOwner(user.username);
  94. // personal repositories are private by default
  95. model.accessRestriction = AccessRestrictionType.VIEW;
  96. model.authorizationControl = AuthorizationControl.NAMED;
  97. }
  98. setupPage(model);
  99. setStatelessHint(false);
  100. setOutputMarkupId(true);
  101. }
  102. public EditRepositoryPage(PageParameters params) {
  103. // edit constructor
  104. super(params);
  105. isCreate = false;
  106. String name = WicketUtils.getRepositoryName(params);
  107. RepositoryModel model = app().repositories().getRepositoryModel(name);
  108. setupPage(model);
  109. setStatelessHint(false);
  110. setOutputMarkupId(true);
  111. }
  112. @Override
  113. protected boolean requiresPageMap() {
  114. return true;
  115. }
  116. @Override
  117. protected Class<? extends BasePage> getRootNavPageClass() {
  118. return RepositoriesPage.class;
  119. }
  120. protected void setupPage(RepositoryModel model) {
  121. this.repositoryModel = model;
  122. // ensure this user can create or edit this repository
  123. checkPermissions(repositoryModel);
  124. List<String> indexedBranches = new ArrayList<String>();
  125. List<String> federationSets = new ArrayList<String>();
  126. final List<RegistrantAccessPermission> repositoryUsers = new ArrayList<RegistrantAccessPermission>();
  127. final List<RegistrantAccessPermission> repositoryTeams = new ArrayList<RegistrantAccessPermission>();
  128. List<String> preReceiveScripts = new ArrayList<String>();
  129. List<String> postReceiveScripts = new ArrayList<String>();
  130. GitBlitWebSession session = GitBlitWebSession.get();
  131. final UserModel user = session.getUser() == null ? UserModel.ANONYMOUS : session.getUser();
  132. final boolean allowEditName = isCreate || isAdmin || repositoryModel.isUsersPersonalRepository(user.username);
  133. if (isCreate) {
  134. if (user.canAdmin()) {
  135. super.setupPage(getString("gb.newRepository"), "");
  136. } else {
  137. super.setupPage(getString("gb.newRepository"), user.getDisplayName());
  138. }
  139. } else {
  140. super.setupPage(getString("gb.edit"), repositoryModel.name);
  141. repositoryUsers.addAll(app().repositories().getUserAccessPermissions(repositoryModel));
  142. repositoryTeams.addAll(app().repositories().getTeamAccessPermissions(repositoryModel));
  143. Collections.sort(repositoryUsers);
  144. Collections.sort(repositoryTeams);
  145. federationSets.addAll(repositoryModel.federationSets);
  146. if (!ArrayUtils.isEmpty(repositoryModel.indexedBranches)) {
  147. indexedBranches.addAll(repositoryModel.indexedBranches);
  148. }
  149. }
  150. final String oldName = repositoryModel.name;
  151. final RegistrantPermissionsPanel usersPalette = new RegistrantPermissionsPanel("users",
  152. RegistrantType.USER, app().users().getAllUsernames(), repositoryUsers, getAccessPermissions());
  153. final RegistrantPermissionsPanel teamsPalette = new RegistrantPermissionsPanel("teams",
  154. RegistrantType.TEAM, app().users().getAllTeamNames(), repositoryTeams, getAccessPermissions());
  155. // owners palette
  156. List<UserChoice> owners = new ArrayList<UserChoice>();
  157. for (String owner : repositoryModel.owners) {
  158. UserModel o = app().users().getUserModel(owner);
  159. if (o != null) {
  160. owners.add(new UserChoice(o.getDisplayName(), o.username, o.emailAddress));
  161. } else {
  162. owners.add(new UserChoice(owner));
  163. }
  164. }
  165. List<UserChoice> persons = new ArrayList<UserChoice>();
  166. for (String person : app().users().getAllUsernames()) {
  167. UserModel o = app().users().getUserModel(person);
  168. if (o != null) {
  169. persons.add(new UserChoice(o.getDisplayName(), o.username, o.emailAddress));
  170. } else {
  171. persons.add(new UserChoice(person));
  172. }
  173. }
  174. final Palette<UserChoice> ownersPalette = new Palette<UserChoice>("owners", new ListModel<UserChoice>(owners), new CollectionModel<UserChoice>(
  175. persons), new ChoiceRenderer<UserChoice>(null, "userId"), 12, true);
  176. // indexed local branches palette
  177. List<String> allLocalBranches = new ArrayList<String>();
  178. allLocalBranches.add(Constants.DEFAULT_BRANCH);
  179. allLocalBranches.addAll(repositoryModel.getLocalBranches());
  180. boolean luceneEnabled = app().settings().getBoolean(Keys.web.allowLuceneIndexing, true);
  181. final Palette<String> indexedBranchesPalette = new Palette<String>("indexedBranches", new ListModel<String>(
  182. indexedBranches), new CollectionModel<String>(allLocalBranches),
  183. new StringChoiceRenderer(), 8, false);
  184. indexedBranchesPalette.setEnabled(luceneEnabled);
  185. // federation sets palette
  186. List<String> sets = app().settings().getStrings(Keys.federation.sets);
  187. final Palette<String> federationSetsPalette = new Palette<String>("federationSets",
  188. new ListModel<String>(federationSets), new CollectionModel<String>(sets),
  189. new StringChoiceRenderer(), 8, false);
  190. // pre-receive palette
  191. if (!ArrayUtils.isEmpty(repositoryModel.preReceiveScripts)) {
  192. preReceiveScripts.addAll(repositoryModel.preReceiveScripts);
  193. }
  194. final Palette<String> preReceivePalette = new Palette<String>("preReceiveScripts",
  195. new ListModel<String>(preReceiveScripts), new CollectionModel<String>(app().repositories()
  196. .getPreReceiveScriptsUnused(repositoryModel)),
  197. new StringChoiceRenderer(), 12, true);
  198. // post-receive palette
  199. if (!ArrayUtils.isEmpty(repositoryModel.postReceiveScripts)) {
  200. postReceiveScripts.addAll(repositoryModel.postReceiveScripts);
  201. }
  202. final Palette<String> postReceivePalette = new Palette<String>("postReceiveScripts",
  203. new ListModel<String>(postReceiveScripts), new CollectionModel<String>(app().repositories()
  204. .getPostReceiveScriptsUnused(repositoryModel)),
  205. new StringChoiceRenderer(), 12, true);
  206. // custom fields
  207. final Map<String, String> customFieldsMap = app().settings().getMap(Keys.groovy.customFields);
  208. List<String> customKeys = new ArrayList<String>(customFieldsMap.keySet());
  209. final ListView<String> customFieldsListView = new ListView<String>("customFieldsListView", customKeys) {
  210. private static final long serialVersionUID = 1L;
  211. @Override
  212. protected void populateItem(ListItem<String> item) {
  213. String key = item.getModelObject();
  214. item.add(new Label("customFieldLabel", customFieldsMap.get(key)));
  215. String value = "";
  216. if (repositoryModel.customFields != null && repositoryModel.customFields.containsKey(key)) {
  217. value = repositoryModel.customFields.get(key);
  218. }
  219. TextField<String> field = new TextField<String>("customFieldValue", new Model<String>(value));
  220. item.add(field);
  221. }
  222. };
  223. customFieldsListView.setReuseItems(true);
  224. CompoundPropertyModel<RepositoryModel> rModel = new CompoundPropertyModel<RepositoryModel>(
  225. repositoryModel);
  226. Form<RepositoryModel> form = new Form<RepositoryModel>("editForm", rModel) {
  227. private static final long serialVersionUID = 1L;
  228. @Override
  229. protected void onSubmit() {
  230. try {
  231. // confirm a repository name was entered
  232. if (repositoryModel.name == null && StringUtils.isEmpty(repositoryModel.name)) {
  233. error(getString("gb.pleaseSetRepositoryName"));
  234. return;
  235. }
  236. // ensure name is trimmed
  237. repositoryModel.name = repositoryModel.name.trim();
  238. // automatically convert backslashes to forward slashes
  239. repositoryModel.name = repositoryModel.name.replace('\\', '/');
  240. // Automatically replace // with /
  241. repositoryModel.name = repositoryModel.name.replace("//", "/");
  242. // prohibit folder paths
  243. if (repositoryModel.name.startsWith("/")) {
  244. error(getString("gb.illegalLeadingSlash"));
  245. return;
  246. }
  247. if (repositoryModel.name.startsWith("../")) {
  248. error(getString("gb.illegalRelativeSlash"));
  249. return;
  250. }
  251. if (repositoryModel.name.contains("/../")) {
  252. error(getString("gb.illegalRelativeSlash"));
  253. return;
  254. }
  255. if (repositoryModel.name.endsWith("/")) {
  256. repositoryModel.name = repositoryModel.name.substring(0, repositoryModel.name.length() - 1);
  257. }
  258. // confirm valid characters in repository name
  259. Character c = StringUtils.findInvalidCharacter(repositoryModel.name);
  260. if (c != null) {
  261. error(MessageFormat.format(getString("gb.illegalCharacterRepositoryName"),
  262. c));
  263. return;
  264. }
  265. if (user.canCreate() && !user.canAdmin() && allowEditName) {
  266. // ensure repository name begins with the user's path
  267. if (!repositoryModel.name.startsWith(user.getPersonalPath())) {
  268. error(MessageFormat.format(getString("gb.illegalPersonalRepositoryLocation"),
  269. user.getPersonalPath()));
  270. return;
  271. }
  272. if (repositoryModel.name.equals(user.getPersonalPath())) {
  273. // reset path prefix and show error
  274. repositoryModel.name = user.getPersonalPath() + "/";
  275. error(getString("gb.pleaseSetRepositoryName"));
  276. return;
  277. }
  278. }
  279. // confirm access restriction selection
  280. if (repositoryModel.accessRestriction == null) {
  281. error(getString("gb.selectAccessRestriction"));
  282. return;
  283. }
  284. // confirm federation strategy selection
  285. if (repositoryModel.federationStrategy == null) {
  286. error(getString("gb.selectFederationStrategy"));
  287. return;
  288. }
  289. // save federation set preferences
  290. if (repositoryModel.federationStrategy.exceeds(FederationStrategy.EXCLUDE)) {
  291. repositoryModel.federationSets.clear();
  292. Iterator<String> sets = federationSetsPalette.getSelectedChoices();
  293. while (sets.hasNext()) {
  294. repositoryModel.federationSets.add(sets.next());
  295. }
  296. }
  297. // set author metric exclusions
  298. String ax = metricAuthorExclusions.getObject();
  299. if (StringUtils.isEmpty(ax)) {
  300. repositoryModel.metricAuthorExclusions = new ArrayList<String>();
  301. } else {
  302. Set<String> list = new HashSet<String>();
  303. for (String exclusion : StringUtils.getStringsFromValue(ax, " ")) {
  304. if (StringUtils.isEmpty(exclusion)) {
  305. continue;
  306. }
  307. if (exclusion.indexOf(' ') > -1) {
  308. list.add("\"" + exclusion + "\"");
  309. } else {
  310. list.add(exclusion);
  311. }
  312. }
  313. repositoryModel.metricAuthorExclusions = new ArrayList<String>(list);
  314. }
  315. // set mailing lists
  316. String ml = mailingLists.getObject();
  317. if (StringUtils.isEmpty(ml)) {
  318. repositoryModel.mailingLists = new ArrayList<String>();
  319. } else {
  320. Set<String> list = new HashSet<String>();
  321. for (String address : ml.split("(,|\\s)")) {
  322. if (StringUtils.isEmpty(address)) {
  323. continue;
  324. }
  325. list.add(address.toLowerCase());
  326. }
  327. repositoryModel.mailingLists = new ArrayList<String>(list);
  328. }
  329. // indexed branches
  330. List<String> indexedBranches = new ArrayList<String>();
  331. Iterator<String> branches = indexedBranchesPalette.getSelectedChoices();
  332. while (branches.hasNext()) {
  333. indexedBranches.add(branches.next());
  334. }
  335. repositoryModel.indexedBranches = indexedBranches;
  336. // owners
  337. repositoryModel.owners.clear();
  338. Iterator<UserChoice> owners = ownersPalette.getSelectedChoices();
  339. while (owners.hasNext()) {
  340. repositoryModel.addOwner(owners.next().getUserId());
  341. }
  342. // pre-receive scripts
  343. List<String> preReceiveScripts = new ArrayList<String>();
  344. Iterator<String> pres = preReceivePalette.getSelectedChoices();
  345. while (pres.hasNext()) {
  346. preReceiveScripts.add(pres.next());
  347. }
  348. repositoryModel.preReceiveScripts = preReceiveScripts;
  349. // post-receive scripts
  350. List<String> postReceiveScripts = new ArrayList<String>();
  351. Iterator<String> post = postReceivePalette.getSelectedChoices();
  352. while (post.hasNext()) {
  353. postReceiveScripts.add(post.next());
  354. }
  355. repositoryModel.postReceiveScripts = postReceiveScripts;
  356. // custom fields
  357. repositoryModel.customFields = new LinkedHashMap<String, String>();
  358. for (int i = 0; i < customFieldsListView.size(); i++) {
  359. ListItem<String> child = (ListItem<String>) customFieldsListView.get(i);
  360. String key = child.getModelObject();
  361. TextField<String> field = (TextField<String>) child.get("customFieldValue");
  362. String value = field.getValue();
  363. repositoryModel.customFields.put(key, value);
  364. }
  365. // save the repository
  366. app().gitblit().updateRepositoryModel(oldName, repositoryModel, isCreate);
  367. // repository access permissions
  368. if (repositoryModel.accessRestriction.exceeds(AccessRestrictionType.NONE)) {
  369. app().gitblit().setUserAccessPermissions(repositoryModel, repositoryUsers);
  370. app().gitblit().setTeamAccessPermissions(repositoryModel, repositoryTeams);
  371. }
  372. } catch (GitBlitException e) {
  373. error(e.getMessage());
  374. return;
  375. }
  376. setRedirect(false);
  377. if (isCreate) {
  378. setResponsePage(RepositoriesPage.class);
  379. } else {
  380. setResponsePage(SummaryPage.class, WicketUtils.newRepositoryParameter(repositoryModel.name));
  381. }
  382. }
  383. };
  384. // do not let the browser pre-populate these fields
  385. form.add(new SimpleAttributeModifier("autocomplete", "off"));
  386. // field names reflective match RepositoryModel fields
  387. form.add(new TextField<String>("name").setEnabled(allowEditName));
  388. form.add(new TextField<String>("description"));
  389. form.add(ownersPalette);
  390. form.add(new CheckBox("allowForks").setEnabled(app().settings().getBoolean(Keys.web.allowForking, true)));
  391. DropDownChoice<AccessRestrictionType> accessRestriction = new DropDownChoice<AccessRestrictionType>("accessRestriction",
  392. AccessRestrictionType.choices(app().settings().getBoolean(Keys.git.allowAnonymousPushes, false)), new AccessRestrictionRenderer());
  393. form.add(accessRestriction);
  394. form.add(new CheckBox("isFrozen"));
  395. // TODO enable origin definition
  396. form.add(new TextField<String>("origin").setEnabled(false/* isCreate */));
  397. // allow relinking HEAD to a branch or tag other than master on edit repository
  398. List<String> availableRefs = new ArrayList<String>();
  399. List<String> availableBranches = new ArrayList<String>();
  400. if (!ArrayUtils.isEmpty(repositoryModel.availableRefs)) {
  401. for (String ref : repositoryModel.availableRefs) {
  402. if (!ref.startsWith(Constants.R_TICKET)) {
  403. availableRefs.add(ref);
  404. if (ref.startsWith(Constants.R_HEADS)) {
  405. availableBranches.add(Repository.shortenRefName(ref));
  406. }
  407. }
  408. }
  409. }
  410. form.add(new DropDownChoice<String>("HEAD", availableRefs).setEnabled(availableRefs.size() > 0));
  411. boolean gcEnabled = app().settings().getBoolean(Keys.git.enableGarbageCollection, false);
  412. int defaultGcPeriod = app().settings().getInteger(Keys.git.defaultGarbageCollectionPeriod, 7);
  413. if (repositoryModel.gcPeriod == 0) {
  414. repositoryModel.gcPeriod = defaultGcPeriod;
  415. }
  416. List<Integer> gcPeriods = Arrays.asList(1, 2, 3, 4, 5, 7, 10, 14 );
  417. form.add(new DropDownChoice<Integer>("gcPeriod", gcPeriods, new GCPeriodRenderer()).setEnabled(gcEnabled));
  418. form.add(new TextField<String>("gcThreshold").setEnabled(gcEnabled));
  419. // federation strategies - remove ORIGIN choice if this repository has
  420. // no origin.
  421. List<FederationStrategy> federationStrategies = new ArrayList<FederationStrategy>(
  422. Arrays.asList(FederationStrategy.values()));
  423. if (StringUtils.isEmpty(repositoryModel.origin)) {
  424. federationStrategies.remove(FederationStrategy.FEDERATE_ORIGIN);
  425. }
  426. form.add(new DropDownChoice<FederationStrategy>("federationStrategy", federationStrategies,
  427. new FederationTypeRenderer()));
  428. form.add(new CheckBox("acceptNewPatchsets"));
  429. form.add(new CheckBox("acceptNewTickets"));
  430. form.add(new CheckBox("requireApproval"));
  431. form.add(new DropDownChoice<String>("mergeTo", availableBranches).setEnabled(availableBranches.size() > 0));
  432. form.add(new CheckBox("useIncrementalPushTags"));
  433. form.add(new CheckBox("showRemoteBranches"));
  434. form.add(new CheckBox("skipSizeCalculation"));
  435. form.add(new CheckBox("skipSummaryMetrics"));
  436. List<Integer> maxActivityCommits = Arrays.asList(-1, 0, 25, 50, 75, 100, 150, 200, 250, 500);
  437. form.add(new DropDownChoice<Integer>("maxActivityCommits", maxActivityCommits, new MaxActivityCommitsRenderer()));
  438. metricAuthorExclusions = new Model<String>(ArrayUtils.isEmpty(repositoryModel.metricAuthorExclusions) ? ""
  439. : StringUtils.flattenStrings(repositoryModel.metricAuthorExclusions, " "));
  440. form.add(new TextField<String>("metricAuthorExclusions", metricAuthorExclusions));
  441. mailingLists = new Model<String>(ArrayUtils.isEmpty(repositoryModel.mailingLists) ? ""
  442. : StringUtils.flattenStrings(repositoryModel.mailingLists, " "));
  443. form.add(new TextField<String>("mailingLists", mailingLists));
  444. form.add(indexedBranchesPalette);
  445. List<AuthorizationControl> acList = Arrays.asList(AuthorizationControl.values());
  446. final RadioChoice<AuthorizationControl> authorizationControl = new RadioChoice<Constants.AuthorizationControl>(
  447. "authorizationControl", acList, new AuthorizationControlRenderer());
  448. form.add(authorizationControl);
  449. final CheckBox verifyCommitter = new CheckBox("verifyCommitter");
  450. verifyCommitter.setOutputMarkupId(true);
  451. form.add(verifyCommitter);
  452. form.add(usersPalette);
  453. form.add(teamsPalette);
  454. form.add(federationSetsPalette);
  455. form.add(preReceivePalette);
  456. form.add(new BulletListPanel("inheritedPreReceive", getString("gb.inherited"), app().repositories()
  457. .getPreReceiveScriptsInherited(repositoryModel)));
  458. form.add(postReceivePalette);
  459. form.add(new BulletListPanel("inheritedPostReceive", getString("gb.inherited"), app().repositories()
  460. .getPostReceiveScriptsInherited(repositoryModel)));
  461. WebMarkupContainer customFieldsSection = new WebMarkupContainer("customFieldsSection");
  462. customFieldsSection.add(customFieldsListView);
  463. form.add(customFieldsSection.setVisible(!app().settings().getString(Keys.groovy.customFields, "").isEmpty()));
  464. // initial enable/disable of permission controls
  465. if (repositoryModel.accessRestriction.equals(AccessRestrictionType.NONE)) {
  466. // anonymous everything, disable all controls
  467. usersPalette.setEnabled(false);
  468. teamsPalette.setEnabled(false);
  469. authorizationControl.setEnabled(false);
  470. verifyCommitter.setEnabled(false);
  471. } else {
  472. // authenticated something
  473. // enable authorization controls
  474. authorizationControl.setEnabled(true);
  475. verifyCommitter.setEnabled(true);
  476. boolean allowFineGrainedControls = repositoryModel.authorizationControl.equals(AuthorizationControl.NAMED);
  477. usersPalette.setEnabled(allowFineGrainedControls);
  478. teamsPalette.setEnabled(allowFineGrainedControls);
  479. }
  480. accessRestriction.add(new AjaxFormComponentUpdatingBehavior("onchange") {
  481. private static final long serialVersionUID = 1L;
  482. @Override
  483. protected void onUpdate(AjaxRequestTarget target) {
  484. // enable/disable permissions panel based on access restriction
  485. boolean allowAuthorizationControl = repositoryModel.accessRestriction.exceeds(AccessRestrictionType.NONE);
  486. authorizationControl.setEnabled(allowAuthorizationControl);
  487. verifyCommitter.setEnabled(allowAuthorizationControl);
  488. boolean allowFineGrainedControls = allowAuthorizationControl && repositoryModel.authorizationControl.equals(AuthorizationControl.NAMED);
  489. usersPalette.setEnabled(allowFineGrainedControls);
  490. teamsPalette.setEnabled(allowFineGrainedControls);
  491. if (allowFineGrainedControls) {
  492. repositoryModel.authorizationControl = AuthorizationControl.NAMED;
  493. }
  494. target.addComponent(authorizationControl);
  495. target.addComponent(verifyCommitter);
  496. target.addComponent(usersPalette);
  497. target.addComponent(teamsPalette);
  498. }
  499. });
  500. authorizationControl.add(new AjaxFormChoiceComponentUpdatingBehavior() {
  501. private static final long serialVersionUID = 1L;
  502. @Override
  503. protected void onUpdate(AjaxRequestTarget target) {
  504. // enable/disable permissions panel based on access restriction
  505. boolean allowAuthorizationControl = repositoryModel.accessRestriction.exceeds(AccessRestrictionType.NONE);
  506. authorizationControl.setEnabled(allowAuthorizationControl);
  507. boolean allowFineGrainedControls = allowAuthorizationControl && repositoryModel.authorizationControl.equals(AuthorizationControl.NAMED);
  508. usersPalette.setEnabled(allowFineGrainedControls);
  509. teamsPalette.setEnabled(allowFineGrainedControls);
  510. if (allowFineGrainedControls) {
  511. repositoryModel.authorizationControl = AuthorizationControl.NAMED;
  512. }
  513. target.addComponent(authorizationControl);
  514. target.addComponent(usersPalette);
  515. target.addComponent(teamsPalette);
  516. }
  517. });
  518. List<CommitMessageRenderer> renderers = Arrays.asList(CommitMessageRenderer.values());
  519. DropDownChoice<CommitMessageRenderer> messageRendererChoice = new DropDownChoice<CommitMessageRenderer>("commitMessageRenderer", renderers);
  520. form.add(messageRendererChoice);
  521. form.add(new Button("save"));
  522. Button cancel = new Button("cancel") {
  523. private static final long serialVersionUID = 1L;
  524. @Override
  525. public void onSubmit() {
  526. if (isCreate) {
  527. setResponsePage(RepositoriesPage.class);
  528. } else {
  529. setResponsePage(SummaryPage.class, WicketUtils.newRepositoryParameter(repositoryModel.name));
  530. }
  531. }
  532. };
  533. cancel.setDefaultFormProcessing(false);
  534. form.add(cancel);
  535. // the user can delete if deletions are allowed AND the user is an admin or the personal owner
  536. // assigned ownership is not sufficient to allow deletion
  537. boolean canDelete = !isCreate && app().repositories().canDelete(repositoryModel)
  538. && (user.canAdmin() || user.isMyPersonalRepository(repositoryModel.name));
  539. Link<Void> delete = new Link<Void>("delete") {
  540. private static final long serialVersionUID = 1L;
  541. @Override
  542. public void onClick() {
  543. RepositoryModel latestModel = app().repositories().getRepositoryModel(repositoryModel.name);
  544. boolean canDelete = app().repositories().canDelete(latestModel);
  545. if (canDelete) {
  546. if (app().repositories().deleteRepositoryModel(latestModel)) {
  547. info(MessageFormat.format(getString("gb.repositoryDeleted"), latestModel));
  548. setResponsePage(RepositoriesPage.class);
  549. } else {
  550. error(MessageFormat.format(getString("gb.repositoryDeleteFailed"), latestModel));
  551. }
  552. } else {
  553. error(MessageFormat.format(getString("gb.repositoryDeleteFailed"), latestModel));
  554. }
  555. }
  556. };
  557. if (canDelete) {
  558. delete.add(new JavascriptEventConfirmation("onclick", MessageFormat.format(
  559. getString("gb.deleteRepository"), repositoryModel)));
  560. }
  561. form.add(delete.setVisible(canDelete));
  562. add(form);
  563. }
  564. /**
  565. * Unfortunately must repeat part of AuthorizaitonStrategy here because that
  566. * mechanism does not take PageParameters into consideration, only page
  567. * instantiation.
  568. *
  569. * Repository Owners should be able to edit their repository.
  570. */
  571. private void checkPermissions(RepositoryModel model) {
  572. boolean authenticateAdmin = app().settings().getBoolean(Keys.web.authenticateAdminPages, true);
  573. boolean allowAdmin = app().settings().getBoolean(Keys.web.allowAdministration, true);
  574. GitBlitWebSession session = GitBlitWebSession.get();
  575. UserModel user = session.getUser();
  576. if (allowAdmin) {
  577. if (authenticateAdmin) {
  578. if (user == null) {
  579. // No Login Available
  580. error(getString("gb.errorAdminLoginRequired"), true);
  581. }
  582. if (isCreate) {
  583. // Create Repository
  584. if (!user.canCreate() && !user.canAdmin()) {
  585. // Only administrators or permitted users may create
  586. error(getString("gb.errorOnlyAdminMayCreateRepository"), true);
  587. }
  588. } else {
  589. // Edit Repository
  590. if (user.canAdmin()) {
  591. // Admins can edit everything
  592. isAdmin = true;
  593. return;
  594. } else {
  595. if (!model.isOwner(user.username)) {
  596. // User is not an Admin nor Owner
  597. error(getString("gb.errorOnlyAdminOrOwnerMayEditRepository"), true);
  598. }
  599. }
  600. }
  601. }
  602. } else {
  603. // No Administration Permitted
  604. error(getString("gb.errorAdministrationDisabled"), true);
  605. }
  606. }
  607. private class AccessRestrictionRenderer implements IChoiceRenderer<AccessRestrictionType> {
  608. private static final long serialVersionUID = 1L;
  609. private final Map<AccessRestrictionType, String> map;
  610. public AccessRestrictionRenderer() {
  611. map = getAccessRestrictions();
  612. }
  613. @Override
  614. public String getDisplayValue(AccessRestrictionType type) {
  615. return map.get(type);
  616. }
  617. @Override
  618. public String getIdValue(AccessRestrictionType type, int index) {
  619. return Integer.toString(index);
  620. }
  621. }
  622. private class FederationTypeRenderer implements IChoiceRenderer<FederationStrategy> {
  623. private static final long serialVersionUID = 1L;
  624. private final Map<FederationStrategy, String> map;
  625. public FederationTypeRenderer() {
  626. map = getFederationTypes();
  627. }
  628. @Override
  629. public String getDisplayValue(FederationStrategy type) {
  630. return map.get(type);
  631. }
  632. @Override
  633. public String getIdValue(FederationStrategy type, int index) {
  634. return Integer.toString(index);
  635. }
  636. }
  637. private class AuthorizationControlRenderer implements IChoiceRenderer<AuthorizationControl> {
  638. private static final long serialVersionUID = 1L;
  639. private final Map<AuthorizationControl, String> map;
  640. public AuthorizationControlRenderer() {
  641. map = getAuthorizationControls();
  642. }
  643. @Override
  644. public String getDisplayValue(AuthorizationControl type) {
  645. return map.get(type);
  646. }
  647. @Override
  648. public String getIdValue(AuthorizationControl type, int index) {
  649. return Integer.toString(index);
  650. }
  651. }
  652. private class GCPeriodRenderer implements IChoiceRenderer<Integer> {
  653. private static final long serialVersionUID = 1L;
  654. public GCPeriodRenderer() {
  655. }
  656. @Override
  657. public String getDisplayValue(Integer value) {
  658. if (value == 1) {
  659. return getString("gb.duration.oneDay");
  660. } else {
  661. return MessageFormat.format(getString("gb.duration.days"), value);
  662. }
  663. }
  664. @Override
  665. public String getIdValue(Integer value, int index) {
  666. return Integer.toString(index);
  667. }
  668. }
  669. private class MaxActivityCommitsRenderer implements IChoiceRenderer<Integer> {
  670. private static final long serialVersionUID = 1L;
  671. public MaxActivityCommitsRenderer() {
  672. }
  673. @Override
  674. public String getDisplayValue(Integer value) {
  675. if (value == -1) {
  676. return getString("gb.excludeFromActivity");
  677. } else if (value == 0) {
  678. return getString("gb.noMaximum");
  679. } else {
  680. return value + " " + getString("gb.commits");
  681. }
  682. }
  683. @Override
  684. public String getIdValue(Integer value, int index) {
  685. return Integer.toString(index);
  686. }
  687. }
  688. }