James Moger
2012-09-10 fabe060d3a435f116128851f828e35c2af5fde67
commit | author | age
13a3f5 1 /*
JM 2  * Copyright 2012 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
18 import java.io.File;
19 import java.io.FileInputStream;
20 import java.io.InputStreamReader;
21 import java.text.MessageFormat;
22 import java.text.SimpleDateFormat;
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.Comparator;
26 import java.util.HashMap;
27 import java.util.HashSet;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Set;
31
32 import org.apache.wicket.Component;
33 import org.apache.wicket.PageParameters;
34 import org.apache.wicket.RedirectException;
35 import org.apache.wicket.behavior.HeaderContributor;
36 import org.apache.wicket.markup.html.basic.Label;
37 import org.apache.wicket.markup.html.link.BookmarkablePageLink;
38 import org.apache.wicket.markup.html.link.ExternalLink;
39 import org.apache.wicket.markup.html.link.Link;
40 import org.apache.wicket.markup.html.panel.Fragment;
41 import org.apache.wicket.markup.repeater.Item;
42 import org.apache.wicket.markup.repeater.data.DataView;
43 import org.apache.wicket.markup.repeater.data.ListDataProvider;
44 import org.eclipse.jgit.lib.Constants;
45
46 import com.gitblit.GitBlit;
47 import com.gitblit.Keys;
48 import com.gitblit.SyndicationServlet;
49 import com.gitblit.models.Activity;
50 import com.gitblit.models.Metric;
51 import com.gitblit.models.ProjectModel;
52 import com.gitblit.models.RepositoryModel;
53 import com.gitblit.models.UserModel;
54 import com.gitblit.utils.ActivityUtils;
55 import com.gitblit.utils.ArrayUtils;
56 import com.gitblit.utils.MarkdownUtils;
57 import com.gitblit.utils.StringUtils;
58 import com.gitblit.wicket.GitBlitWebApp;
59 import com.gitblit.wicket.GitBlitWebSession;
60 import com.gitblit.wicket.PageRegistration;
61 import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
62 import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
63 import com.gitblit.wicket.WicketUtils;
64 import com.gitblit.wicket.charting.GoogleChart;
65 import com.gitblit.wicket.charting.GoogleCharts;
66 import com.gitblit.wicket.charting.GoogleLineChart;
67 import com.gitblit.wicket.charting.GooglePieChart;
68 import com.gitblit.wicket.panels.ActivityPanel;
69 import com.gitblit.wicket.panels.BasePanel.JavascriptEventConfirmation;
70 import com.gitblit.wicket.panels.LinkPanel;
71 import com.gitblit.wicket.panels.RepositoryUrlPanel;
72
73 public class ProjectPage extends RootPage {
74     
75     List<ProjectModel> projectModels = new ArrayList<ProjectModel>();
76
77     public ProjectPage() {
78         super();
79         throw new RedirectException(GitBlitWebApp.get().getHomePage());
80     }
81
82     public ProjectPage(PageParameters params) {
83         super(params);
84         setup(params);
85     }
86
87     @Override
88     protected boolean reusePageParameters() {
89         return true;
90     }
91
92     private void setup(PageParameters params) {
93         setupPage("", "");
94         // check to see if we should display a login message
95         boolean authenticateView = GitBlit.getBoolean(Keys.web.authenticateViewPages, true);
96         if (authenticateView && !GitBlitWebSession.get().isLoggedIn()) {
97             authenticationError("Please login");
98             return;
99         }
100
101         String projectName = WicketUtils.getProjectName(params);
102         if (StringUtils.isEmpty(projectName)) {
103             throw new RedirectException(GitBlitWebApp.get().getHomePage());
104         }
105         
106         ProjectModel project = getProjectModel(projectName);
107         if (project == null) {
108             throw new RedirectException(GitBlitWebApp.get().getHomePage());
109         }
110         
111         add(new Label("projectTitle", project.getDisplayName()));
112         add(new Label("projectDescription", project.description));
113         
114         String feedLink = SyndicationServlet.asLink(getRequest().getRelativePathPrefixToContextRoot(), projectName, null, 0);
115         add(new ExternalLink("syndication", feedLink));
116
117         add(WicketUtils.syndicationDiscoveryLink(SyndicationServlet.getTitle(project.getDisplayName(),
118                 null), feedLink));
119         
fabe06 120         final String projectPath;
13a3f5 121         if (project.isRoot) {
fabe06 122             projectPath = "";
13a3f5 123         } else {
fabe06 124             projectPath = projectName + "/";
13a3f5 125         }
JM 126         
127         // project markdown message
fabe06 128         File pmkd = new File(GitBlit.getRepositoriesFolder(),  projectPath + "project.mkd");
13a3f5 129         String pmessage = readMarkdown(projectName, pmkd);
JM 130         Component projectMessage = new Label("projectMessage", pmessage)
131                 .setEscapeModelStrings(false).setVisible(pmessage.length() > 0);
132         add(projectMessage);
133
134         // markdown message above repositories list
fabe06 135         File rmkd = new File(GitBlit.getRepositoriesFolder(),  projectPath + "repositories.mkd");
13a3f5 136         String rmessage = readMarkdown(projectName, rmkd);
JM 137         Component repositoriesMessage = new Label("repositoriesMessage", rmessage)
138                 .setEscapeModelStrings(false).setVisible(rmessage.length() > 0);
139         add(repositoriesMessage);
140
141         List<RepositoryModel> repositories = getRepositories(params);
142         
143         Collections.sort(repositories, new Comparator<RepositoryModel>() {
144             @Override
145             public int compare(RepositoryModel o1, RepositoryModel o2) {
146                 // reverse-chronological sort
147                 return o2.lastChange.compareTo(o1.lastChange);
148             }
149         });
150
151         final boolean showSwatch = GitBlit.getBoolean(Keys.web.repositoryListSwatches, true);
152         final boolean gitServlet = GitBlit.getBoolean(Keys.git.enableGitServlet, true);
153         final boolean showSize = GitBlit.getBoolean(Keys.web.showRepositorySizes, true);
154         
155         final ListDataProvider<RepositoryModel> dp = new ListDataProvider<RepositoryModel>(repositories);
156         DataView<RepositoryModel> dataView = new DataView<RepositoryModel>("repository", dp) {
157             private static final long serialVersionUID = 1L;
158
159             public void populateItem(final Item<RepositoryModel> item) {
160                 final RepositoryModel entry = item.getModelObject();
161
162                 // repository swatch
163                 Component swatch;
164                 if (entry.isBare){
165                     swatch = new Label("repositorySwatch", "&nbsp;").setEscapeModelStrings(false);
166                 } else {
167                     swatch = new Label("repositorySwatch", "!");
168                     WicketUtils.setHtmlTooltip(swatch, getString("gb.workingCopyWarning"));
169                 }
170                 WicketUtils.setCssBackground(swatch, entry.toString());
171                 item.add(swatch);
172                 swatch.setVisible(showSwatch);
173                 
174                 PageParameters pp = WicketUtils.newRepositoryParameter(entry.name);
fabe06 175                 item.add(new LinkPanel("repositoryName", "list", StringUtils.getRelativePath(projectPath, StringUtils.stripDotGit(entry.name)), SummaryPage.class, pp));
13a3f5 176                 item.add(new Label("repositoryDescription", entry.description).setVisible(!StringUtils.isEmpty(entry.description)));
JM 177                 
178                 item.add(new BookmarkablePageLink<Void>("tickets", TicketsPage.class, pp).setVisible(entry.useTickets));
179                 item.add(new BookmarkablePageLink<Void>("docs", DocsPage.class, pp).setVisible(entry.useDocs));
180
181                 if (entry.isFrozen) {
182                     item.add(WicketUtils.newImage("frozenIcon", "cold_16x16.png",
183                             getString("gb.isFrozen")));
184                 } else {
185                     item.add(WicketUtils.newClearPixel("frozenIcon").setVisible(false));
186                 }
187
188                 if (entry.isFederated) {
189                     item.add(WicketUtils.newImage("federatedIcon", "federated_16x16.png",
190                             getString("gb.isFederated")));
191                 } else {
192                     item.add(WicketUtils.newClearPixel("federatedIcon").setVisible(false));
193                 }
194                 switch (entry.accessRestriction) {
195                 case NONE:
196                     item.add(WicketUtils.newBlankImage("accessRestrictionIcon").setVisible(false));
197                     break;
198                 case PUSH:
199                     item.add(WicketUtils.newImage("accessRestrictionIcon", "lock_go_16x16.png",
200                             getAccessRestrictions().get(entry.accessRestriction)));
201                     break;
202                 case CLONE:
203                     item.add(WicketUtils.newImage("accessRestrictionIcon", "lock_pull_16x16.png",
204                             getAccessRestrictions().get(entry.accessRestriction)));
205                     break;
206                 case VIEW:
207                     item.add(WicketUtils.newImage("accessRestrictionIcon", "shield_16x16.png",
208                             getAccessRestrictions().get(entry.accessRestriction)));
209                     break;
210                 default:
211                     item.add(WicketUtils.newBlankImage("accessRestrictionIcon"));
212                 }
213
214                 item.add(new Label("repositoryOwner", StringUtils.isEmpty(entry.owner) ? "" : (entry.owner + " (" + getString("gb.owner") + ")")));
215                 
216                 
217                 UserModel user = GitBlitWebSession.get().getUser();
218                 Fragment repositoryLinks;                
219                 boolean showOwner = user != null && user.username.equalsIgnoreCase(entry.owner);
220                 if (showAdmin || showOwner) {
221                     repositoryLinks = new Fragment("repositoryLinks",
222                             showAdmin ? "repositoryAdminLinks" : "repositoryOwnerLinks", this);
223                     repositoryLinks.add(new BookmarkablePageLink<Void>("editRepository",
224                             EditRepositoryPage.class, WicketUtils
225                                     .newRepositoryParameter(entry.name)));
226                     if (showAdmin) {
227                         Link<Void> deleteLink = new Link<Void>("deleteRepository") {
228
229                             private static final long serialVersionUID = 1L;
230
231                             @Override
232                             public void onClick() {
233                                 if (GitBlit.self().deleteRepositoryModel(entry)) {
234                                     info(MessageFormat.format(getString("gb.repositoryDeleted"), entry));
235                                     // TODO dp.remove(entry);
236                                 } else {
237                                     error(MessageFormat.format(getString("gb.repositoryDeleteFailed"), entry));
238                                 }
239                             }
240                         };
241                         deleteLink.add(new JavascriptEventConfirmation("onclick", MessageFormat.format(
242                                 getString("gb.deleteRepository"), entry)));
243                         repositoryLinks.add(deleteLink);
244                     }
245                 } else {
246                     repositoryLinks = new Fragment("repositoryLinks", "repositoryUserLinks", this);
247                 }
248                 
249                 repositoryLinks.add(new BookmarkablePageLink<Void>("tree", TreePage.class,
250                         WicketUtils.newRepositoryParameter(entry.name)).setEnabled(entry.hasCommits));
251
252                 repositoryLinks.add(new BookmarkablePageLink<Void>("log", LogPage.class,
253                         WicketUtils.newRepositoryParameter(entry.name)).setEnabled(entry.hasCommits));
254
255                 item.add(repositoryLinks);
256                 
257                 String lastChange;
258                 if (entry.lastChange.getTime() == 0) {
259                     lastChange = "--";
260                 } else {
261                     lastChange = getTimeUtils().timeAgo(entry.lastChange);
262                 }
263                 Label lastChangeLabel = new Label("repositoryLastChange", lastChange);
264                 item.add(lastChangeLabel);
265                 WicketUtils.setCssClass(lastChangeLabel, getTimeUtils().timeAgoCss(entry.lastChange));
266                 
267                 if (entry.hasCommits) {
268                     // Existing repository
269                     item.add(new Label("repositorySize", entry.size).setVisible(showSize));
270                 } else {
271                     // New repository
272                     item.add(new Label("repositorySize", getString("gb.empty"))
273                             .setEscapeModelStrings(false));
274                 }
275                 
276                 item.add(new ExternalLink("syndication", SyndicationServlet.asLink("",
277                         entry.name, null, 0)));
278                 
279                 List<String> repositoryUrls = new ArrayList<String>();
280                 if (gitServlet) {
281                     // add the Gitblit repository url
282                     repositoryUrls.add(getRepositoryUrl(entry));
283                 }
284                 repositoryUrls.addAll(GitBlit.self().getOtherCloneUrls(entry.name));
285                 
286                 String primaryUrl = ArrayUtils.isEmpty(repositoryUrls) ? "" : repositoryUrls.remove(0);
287                 item.add(new RepositoryUrlPanel("repositoryCloneUrl", primaryUrl));
288             }
289         };
290         add(dataView);
291
292         // project activity
293         // parameters
294         int daysBack = WicketUtils.getDaysBack(params);
295         if (daysBack < 1) {
296             daysBack = 14;
297         }
298         String objectId = WicketUtils.getObject(params);
299
300         List<Activity> recentActivity = ActivityUtils.getRecentActivity(repositories, 
301                 daysBack, objectId, getTimeZone());
302         if (recentActivity.size() == 0) {
303             // no activity, skip graphs and activity panel
304             add(new Label("subheader", MessageFormat.format(getString("gb.recentActivityNone"),
305                     daysBack)));
306             add(new Label("activityPanel"));
307         } else {
308             // calculate total commits and total authors
309             int totalCommits = 0;
310             Set<String> uniqueAuthors = new HashSet<String>();
311             for (Activity activity : recentActivity) {
312                 totalCommits += activity.getCommitCount();
313                 uniqueAuthors.addAll(activity.getAuthorMetrics().keySet());
314             }
315             int totalAuthors = uniqueAuthors.size();
316
317             // add the subheader with stat numbers
318             add(new Label("subheader", MessageFormat.format(getString("gb.recentActivityStats"),
319                     daysBack, totalCommits, totalAuthors)));
320
321             // create the activity charts
322             GoogleCharts charts = createCharts(recentActivity);
323             add(new HeaderContributor(charts));
324
325             // add activity panel
326             add(new ActivityPanel("activityPanel", recentActivity));
327         }
328     }
329     
330     /**
331      * Creates the daily activity line chart, the active repositories pie chart,
332      * and the active authors pie chart
333      * 
334      * @param recentActivity
335      * @return
336      */
337     private GoogleCharts createCharts(List<Activity> recentActivity) {
338         // activity metrics
339         Map<String, Metric> repositoryMetrics = new HashMap<String, Metric>();
340         Map<String, Metric> authorMetrics = new HashMap<String, Metric>();
341
342         // aggregate repository and author metrics
343         for (Activity activity : recentActivity) {
344
345             // aggregate author metrics
346             for (Map.Entry<String, Metric> entry : activity.getAuthorMetrics().entrySet()) {
347                 String author = entry.getKey();
348                 if (!authorMetrics.containsKey(author)) {
349                     authorMetrics.put(author, new Metric(author));
350                 }
351                 authorMetrics.get(author).count += entry.getValue().count;
352             }
353
354             // aggregate repository metrics
355             for (Map.Entry<String, Metric> entry : activity.getRepositoryMetrics().entrySet()) {
356                 String repository = StringUtils.stripDotGit(entry.getKey());
357                 if (!repositoryMetrics.containsKey(repository)) {
358                     repositoryMetrics.put(repository, new Metric(repository));
359                 }
360                 repositoryMetrics.get(repository).count += entry.getValue().count;
361             }
362         }
363
364         // build google charts
365         int w = 310;
366         int h = 150;
367         GoogleCharts charts = new GoogleCharts();
368
369         // sort in reverse-chronological order and then reverse that
370         Collections.sort(recentActivity);
371         Collections.reverse(recentActivity);
372
373         // daily line chart
374         GoogleChart chart = new GoogleLineChart("chartDaily", getString("gb.dailyActivity"), "day",
375                 getString("gb.commits"));
376         SimpleDateFormat df = new SimpleDateFormat("MMM dd");
377         df.setTimeZone(getTimeZone());
378         for (Activity metric : recentActivity) {
379             chart.addValue(df.format(metric.startDate), metric.getCommitCount());
380         }
381         chart.setWidth(w);
382         chart.setHeight(h);
383         charts.addChart(chart);
384
385         // active repositories pie chart
386         chart = new GooglePieChart("chartRepositories", getString("gb.activeRepositories"),
387                 getString("gb.repository"), getString("gb.commits"));
388         for (Metric metric : repositoryMetrics.values()) {
389             chart.addValue(metric.name, metric.count);
390         }
391         chart.setWidth(w);
392         chart.setHeight(h);
393         charts.addChart(chart);
394
395         // active authors pie chart
396         chart = new GooglePieChart("chartAuthors", getString("gb.activeAuthors"),
397                 getString("gb.author"), getString("gb.commits"));
398         for (Metric metric : authorMetrics.values()) {
399             chart.addValue(metric.name, metric.count);
400         }
401         chart.setWidth(w);
402         chart.setHeight(h);
403         charts.addChart(chart);
404
405         return charts;
406     }
407
408     @Override
409     protected void addDropDownMenus(List<PageRegistration> pages) {
410         PageParameters params = getPageParameters();
411
412         DropDownMenuRegistration projects = new DropDownMenuRegistration("gb.projects",
413                 ProjectPage.class);
414         projects.menuItems.addAll(getProjectsMenu());
415         pages.add(0, projects);
416
417         DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters",
418                 ProjectPage.class);
419         // preserve time filter option on repository choices
420         menu.menuItems.addAll(getRepositoryFilterItems(params));
421
422         // preserve repository filter option on time choices
423         menu.menuItems.addAll(getTimeFilterItems(params));
424
425         if (menu.menuItems.size() > 0) {
426             // Reset Filter
427             menu.menuItems.add(new DropDownMenuItem(getString("gb.reset"), null, null));
428         }
429
430         pages.add(menu);
431     }
432     
433     @Override
434     protected List<ProjectModel> getProjectModels() {
435         if (projectModels.isEmpty()) {
436             final UserModel user = GitBlitWebSession.get().getUser();
437             List<ProjectModel> projects = GitBlit.self().getProjectModels(user);
438             projectModels.addAll(projects);
439         }
440         return projectModels;
441     }
442     
443     private ProjectModel getProjectModel(String name) {
444         for (ProjectModel project : getProjectModels()) {
445             if (name.equalsIgnoreCase(project.name)) {
446                 return project;
447             }
448         }
449         return null;
450     }
451     
452     protected List<DropDownMenuItem> getProjectsMenu() {
453         List<DropDownMenuItem> menu = new ArrayList<DropDownMenuItem>();
454         List<ProjectModel> projects = getProjectModels();
455         int maxProjects = 15;
456         boolean showAllProjects = projects.size() > maxProjects;
457         if (showAllProjects) {
458
459             // sort by last changed
460             Collections.sort(projects, new Comparator<ProjectModel>() {
461                 @Override
462                 public int compare(ProjectModel o1, ProjectModel o2) {
463                     return o2.lastChange.compareTo(o1.lastChange);
464                 }
465             });
466
467             // take most recent subset
468             projects = projects.subList(0, maxProjects);
469
470             // sort those by name
471             Collections.sort(projects);
472         }
473
474         for (ProjectModel project : projects) {
475             menu.add(new DropDownMenuItem(project.getDisplayName(), "p", project.name));
476         }
477         if (showAllProjects) {
478             menu.add(new DropDownMenuItem());
479             menu.add(new DropDownMenuItem("all projects", null, null));
480         }
481         return menu;
482     }
483
484
485     private String readMarkdown(String projectName, File projectMessage) {
486         String message = "";
487         if (projectMessage.exists()) {
488             // Read user-supplied message
489             try {
490                 FileInputStream fis = new FileInputStream(projectMessage);
491                 InputStreamReader reader = new InputStreamReader(fis,
492                         Constants.CHARACTER_ENCODING);
493                 message = MarkdownUtils.transformMarkdown(reader);
494                 reader.close();
495             } catch (Throwable t) {
496                 message = getString("gb.failedToRead") + " " + projectMessage;
497                 warn(message, t);
498             }
499         }
500         return message;
501     }
502 }