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.

RepositoryPage.java 27KB

11 years ago
11 years ago
11 years ago
11 years ago
13 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
13 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
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
13 years ago
13 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741
  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.io.Serializable;
  18. import java.text.MessageFormat;
  19. import java.util.ArrayList;
  20. import java.util.Arrays;
  21. import java.util.Date;
  22. import java.util.HashMap;
  23. import java.util.LinkedHashSet;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.Set;
  27. import org.apache.wicket.Component;
  28. import org.apache.wicket.PageParameters;
  29. import org.apache.wicket.RestartResponseException;
  30. import org.apache.wicket.behavior.SimpleAttributeModifier;
  31. import org.apache.wicket.markup.html.basic.Label;
  32. import org.apache.wicket.markup.html.form.DropDownChoice;
  33. import org.apache.wicket.markup.html.form.TextField;
  34. import org.apache.wicket.markup.html.link.ExternalLink;
  35. import org.apache.wicket.markup.html.panel.Fragment;
  36. import org.apache.wicket.model.IModel;
  37. import org.apache.wicket.model.Model;
  38. import org.apache.wicket.request.target.basic.RedirectRequestTarget;
  39. import org.eclipse.jgit.diff.DiffEntry.ChangeType;
  40. import org.eclipse.jgit.lib.PersonIdent;
  41. import org.eclipse.jgit.lib.Repository;
  42. import org.eclipse.jgit.revwalk.RevCommit;
  43. import org.slf4j.Logger;
  44. import org.slf4j.LoggerFactory;
  45. import com.gitblit.Constants;
  46. import com.gitblit.GitBlitException;
  47. import com.gitblit.Keys;
  48. import com.gitblit.extensions.RepositoryNavLinkExtension;
  49. import com.gitblit.models.NavLink;
  50. import com.gitblit.models.NavLink.ExternalNavLink;
  51. import com.gitblit.models.NavLink.PageNavLink;
  52. import com.gitblit.models.ProjectModel;
  53. import com.gitblit.models.RefModel;
  54. import com.gitblit.models.RepositoryModel;
  55. import com.gitblit.models.SubmoduleModel;
  56. import com.gitblit.models.UserModel;
  57. import com.gitblit.models.UserRepositoryPreferences;
  58. import com.gitblit.servlet.PagesServlet;
  59. import com.gitblit.servlet.SyndicationServlet;
  60. import com.gitblit.utils.ArrayUtils;
  61. import com.gitblit.utils.BugtraqProcessor;
  62. import com.gitblit.utils.DeepCopier;
  63. import com.gitblit.utils.JGitUtils;
  64. import com.gitblit.utils.RefLogUtils;
  65. import com.gitblit.utils.StringUtils;
  66. import com.gitblit.wicket.CacheControl;
  67. import com.gitblit.wicket.GitBlitWebSession;
  68. import com.gitblit.wicket.SessionlessForm;
  69. import com.gitblit.wicket.WicketUtils;
  70. import com.gitblit.wicket.panels.LinkPanel;
  71. import com.gitblit.wicket.panels.NavigationPanel;
  72. import com.gitblit.wicket.panels.RefsPanel;
  73. public abstract class RepositoryPage extends RootPage {
  74. protected final Logger logger = LoggerFactory.getLogger(getClass());
  75. private final String PARAM_STAR = "star";
  76. protected final String projectName;
  77. protected final String repositoryName;
  78. protected final String objectId;
  79. private transient Repository r;
  80. private RepositoryModel m;
  81. private Map<String, SubmoduleModel> submodules;
  82. private boolean showAdmin;
  83. private boolean isOwner;
  84. public RepositoryPage(PageParameters params) {
  85. super(params);
  86. repositoryName = WicketUtils.getRepositoryName(params);
  87. String root = StringUtils.getFirstPathElement(repositoryName);
  88. if (StringUtils.isEmpty(root)) {
  89. projectName = app().settings().getString(Keys.web.repositoryRootGroupName, "main");
  90. } else {
  91. projectName = root;
  92. }
  93. objectId = WicketUtils.getObject(params);
  94. if (StringUtils.isEmpty(repositoryName)) {
  95. error(MessageFormat.format(getString("gb.repositoryNotSpecifiedFor"), getPageName()), true);
  96. }
  97. if (!getRepositoryModel().hasCommits && getClass() != EmptyRepositoryPage.class) {
  98. throw new RestartResponseException(EmptyRepositoryPage.class, params);
  99. }
  100. if (getRepositoryModel().isCollectingGarbage) {
  101. error(MessageFormat.format(getString("gb.busyCollectingGarbage"), getRepositoryModel().name), true);
  102. }
  103. if (objectId != null) {
  104. RefModel branch = null;
  105. if ((branch = JGitUtils.getBranch(getRepository(), objectId)) != null) {
  106. UserModel user = GitBlitWebSession.get().getUser();
  107. if (user == null) {
  108. // workaround until get().getUser() is reviewed throughout the app
  109. user = UserModel.ANONYMOUS;
  110. }
  111. boolean canAccess = user.canView(getRepositoryModel(),
  112. branch.reference.getName());
  113. if (!canAccess) {
  114. error(getString("gb.accessDenied"), true);
  115. }
  116. }
  117. }
  118. if (params.containsKey(PARAM_STAR)) {
  119. // set starred state
  120. boolean star = params.getBoolean(PARAM_STAR);
  121. UserModel user = GitBlitWebSession.get().getUser();
  122. if (user != null && user.isAuthenticated) {
  123. UserRepositoryPreferences prefs = user.getPreferences().getRepositoryPreferences(getRepositoryModel().name);
  124. prefs.starred = star;
  125. try {
  126. app().gitblit().reviseUser(user.username, user);
  127. } catch (GitBlitException e) {
  128. logger.error("Failed to update user " + user.username, e);
  129. error(getString("gb.failedToUpdateUser"), false);
  130. }
  131. }
  132. }
  133. showAdmin = false;
  134. if (app().settings().getBoolean(Keys.web.authenticateAdminPages, true)) {
  135. boolean allowAdmin = app().settings().getBoolean(Keys.web.allowAdministration, false);
  136. showAdmin = allowAdmin && GitBlitWebSession.get().canAdmin();
  137. } else {
  138. showAdmin = app().settings().getBoolean(Keys.web.allowAdministration, false);
  139. }
  140. isOwner = GitBlitWebSession.get().isLoggedIn()
  141. && (getRepositoryModel().isOwner(GitBlitWebSession.get().getUsername()));
  142. // register the available navigation links for this page and user
  143. List<NavLink> navLinks = registerNavLinks();
  144. // standard navigation links
  145. NavigationPanel navigationPanel = new NavigationPanel("repositoryNavPanel", getRepoNavPageClass(), navLinks);
  146. add(navigationPanel);
  147. add(new ExternalLink("syndication", SyndicationServlet.asLink(getRequest()
  148. .getRelativePathPrefixToContextRoot(), repositoryName, null, 0)));
  149. // add floating search form
  150. SearchForm searchForm = new SearchForm("searchForm", repositoryName);
  151. add(searchForm);
  152. searchForm.setTranslatedAttributes();
  153. // set stateless page preference
  154. setStatelessHint(true);
  155. }
  156. @Override
  157. protected Class<? extends BasePage> getRootNavPageClass() {
  158. return RepositoriesPage.class;
  159. }
  160. protected Class<? extends BasePage> getRepoNavPageClass() {
  161. return getClass();
  162. }
  163. protected BugtraqProcessor bugtraqProcessor() {
  164. return new BugtraqProcessor(app().settings());
  165. }
  166. private List<NavLink> registerNavLinks() {
  167. PageParameters params = null;
  168. if (!StringUtils.isEmpty(repositoryName)) {
  169. params = WicketUtils.newRepositoryParameter(repositoryName);
  170. }
  171. List<NavLink> navLinks = new ArrayList<NavLink>();
  172. Repository r = getRepository();
  173. RepositoryModel model = getRepositoryModel();
  174. // standard links
  175. if (RefLogUtils.getRefLogBranch(r) == null) {
  176. navLinks.add(new PageNavLink("gb.summary", SummaryPage.class, params));
  177. } else {
  178. navLinks.add(new PageNavLink("gb.summary", SummaryPage.class, params));
  179. // pages.put("overview", new PageRegistration("gb.overview", OverviewPage.class, params));
  180. navLinks.add(new PageNavLink("gb.reflog", ReflogPage.class, params));
  181. }
  182. if (!model.hasCommits) {
  183. return navLinks;
  184. }
  185. navLinks.add(new PageNavLink("gb.commits", LogPage.class, params));
  186. navLinks.add(new PageNavLink("gb.tree", TreePage.class, params));
  187. if (app().tickets().isReady() && (app().tickets().isAcceptingNewTickets(model) || app().tickets().hasTickets(model))) {
  188. PageParameters tParams = WicketUtils.newOpenTicketsParameter(repositoryName);
  189. navLinks.add(new PageNavLink("gb.tickets", TicketsPage.class, tParams));
  190. }
  191. navLinks.add(new PageNavLink("gb.docs", DocsPage.class, params, true));
  192. if (app().settings().getBoolean(Keys.web.allowForking, true)) {
  193. navLinks.add(new PageNavLink("gb.forks", ForksPage.class, params, true));
  194. }
  195. navLinks.add(new PageNavLink("gb.compare", ComparePage.class, params, true));
  196. // conditional links
  197. // per-repository extra navlinks
  198. if (JGitUtils.getPagesBranch(r) != null) {
  199. ExternalNavLink pagesLink = new ExternalNavLink("gb.pages", PagesServlet.asLink(
  200. getRequest().getRelativePathPrefixToContextRoot(), repositoryName, null), true);
  201. navLinks.add(pagesLink);
  202. }
  203. UserModel user = UserModel.ANONYMOUS;
  204. if (GitBlitWebSession.get().isLoggedIn()) {
  205. user = GitBlitWebSession.get().getUser();
  206. }
  207. // add repository nav link extensions
  208. List<RepositoryNavLinkExtension> extensions = app().plugins().getExtensions(RepositoryNavLinkExtension.class);
  209. for (RepositoryNavLinkExtension ext : extensions) {
  210. navLinks.addAll(ext.getNavLinks(user, model));
  211. }
  212. return navLinks;
  213. }
  214. protected boolean allowForkControls() {
  215. return app().settings().getBoolean(Keys.web.allowForking, true);
  216. }
  217. @Override
  218. protected void setupPage(String repositoryName, String pageName) {
  219. String projectName = StringUtils.getFirstPathElement(repositoryName);
  220. ProjectModel project = app().projects().getProjectModel(projectName);
  221. if (project.isUserProject()) {
  222. // user-as-project
  223. add(new LinkPanel("projectTitle", null, project.getDisplayName(),
  224. UserPage.class, WicketUtils.newUsernameParameter(project.name.substring(1))));
  225. } else {
  226. // project
  227. add(new LinkPanel("projectTitle", null, project.name,
  228. ProjectPage.class, WicketUtils.newProjectParameter(project.name)));
  229. }
  230. String name = StringUtils.stripDotGit(repositoryName);
  231. if (!StringUtils.isEmpty(projectName) && name.startsWith(projectName)) {
  232. name = name.substring(projectName.length() + 1);
  233. }
  234. add(new LinkPanel("repositoryName", null, name, SummaryPage.class,
  235. WicketUtils.newRepositoryParameter(repositoryName)));
  236. UserModel user = GitBlitWebSession.get().getUser();
  237. if (user == null) {
  238. user = UserModel.ANONYMOUS;
  239. }
  240. // indicate origin repository
  241. RepositoryModel model = getRepositoryModel();
  242. if (StringUtils.isEmpty(model.originRepository)) {
  243. if (model.isMirror) {
  244. Fragment mirrorFrag = new Fragment("originRepository", "mirrorFragment", this);
  245. Label lbl = new Label("originRepository", MessageFormat.format(getString("gb.mirrorOf"), "<b>" + model.origin + "</b>"));
  246. mirrorFrag.add(lbl.setEscapeModelStrings(false));
  247. add(mirrorFrag);
  248. } else {
  249. add(new Label("originRepository").setVisible(false));
  250. }
  251. } else {
  252. RepositoryModel origin = app().repositories().getRepositoryModel(model.originRepository);
  253. if (origin == null) {
  254. // no origin repository
  255. add(new Label("originRepository").setVisible(false));
  256. } else if (!user.canView(origin)) {
  257. // show origin repository without link
  258. Fragment forkFrag = new Fragment("originRepository", "originFragment", this);
  259. forkFrag.add(new Label("originRepository", StringUtils.stripDotGit(model.originRepository)));
  260. add(forkFrag);
  261. } else {
  262. // link to origin repository
  263. Fragment forkFrag = new Fragment("originRepository", "originFragment", this);
  264. forkFrag.add(new LinkPanel("originRepository", null, StringUtils.stripDotGit(model.originRepository),
  265. SummaryPage.class, WicketUtils.newRepositoryParameter(model.originRepository)));
  266. add(forkFrag);
  267. }
  268. }
  269. // new ticket button
  270. if (user.isAuthenticated && app().tickets().isAcceptingNewTickets(getRepositoryModel())) {
  271. String newTicketUrl = getRequestCycle().urlFor(NewTicketPage.class, WicketUtils.newRepositoryParameter(repositoryName)).toString();
  272. addToolbarButton("newTicketLink", "fa fa-ticket", getString("gb.new"), newTicketUrl);
  273. } else {
  274. add(new Label("newTicketLink").setVisible(false));
  275. }
  276. // (un)star link allows a user to star a repository
  277. if (user.isAuthenticated && model.hasCommits) {
  278. PageParameters starParams = DeepCopier.copy(getPageParameters());
  279. starParams.put(PARAM_STAR, !user.getPreferences().isStarredRepository(model.name));
  280. String toggleStarUrl = getRequestCycle().urlFor(getClass(), starParams).toString();
  281. if (user.getPreferences().isStarredRepository(model.name)) {
  282. // show unstar button
  283. add(new Label("starLink").setVisible(false));
  284. addToolbarButton("unstarLink", "icon-star-empty", getString("gb.unstar"), toggleStarUrl);
  285. } else {
  286. // show star button
  287. addToolbarButton("starLink", "icon-star", getString("gb.star"), toggleStarUrl);
  288. add(new Label("unstarLink").setVisible(false));
  289. }
  290. } else {
  291. // anonymous user
  292. add(new Label("starLink").setVisible(false));
  293. add(new Label("unstarLink").setVisible(false));
  294. }
  295. // fork controls
  296. if (!allowForkControls() || !user.isAuthenticated) {
  297. // must be logged-in to fork, hide all fork controls
  298. add(new ExternalLink("forkLink", "").setVisible(false));
  299. add(new ExternalLink("myForkLink", "").setVisible(false));
  300. } else {
  301. String fork = app().repositories().getFork(user.username, model.name);
  302. boolean hasFork = fork != null;
  303. boolean canFork = user.canFork(model) && model.hasCommits;
  304. if (hasFork || !canFork) {
  305. // user not allowed to fork or fork already exists or repo forbids forking
  306. add(new ExternalLink("forkLink", "").setVisible(false));
  307. if (hasFork && !fork.equals(model.name)) {
  308. // user has fork, view my fork link
  309. String url = getRequestCycle().urlFor(SummaryPage.class, WicketUtils.newRepositoryParameter(fork)).toString();
  310. add(new ExternalLink("myForkLink", url));
  311. } else {
  312. // no fork, hide view my fork link
  313. add(new ExternalLink("myForkLink", "").setVisible(false));
  314. }
  315. } else if (canFork) {
  316. // can fork and we do not have one
  317. add(new ExternalLink("myForkLink", "").setVisible(false));
  318. String url = getRequestCycle().urlFor(ForkPage.class, WicketUtils.newRepositoryParameter(model.name)).toString();
  319. add(new ExternalLink("forkLink", url));
  320. }
  321. }
  322. if (showAdmin || isOwner) {
  323. String url = getRequestCycle().urlFor(EditRepositoryPage.class, WicketUtils.newRepositoryParameter(model.name)).toString();
  324. add(new ExternalLink("editLink", url));
  325. } else {
  326. add(new Label("editLink").setVisible(false));
  327. }
  328. super.setupPage(repositoryName, pageName);
  329. }
  330. protected void addToolbarButton(String wicketId, String iconClass, String label, String url) {
  331. Fragment button = new Fragment(wicketId, "toolbarLinkFragment", this);
  332. Label icon = new Label("icon");
  333. WicketUtils.setCssClass(icon, iconClass);
  334. button.add(icon);
  335. button.add(new Label("label", label));
  336. button.add(new SimpleAttributeModifier("href", url));
  337. add(button);
  338. }
  339. protected void addSyndicationDiscoveryLink() {
  340. add(WicketUtils.syndicationDiscoveryLink(SyndicationServlet.getTitle(repositoryName,
  341. objectId), SyndicationServlet.asLink(getRequest()
  342. .getRelativePathPrefixToContextRoot(), repositoryName, objectId, 0)));
  343. }
  344. protected Repository getRepository() {
  345. if (r == null) {
  346. Repository r = app().repositories().getRepository(repositoryName);
  347. if (r == null) {
  348. error(getString("gb.canNotLoadRepository") + " " + repositoryName, true);
  349. return null;
  350. }
  351. this.r = r;
  352. }
  353. return r;
  354. }
  355. protected RepositoryModel getRepositoryModel() {
  356. if (m == null) {
  357. RepositoryModel model = app().repositories().getRepositoryModel(
  358. GitBlitWebSession.get().getUser(), repositoryName);
  359. if (model == null) {
  360. if (app().repositories().hasRepository(repositoryName, true)) {
  361. // has repository, but unauthorized
  362. authenticationError(getString("gb.unauthorizedAccessForRepository") + " " + repositoryName);
  363. } else {
  364. // does not have repository
  365. error(getString("gb.canNotLoadRepository") + " " + repositoryName, true);
  366. }
  367. return null;
  368. }
  369. m = model;
  370. }
  371. return m;
  372. }
  373. protected RevCommit getCommit() {
  374. RevCommit commit = JGitUtils.getCommit(r, objectId);
  375. if (commit == null) {
  376. error(MessageFormat.format(getString("gb.failedToFindCommit"),
  377. objectId, repositoryName, getPageName()), null, LogPage.class,
  378. WicketUtils.newRepositoryParameter(repositoryName));
  379. }
  380. getSubmodules(commit);
  381. return commit;
  382. }
  383. protected String getBestCommitId(RevCommit commit) {
  384. String head = null;
  385. try {
  386. head = r.resolve(getRepositoryModel().HEAD).getName();
  387. } catch (Exception e) {
  388. }
  389. String id = commit.getName();
  390. if (!StringUtils.isEmpty(head) && head.equals(id)) {
  391. // match default branch
  392. return Repository.shortenRefName(getRepositoryModel().HEAD);
  393. }
  394. // find first branch match
  395. for (RefModel ref : JGitUtils.getLocalBranches(r, false, -1)) {
  396. if (ref.getObjectId().getName().equals(id)) {
  397. return ref.getName();
  398. }
  399. }
  400. // return sha
  401. return id;
  402. }
  403. protected Map<String, SubmoduleModel> getSubmodules(RevCommit commit) {
  404. if (submodules == null) {
  405. submodules = new HashMap<String, SubmoduleModel>();
  406. for (SubmoduleModel model : JGitUtils.getSubmodules(r, commit.getTree())) {
  407. submodules.put(model.path, model);
  408. }
  409. }
  410. return submodules;
  411. }
  412. protected SubmoduleModel getSubmodule(String path) {
  413. SubmoduleModel model = null;
  414. if (submodules != null) {
  415. model = submodules.get(path);
  416. }
  417. if (model == null) {
  418. // undefined submodule?!
  419. model = new SubmoduleModel(path.substring(path.lastIndexOf('/') + 1), path, path);
  420. model.hasSubmodule = false;
  421. model.gitblitPath = model.name;
  422. return model;
  423. } else {
  424. // extract the repository name from the clone url
  425. List<String> patterns = app().settings().getStrings(Keys.git.submoduleUrlPatterns);
  426. String submoduleName = StringUtils.extractRepositoryPath(model.url, patterns.toArray(new String[0]));
  427. // determine the current path for constructing paths relative
  428. // to the current repository
  429. String currentPath = "";
  430. if (repositoryName.indexOf('/') > -1) {
  431. currentPath = repositoryName.substring(0, repositoryName.lastIndexOf('/') + 1);
  432. }
  433. // try to locate the submodule repository
  434. // prefer bare to non-bare names
  435. List<String> candidates = new ArrayList<String>();
  436. // relative
  437. candidates.add(currentPath + StringUtils.stripDotGit(submoduleName));
  438. candidates.add(candidates.get(candidates.size() - 1) + ".git");
  439. // relative, no subfolder
  440. if (submoduleName.lastIndexOf('/') > -1) {
  441. String name = submoduleName.substring(submoduleName.lastIndexOf('/') + 1);
  442. candidates.add(currentPath + StringUtils.stripDotGit(name));
  443. candidates.add(candidates.get(candidates.size() - 1) + ".git");
  444. }
  445. // absolute
  446. candidates.add(StringUtils.stripDotGit(submoduleName));
  447. candidates.add(candidates.get(candidates.size() - 1) + ".git");
  448. // absolute, no subfolder
  449. if (submoduleName.lastIndexOf('/') > -1) {
  450. String name = submoduleName.substring(submoduleName.lastIndexOf('/') + 1);
  451. candidates.add(StringUtils.stripDotGit(name));
  452. candidates.add(candidates.get(candidates.size() - 1) + ".git");
  453. }
  454. // create a unique, ordered set of candidate paths
  455. Set<String> paths = new LinkedHashSet<String>(candidates);
  456. for (String candidate : paths) {
  457. if (app().repositories().hasRepository(candidate)) {
  458. model.hasSubmodule = true;
  459. model.gitblitPath = candidate;
  460. return model;
  461. }
  462. }
  463. // we do not have a copy of the submodule, but we need a path
  464. model.gitblitPath = candidates.get(0);
  465. return model;
  466. }
  467. }
  468. protected String getShortObjectId(String objectId) {
  469. return objectId.substring(0, app().settings().getInteger(Keys.web.shortCommitIdLength, 6));
  470. }
  471. protected void addRefs(Repository r, RevCommit c) {
  472. add(new RefsPanel("refsPanel", repositoryName, c, JGitUtils.getAllRefs(r, getRepositoryModel().showRemoteBranches)));
  473. }
  474. protected void addFullText(String wicketId, String text) {
  475. RepositoryModel model = getRepositoryModel();
  476. String content = bugtraqProcessor().processCommitMessage(r, model, text);
  477. String html;
  478. switch (model.commitMessageRenderer) {
  479. case MARKDOWN:
  480. html = MessageFormat.format("<div class='commit_message'>{0}</div>", content);
  481. break;
  482. default:
  483. html = MessageFormat.format("<pre class='commit_message'>{0}</pre>", content);
  484. break;
  485. }
  486. add(new Label(wicketId, html).setEscapeModelStrings(false));
  487. }
  488. protected abstract String getPageName();
  489. protected Component createPersonPanel(String wicketId, PersonIdent identity,
  490. Constants.SearchType searchType) {
  491. String name = identity == null ? "" : identity.getName();
  492. String address = identity == null ? "" : identity.getEmailAddress();
  493. name = StringUtils.removeNewlines(name);
  494. address = StringUtils.removeNewlines(address);
  495. boolean showEmail = app().settings().getBoolean(Keys.web.showEmailAddresses, false);
  496. if (!showEmail || StringUtils.isEmpty(name) || StringUtils.isEmpty(address)) {
  497. String value = name;
  498. if (StringUtils.isEmpty(value)) {
  499. if (showEmail) {
  500. value = address;
  501. } else {
  502. value = getString("gb.missingUsername");
  503. }
  504. }
  505. Fragment partial = new Fragment(wicketId, "partialPersonIdent", this);
  506. LinkPanel link = new LinkPanel("personName", "list", value, GitSearchPage.class,
  507. WicketUtils.newSearchParameter(repositoryName, objectId, value, searchType));
  508. setPersonSearchTooltip(link, value, searchType);
  509. partial.add(link);
  510. return partial;
  511. } else {
  512. Fragment fullPerson = new Fragment(wicketId, "fullPersonIdent", this);
  513. LinkPanel nameLink = new LinkPanel("personName", "list", name, GitSearchPage.class,
  514. WicketUtils.newSearchParameter(repositoryName, objectId, name, searchType));
  515. setPersonSearchTooltip(nameLink, name, searchType);
  516. fullPerson.add(nameLink);
  517. LinkPanel addressLink = new LinkPanel("personAddress", "hidden-phone list", "<" + address + ">",
  518. GitSearchPage.class, WicketUtils.newSearchParameter(repositoryName, objectId,
  519. address, searchType));
  520. setPersonSearchTooltip(addressLink, address, searchType);
  521. fullPerson.add(addressLink);
  522. return fullPerson;
  523. }
  524. }
  525. protected void setPersonSearchTooltip(Component component, String value,
  526. Constants.SearchType searchType) {
  527. if (searchType.equals(Constants.SearchType.AUTHOR)) {
  528. WicketUtils.setHtmlTooltip(component, getString("gb.searchForAuthor") + " " + value);
  529. } else if (searchType.equals(Constants.SearchType.COMMITTER)) {
  530. WicketUtils.setHtmlTooltip(component, getString("gb.searchForCommitter") + " " + value);
  531. }
  532. }
  533. protected void setChangeTypeTooltip(Component container, ChangeType type) {
  534. switch (type) {
  535. case ADD:
  536. WicketUtils.setHtmlTooltip(container, getString("gb.addition"));
  537. break;
  538. case COPY:
  539. case RENAME:
  540. WicketUtils.setHtmlTooltip(container, getString("gb.rename"));
  541. break;
  542. case DELETE:
  543. WicketUtils.setHtmlTooltip(container, getString("gb.deletion"));
  544. break;
  545. case MODIFY:
  546. WicketUtils.setHtmlTooltip(container, getString("gb.modification"));
  547. break;
  548. }
  549. }
  550. @Override
  551. protected void onBeforeRender() {
  552. // dispose of repository object
  553. if (r != null) {
  554. r.close();
  555. r = null;
  556. }
  557. // setup page header and footer
  558. setupPage(repositoryName, "/ " + getPageName());
  559. super.onBeforeRender();
  560. }
  561. @Override
  562. protected void setLastModified() {
  563. if (getClass().isAnnotationPresent(CacheControl.class)) {
  564. CacheControl cacheControl = getClass().getAnnotation(CacheControl.class);
  565. switch (cacheControl.value()) {
  566. case REPOSITORY:
  567. RepositoryModel repository = getRepositoryModel();
  568. if (repository != null) {
  569. setLastModified(repository.lastChange);
  570. }
  571. break;
  572. case COMMIT:
  573. RevCommit commit = getCommit();
  574. if (commit != null) {
  575. Date commitDate = JGitUtils.getCommitDate(commit);
  576. setLastModified(commitDate);
  577. }
  578. break;
  579. default:
  580. super.setLastModified();
  581. }
  582. }
  583. }
  584. protected PageParameters newRepositoryParameter() {
  585. return WicketUtils.newRepositoryParameter(repositoryName);
  586. }
  587. protected PageParameters newCommitParameter() {
  588. return WicketUtils.newObjectParameter(repositoryName, objectId);
  589. }
  590. protected PageParameters newCommitParameter(String commitId) {
  591. return WicketUtils.newObjectParameter(repositoryName, commitId);
  592. }
  593. public boolean isShowAdmin() {
  594. return showAdmin;
  595. }
  596. public boolean isOwner() {
  597. return isOwner;
  598. }
  599. private class SearchForm extends SessionlessForm<Void> implements Serializable {
  600. private static final long serialVersionUID = 1L;
  601. private final String repositoryName;
  602. private final IModel<String> searchBoxModel = new Model<String>("");
  603. private final IModel<Constants.SearchType> searchTypeModel = new Model<Constants.SearchType>(
  604. Constants.SearchType.COMMIT);
  605. public SearchForm(String id, String repositoryName) {
  606. super(id, RepositoryPage.this.getClass(), RepositoryPage.this.getPageParameters());
  607. this.repositoryName = repositoryName;
  608. DropDownChoice<Constants.SearchType> searchType = new DropDownChoice<Constants.SearchType>(
  609. "searchType", Arrays.asList(Constants.SearchType.values()));
  610. searchType.setModel(searchTypeModel);
  611. add(searchType.setVisible(app().settings().getBoolean(Keys.web.showSearchTypeSelection, false)));
  612. TextField<String> searchBox = new TextField<String>("searchBox", searchBoxModel);
  613. add(searchBox);
  614. }
  615. void setTranslatedAttributes() {
  616. WicketUtils.setHtmlTooltip(get("searchType"), getString("gb.searchTypeTooltip"));
  617. WicketUtils.setHtmlTooltip(get("searchBox"),
  618. MessageFormat.format(getString("gb.searchTooltip"), repositoryName));
  619. WicketUtils.setInputPlaceholder(get("searchBox"), getString("gb.search"));
  620. }
  621. @Override
  622. public void onSubmit() {
  623. Constants.SearchType searchType = searchTypeModel.getObject();
  624. String searchString = searchBoxModel.getObject();
  625. if (StringUtils.isEmpty(searchString)) {
  626. // redirect to self to avoid wicket page update bug
  627. String absoluteUrl = getCanonicalUrl();
  628. getRequestCycle().setRequestTarget(new RedirectRequestTarget(absoluteUrl));
  629. return;
  630. }
  631. for (Constants.SearchType type : Constants.SearchType.values()) {
  632. if (searchString.toLowerCase().startsWith(type.name().toLowerCase() + ":")) {
  633. searchType = type;
  634. searchString = searchString.substring(type.name().toLowerCase().length() + 1)
  635. .trim();
  636. break;
  637. }
  638. }
  639. Class<? extends BasePage> searchPageClass = GitSearchPage.class;
  640. RepositoryModel model = app().repositories().getRepositoryModel(repositoryName);
  641. if (app().settings().getBoolean(Keys.web.allowLuceneIndexing, true)
  642. && !ArrayUtils.isEmpty(model.indexedBranches)) {
  643. // this repository is Lucene-indexed
  644. searchPageClass = LuceneSearchPage.class;
  645. }
  646. // use an absolute url to workaround Wicket-Tomcat problems with
  647. // mounted url parameters (issue-111)
  648. PageParameters params = WicketUtils.newSearchParameter(repositoryName, null, searchString, searchType);
  649. String absoluteUrl = getCanonicalUrl(searchPageClass, params);
  650. getRequestCycle().setRequestTarget(new RedirectRequestTarget(absoluteUrl));
  651. }
  652. }
  653. }